1 // FIXME: xml namespace support???
2 // FIXME: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
3 // FIXME: parentElement is parentNode that skips DocumentFragment etc but will be hard to work in with my compatibility...
4 
5 // FIXME: the scriptable list is quite arbitrary
6 
7 
8 // xml entity references?!
9 
10 /++
11 	This is an html DOM implementation, started with cloning
12 	what the browser offers in Javascript, but going well beyond
13 	it in convenience.
14 
15 	If you can do it in Javascript, you can probably do it with
16 	this module, and much more.
17 
18 	---
19 	import arsd.dom;
20 
21 	void main() {
22 		auto document = new Document("<html><p>paragraph</p></html>");
23 		writeln(document.querySelector("p"));
24 		document.root.innerHTML = "<p>hey</p>";
25 		writeln(document);
26 	}
27 	---
28 
29 	BTW: this file optionally depends on `arsd.characterencodings`, to
30 	help it correctly read files from the internet. You should be able to
31 	get characterencodings.d from the same place you got this file.
32 
33 	If you want it to stand alone, just always use the `Document.parseUtf8`
34 	function or the constructor that takes a string.
35 
36 	Symbol_groups:
37 
38 	core_functionality =
39 
40 	These members provide core functionality. The members on these classes
41 	will provide most your direct interaction.
42 
43 	bonus_functionality =
44 
45 	These provide additional functionality for special use cases.
46 
47 	implementations =
48 
49 	These provide implementations of other functionality.
50 +/
51 module arsd.dom;
52 
53 // FIXME: support the css standard namespace thing in the selectors too
54 
55 version(with_arsd_jsvar)
56 	import arsd.jsvar;
57 else {
58 	enum scriptable = "arsd_jsvar_compatible";
59 }
60 
61 // this is only meant to be used at compile time, as a filter for opDispatch
62 // lists the attributes we want to allow without the use of .attr
63 bool isConvenientAttribute(string name) {
64 	static immutable list = [
65 		"name", "id", "href", "value",
66 		"checked", "selected", "type",
67 		"src", "content", "pattern",
68 		"placeholder", "required", "alt",
69 		"rel",
70 		"method", "action", "enctype"
71 	];
72 	foreach(l; list)
73 		if(name == l) return true;
74 	return false;
75 }
76 
77 
78 // FIXME: something like <ol>spam <ol> with no closing </ol> should read the second tag as the closer in garbage mode
79 // FIXME: failing to close a paragraph sometimes messes things up too
80 
81 // FIXME: it would be kinda cool to have some support for internal DTDs
82 // and maybe XPath as well, to some extent
83 /*
84 	we could do
85 	meh this sux
86 
87 	auto xpath = XPath(element);
88 
89 	     // get the first p
90 	xpath.p[0].a["href"]
91 */
92 
93 
94 /++
95 	The main document interface, including a html or xml parser.
96 
97 	There's three main ways to create a Document:
98 
99 	If you want to parse something and inspect the tags, you can use the [this|constructor]:
100 	---
101 		// create and parse some HTML in one call
102 		auto document = new Document("<html></html>");
103 
104 		// or some XML
105 		auto document = new Document("<xml></xml>", true, true); // strict mode enabled
106 
107 		// or better yet:
108 		auto document = new XmlDocument("<xml></xml>"); // specialized subclass
109 	---
110 
111 	If you want to download something and parse it in one call, the [fromUrl] static function can help:
112 	---
113 		auto document = Document.fromUrl("http://dlang.org/");
114 	---
115 	(note that this requires my [arsd.characterencodings] and [arsd.http2] libraries)
116 
117 	And, if you need to inspect things like `<%= foo %>` tags and comments, you can add them to the dom like this, with the [enableAddingSpecialTagsToDom]
118 	and [parseUtf8] or [parseGarbage] functions:
119 	---
120 		auto document = new Document();
121 		document.enableAddingSpecialTagsToDom();
122 		document.parseUtf8("<example></example>", true, true); // changes the trues to false to switch from xml to html mode
123 	---
124 
125 	However you parse it, it will put a few things into special variables.
126 
127 	[root] contains the root document.
128 	[prolog] contains the instructions before the root (like `<!DOCTYPE html>`). To keep the original things, you will need to [enableAddingSpecialTagsToDom] first, otherwise the library will return generic strings in there. [piecesBeforeRoot] will have other parsed instructions, if [enableAddingSpecialTagsToDom] is called.
129 	[piecesAfterRoot] will contain any xml-looking data after the root tag is closed.
130 
131 	Most often though, you will not need to look at any of that data, since `Document` itself has methods like [querySelector], [appendChild], and more which will forward to the root [Element] for you.
132 +/
133 /// Group: core_functionality
134 class Document : FileResource, DomParent {
135 	inout(Document) asDocument() inout { return this; }
136 	inout(Element) asElement() inout { return null; }
137 
138 	void processNodeWhileParsing(Element parent, Element child) {
139 		parent.appendChild(child);
140 	}
141 
142 	/++
143 		Convenience method for web scraping. Requires [arsd.http2] to be
144 		included in the build as well as [arsd.characterencodings].
145 
146 		This will download the file from the given url and create a document
147 		off it, using a strict constructor or a [parseGarbage], depending on
148 		the value of `strictMode`.
149 	+/
150 	static Document fromUrl()(string url, bool strictMode = false) {
151 		import arsd.http2;
152 		auto client = new HttpClient();
153 
154 		auto req = client.navigateTo(Uri(url), HttpVerb.GET);
155 		auto res = req.waitForCompletion();
156 
157 		auto document = new Document();
158 		if(strictMode) {
159 			document.parse(cast(string) res.content, true, true, res.contentTypeCharset);
160 		} else {
161 			document.parseGarbage(cast(string) res.content);
162 		}
163 
164 		return document;
165 	}
166 
167 	/++
168 		Creates a document with the given source data. If you want HTML behavior, use `caseSensitive` and `struct` set to `false`. For XML mode, set them to `true`.
169 
170 		Please note that anything after the root element will be found in [piecesAfterRoot]. Comments, processing instructions, and other special tags will be stripped out b default. You can customize this by using the zero-argument constructor and setting callbacks on the [parseSawComment], [parseSawBangInstruction], [parseSawAspCode], [parseSawPhpCode], and [parseSawQuestionInstruction] members, then calling one of the [parseUtf8], [parseGarbage], or [parse] functions. Calling the convenience method, [enableAddingSpecialTagsToDom], will enable all those things at once.
171 
172 		See_Also:
173 			[parseGarbage]
174 			[parseUtf8]
175 			[parseUrl]
176 	+/
177 	this(string data, bool caseSensitive = false, bool strict = false) {
178 		parseUtf8(data, caseSensitive, strict);
179 	}
180 
181 	/**
182 		Creates an empty document. It has *nothing* in it at all, ready.
183 	*/
184 	this() {
185 
186 	}
187 
188 	/++
189 		This is just something I'm toying with. Right now, you use opIndex to put in css selectors.
190 		It returns a struct that forwards calls to all elements it holds, and returns itself so you
191 		can chain it.
192 
193 		Example: document["p"].innerText("hello").addClass("modified");
194 
195 		Equivalent to: foreach(e; document.getElementsBySelector("p")) { e.innerText("hello"); e.addClas("modified"); }
196 
197 		Note: always use function calls (not property syntax) and don't use toString in there for best results.
198 
199 		You can also do things like: document["p"]["b"] though tbh I'm not sure why since the selector string can do all that anyway. Maybe
200 		you could put in some kind of custom filter function tho.
201 	+/
202 	ElementCollection opIndex(string selector) {
203 		auto e = ElementCollection(this.root);
204 		return e[selector];
205 	}
206 
207 	string _contentType = "text/html; charset=utf-8";
208 
209 	/// If you're using this for some other kind of XML, you can
210 	/// set the content type here.
211 	///
212 	/// Note: this has no impact on the function of this class.
213 	/// It is only used if the document is sent via a protocol like HTTP.
214 	///
215 	/// This may be called by parse() if it recognizes the data. Otherwise,
216 	/// if you don't set it, it assumes text/html; charset=utf-8.
217 	@property string contentType(string mimeType) {
218 		_contentType = mimeType;
219 		return _contentType;
220 	}
221 
222 	/// implementing the FileResource interface, useful for sending via
223 	/// http automatically.
224 	@property string filename() const { return null; }
225 
226 	/// implementing the FileResource interface, useful for sending via
227 	/// http automatically.
228 	override @property string contentType() const {
229 		return _contentType;
230 	}
231 
232 	/// implementing the FileResource interface; it calls toString.
233 	override immutable(ubyte)[] getData() const {
234 		return cast(immutable(ubyte)[]) this.toString();
235 	}
236 
237 
238 	/*
239 	/// Concatenates any consecutive text nodes
240 	void normalize() {
241 
242 	}
243 	*/
244 
245 	/// This will set delegates for parseSaw* (note: this overwrites anything else you set, and you setting subsequently will overwrite this) that add those things to the dom tree when it sees them.
246 	/// Call this before calling parse().
247 
248 	/++
249 		Adds objects to the dom representing things normally stripped out during the default parse, like comments, `<!instructions>`, `<% code%>`, and `<? code?>` all at once.
250 
251 		Note this will also preserve the prolog and doctype from the original file, if there was one.
252 
253 		See_Also:
254 			[parseSawComment]
255 			[parseSawAspCode]
256 			[parseSawPhpCode]
257 			[parseSawQuestionInstruction]
258 			[parseSawBangInstruction]
259 	+/
260 	void enableAddingSpecialTagsToDom() {
261 		parseSawComment = (string) => true;
262 		parseSawAspCode = (string) => true;
263 		parseSawPhpCode = (string) => true;
264 		parseSawQuestionInstruction = (string) => true;
265 		parseSawBangInstruction = (string) => true;
266 	}
267 
268 	/// If the parser sees a html comment, it will call this callback
269 	/// <!-- comment --> will call parseSawComment(" comment ")
270 	/// Return true if you want the node appended to the document. It will be in a [HtmlComment] object.
271 	bool delegate(string) parseSawComment;
272 
273 	/// If the parser sees <% asp code... %>, it will call this callback.
274 	/// It will be passed "% asp code... %" or "%= asp code .. %"
275 	/// Return true if you want the node appended to the document. It will be in an [AspCode] object.
276 	bool delegate(string) parseSawAspCode;
277 
278 	/// If the parser sees <?php php code... ?>, it will call this callback.
279 	/// It will be passed "?php php code... ?" or "?= asp code .. ?"
280 	/// Note: dom.d cannot identify  the other php <? code ?> short format.
281 	/// Return true if you want the node appended to the document. It will be in a [PhpCode] object.
282 	bool delegate(string) parseSawPhpCode;
283 
284 	/// if it sees a <?xxx> that is not php or asp
285 	/// it calls this function with the contents.
286 	/// <?SOMETHING foo> calls parseSawQuestionInstruction("?SOMETHING foo")
287 	/// Unlike the php/asp ones, this ends on the first > it sees, without requiring ?>.
288 	/// Return true if you want the node appended to the document. It will be in a [QuestionInstruction] object.
289 	bool delegate(string) parseSawQuestionInstruction;
290 
291 	/// if it sees a <! that is not CDATA or comment (CDATA is handled automatically and comments call parseSawComment),
292 	/// it calls this function with the contents.
293 	/// <!SOMETHING foo> calls parseSawBangInstruction("SOMETHING foo")
294 	/// Return true if you want the node appended to the document. It will be in a [BangInstruction] object.
295 	bool delegate(string) parseSawBangInstruction;
296 
297 	/// Given the kind of garbage you find on the Internet, try to make sense of it.
298 	/// Equivalent to document.parse(data, false, false, null);
299 	/// (Case-insensitive, non-strict, determine character encoding from the data.)
300 
301 	/// NOTE: this makes no attempt at added security, but it will try to recover from anything instead of throwing.
302 	///
303 	/// It is a template so it lazily imports characterencodings.
304 	void parseGarbage()(string data) {
305 		parse(data, false, false, null);
306 	}
307 
308 	/// Parses well-formed UTF-8, case-sensitive, XML or XHTML
309 	/// Will throw exceptions on things like unclosed tags.
310 	void parseStrict(string data, bool pureXmlMode = false) {
311 		parseStream(toUtf8Stream(data), true, true, pureXmlMode);
312 	}
313 
314 	/// Parses well-formed UTF-8 in loose mode (by default). Tries to correct
315 	/// tag soup, but does NOT try to correct bad character encodings.
316 	///
317 	/// They will still throw an exception.
318 	void parseUtf8(string data, bool caseSensitive = false, bool strict = false) {
319 		parseStream(toUtf8Stream(data), caseSensitive, strict);
320 	}
321 
322 	// this is a template so we get lazy import behavior
323 	Utf8Stream handleDataEncoding()(in string rawdata, string dataEncoding, bool strict) {
324 		import arsd.characterencodings;
325 		// gotta determine the data encoding. If you know it, pass it in above to skip all this.
326 		if(dataEncoding is null) {
327 			dataEncoding = tryToDetermineEncoding(cast(const(ubyte[])) rawdata);
328 			// it can't tell... probably a random 8 bit encoding. Let's check the document itself.
329 			// Now, XML and HTML can both list encoding in the document, but we can't really parse
330 			// it here without changing a lot of code until we know the encoding. So I'm going to
331 			// do some hackish string checking.
332 			if(dataEncoding is null) {
333 				auto dataAsBytes = cast(immutable(ubyte)[]) rawdata;
334 				// first, look for an XML prolog
335 				auto idx = indexOfBytes(dataAsBytes, cast(immutable ubyte[]) "encoding=\"");
336 				if(idx != -1) {
337 					idx += "encoding=\"".length;
338 					// we're probably past the prolog if it's this far in; we might be looking at
339 					// content. Forget about it.
340 					if(idx > 100)
341 						idx = -1;
342 				}
343 				// if that fails, we're looking for Content-Type http-equiv or a meta charset (see html5)..
344 				if(idx == -1) {
345 					idx = indexOfBytes(dataAsBytes, cast(immutable ubyte[]) "charset=");
346 					if(idx != -1) {
347 						idx += "charset=".length;
348 						if(dataAsBytes[idx] == '"')
349 							idx++;
350 					}
351 				}
352 
353 				// found something in either branch...
354 				if(idx != -1) {
355 					// read till a quote or about 12 chars, whichever comes first...
356 					auto end = idx;
357 					while(end < dataAsBytes.length && dataAsBytes[end] != '"' && end - idx < 12)
358 						end++;
359 
360 					dataEncoding = cast(string) dataAsBytes[idx .. end];
361 				}
362 				// otherwise, we just don't know.
363 			}
364 		}
365 
366 		if(dataEncoding is null) {
367 			if(strict)
368 				throw new MarkupException("I couldn't figure out the encoding of this document.");
369 			else
370 			// if we really don't know by here, it means we already tried UTF-8,
371 			// looked for utf 16 and 32 byte order marks, and looked for xml or meta
372 			// tags... let's assume it's Windows-1252, since that's probably the most
373 			// common aside from utf that wouldn't be labeled.
374 
375 			dataEncoding = "Windows 1252";
376 		}
377 
378 		// and now, go ahead and convert it.
379 
380 		string data;
381 
382 		if(!strict) {
383 			// if we're in non-strict mode, we need to check
384 			// the document for mislabeling too; sometimes
385 			// web documents will say they are utf-8, but aren't
386 			// actually properly encoded. If it fails to validate,
387 			// we'll assume it's actually Windows encoding - the most
388 			// likely candidate for mislabeled garbage.
389 			dataEncoding = dataEncoding.toLower();
390 			dataEncoding = dataEncoding.replace(" ", "");
391 			dataEncoding = dataEncoding.replace("-", "");
392 			dataEncoding = dataEncoding.replace("_", "");
393 			if(dataEncoding == "utf8") {
394 				try {
395 					validate(rawdata);
396 				} catch(UTFException e) {
397 					dataEncoding = "Windows 1252";
398 				}
399 			}
400 		}
401 
402 		if(dataEncoding != "UTF-8") {
403 			if(strict)
404 				data = convertToUtf8(cast(immutable(ubyte)[]) rawdata, dataEncoding);
405 			else {
406 				try {
407 					data = convertToUtf8(cast(immutable(ubyte)[]) rawdata, dataEncoding);
408 				} catch(Exception e) {
409 					data = convertToUtf8(cast(immutable(ubyte)[]) rawdata, "Windows 1252");
410 				}
411 			}
412 		} else
413 			data = rawdata;
414 
415 		return toUtf8Stream(data);
416 	}
417 
418 	private
419 	Utf8Stream toUtf8Stream(in string rawdata) {
420 		string data = rawdata;
421 		static if(is(Utf8Stream == string))
422 			return data;
423 		else
424 			return new Utf8Stream(data);
425 	}
426 
427 	/++
428 		List of elements that can be assumed to be self-closed
429 		in this document. The default for a Document are a hard-coded
430 		list of ones appropriate for HTML. For [XmlDocument], it defaults
431 		to empty. You can modify this after construction but before parsing.
432 
433 		History:
434 			Added February 8, 2021 (included in dub release 9.2)
435 	+/
436 	string[] selfClosedElements = htmlSelfClosedElements;
437 
438 	/++
439 		List of elements that are considered inline for pretty printing.
440 		The default for a Document are hard-coded to something appropriate
441 		for HTML. For [XmlDocument], it defaults to empty. You can modify
442 		this after construction but before parsing.
443 
444 		History:
445 			Added June 21, 2021 (included in dub release 10.1)
446 	+/
447 	string[] inlineElements = htmlInlineElements;
448 
449 	/**
450 		Take XMLish data and try to make the DOM tree out of it.
451 
452 		The goal isn't to be perfect, but to just be good enough to
453 		approximate Javascript's behavior.
454 
455 		If strict, it throws on something that doesn't make sense.
456 		(Examples: mismatched tags. It doesn't validate!)
457 		If not strict, it tries to recover anyway, and only throws
458 		when something is REALLY unworkable.
459 
460 		If strict is false, it uses a magic list of tags that needn't
461 		be closed. If you are writing a document specifically for this,
462 		try to avoid such - use self closed tags at least. Easier to parse.
463 
464 		The dataEncoding argument can be used to pass a specific
465 		charset encoding for automatic conversion. If null (which is NOT
466 		the default!), it tries to determine from the data itself,
467 		using the xml prolog or meta tags, and assumes UTF-8 if unsure.
468 
469 		If this assumption is wrong, it can throw on non-ascii
470 		characters!
471 
472 
473 		Note that it previously assumed the data was encoded as UTF-8, which
474 		is why the dataEncoding argument defaults to that.
475 
476 		So it shouldn't break backward compatibility.
477 
478 		But, if you want the best behavior on wild data - figuring it out from the document
479 		instead of assuming - you'll probably want to change that argument to null.
480 
481 		This is a template so it lazily imports arsd.characterencodings, which is required
482 		to fix up data encodings.
483 
484 		If you are sure the encoding is good, try parseUtf8 or parseStrict to avoid the
485 		dependency. If it is data from the Internet though, a random website, the encoding
486 		is often a lie. This function, if dataEncoding == null, can correct for that, or
487 		you can try parseGarbage. In those cases, arsd.characterencodings is required to
488 		compile.
489 	*/
490 	void parse()(in string rawdata, bool caseSensitive = false, bool strict = false, string dataEncoding = "UTF-8") {
491 		auto data = handleDataEncoding(rawdata, dataEncoding, strict);
492 		parseStream(data, caseSensitive, strict);
493 	}
494 
495 	// note: this work best in strict mode, unless data is just a simple string wrapper
496 	void parseStream(Utf8Stream data, bool caseSensitive = false, bool strict = false, bool pureXmlMode = false) {
497 		// FIXME: this parser could be faster; it's in the top ten biggest tree times according to the profiler
498 		// of my big app.
499 
500 		assert(data !is null);
501 
502 		// go through character by character.
503 		// if you see a <, consider it a tag.
504 		// name goes until the first non tagname character
505 		// then see if it self closes or has an attribute
506 
507 		// if not in a tag, anything not a tag is a big text
508 		// node child. It ends as soon as it sees a <
509 
510 		// Whitespace in text or attributes is preserved, but not between attributes
511 
512 		// &amp; and friends are converted when I know them, left the same otherwise
513 
514 
515 		// this it should already be done correctly.. so I'm leaving it off to net a ~10% speed boost on my typical test file (really)
516 		//validate(data); // it *must* be UTF-8 for this to work correctly
517 
518 		sizediff_t pos = 0;
519 
520 		clear();
521 
522 		loose = !caseSensitive;
523 
524 		bool sawImproperNesting = false;
525 		bool paragraphHackfixRequired = false;
526 
527 		int getLineNumber(sizediff_t p) {
528 			int line = 1;
529 			foreach(c; data[0..p])
530 				if(c == '\n')
531 					line++;
532 			return line;
533 		}
534 
535 		void parseError(string message) {
536 			throw new MarkupException(format("char %d (line %d): %s", pos, getLineNumber(pos), message));
537 		}
538 
539 		bool eatWhitespace() {
540 			bool ateAny = false;
541 			while(pos < data.length && data[pos].isSimpleWhite) {
542 				pos++;
543 				ateAny = true;
544 			}
545 			return ateAny;
546 		}
547 
548 		string readTagName() {
549 			// remember to include : for namespaces
550 			// basically just keep going until >, /, or whitespace
551 			auto start = pos;
552 			while(data[pos] != '>' && data[pos] != '/' && !data[pos].isSimpleWhite)
553 			{
554 				pos++;
555 				if(pos == data.length) {
556 					if(strict)
557 						throw new Exception("tag name incomplete when file ended");
558 					else
559 						break;
560 				}
561 			}
562 
563 			if(!caseSensitive)
564 				return toLower(data[start..pos]);
565 			else
566 				return data[start..pos];
567 		}
568 
569 		string readAttributeName() {
570 			// remember to include : for namespaces
571 			// basically just keep going until >, /, or whitespace
572 			auto start = pos;
573 			while(data[pos] != '>' && data[pos] != '/'  && data[pos] != '=' && !data[pos].isSimpleWhite)
574 			{
575 				if(data[pos] == '<') {
576 					if(strict)
577 						throw new MarkupException("The character < can never appear in an attribute name. Line " ~ to!string(getLineNumber(pos)));
578 					else
579 						break; // e.g. <a href="something" <img src="poo" /></a>. The > should have been after the href, but some shitty files don't do that right and the browser handles it, so we will too, by pretending the > was indeed there
580 				}
581 				pos++;
582 				if(pos == data.length) {
583 					if(strict)
584 						throw new Exception("unterminated attribute name");
585 					else
586 						break;
587 				}
588 			}
589 
590 			if(!caseSensitive)
591 				return toLower(data[start..pos]);
592 			else
593 				return data[start..pos];
594 		}
595 
596 		string readAttributeValue() {
597 			if(pos >= data.length) {
598 				if(strict)
599 					throw new Exception("no attribute value before end of file");
600 				else
601 					return null;
602 			}
603 			switch(data[pos]) {
604 				case '\'':
605 				case '"':
606 					auto started = pos;
607 					char end = data[pos];
608 					pos++;
609 					auto start = pos;
610 					while(pos < data.length && data[pos] != end)
611 						pos++;
612 					if(strict && pos == data.length)
613 						throw new MarkupException("Unclosed attribute value, started on char " ~ to!string(started));
614 					string v = htmlEntitiesDecode(data[start..pos], strict);
615 					pos++; // skip over the end
616 				return v;
617 				default:
618 					if(strict)
619 						parseError("Attributes must be quoted");
620 					// read until whitespace or terminator (/> or >)
621 					auto start = pos;
622 					while(
623 						pos < data.length &&
624 						data[pos] != '>' &&
625 						// unquoted attributes might be urls, so gotta be careful with them and self-closed elements
626 						!(data[pos] == '/' && pos + 1 < data.length && data[pos+1] == '>') &&
627 						!data[pos].isSimpleWhite)
628 							pos++;
629 
630 					string v = htmlEntitiesDecode(data[start..pos], strict);
631 					// don't skip the end - we'll need it later
632 					return v;
633 			}
634 		}
635 
636 		TextNode readTextNode() {
637 			auto start = pos;
638 			while(pos < data.length && data[pos] != '<') {
639 				pos++;
640 			}
641 
642 			return TextNode.fromUndecodedString(this, data[start..pos]);
643 		}
644 
645 		// this is obsolete!
646 		RawSource readCDataNode() {
647 			auto start = pos;
648 			while(pos < data.length && data[pos] != '<') {
649 				pos++;
650 			}
651 
652 			return new RawSource(this, data[start..pos]);
653 		}
654 
655 
656 		struct Ele {
657 			int type; // element or closing tag or nothing
658 				/*
659 					type == 0 means regular node, self-closed (element is valid)
660 					type == 1 means closing tag (payload is the tag name, element may be valid)
661 					type == 2 means you should ignore it completely
662 					type == 3 means it is a special element that should be appended, if possible, e.g. a <!DOCTYPE> that was chosen to be kept, php code, or comment. It will be appended at the current element if inside the root, and to a special document area if not
663 					type == 4 means the document was totally empty
664 				*/
665 			Element element; // for type == 0 or type == 3
666 			string payload; // for type == 1
667 		}
668 		// recursively read a tag
669 		Ele readElement(string[] parentChain = null) {
670 			// FIXME: this is the slowest function in this module, by far, even in strict mode.
671 			// Loose mode should perform decently, but strict mode is the important one.
672 			if(!strict && parentChain is null)
673 				parentChain = [];
674 
675 			static string[] recentAutoClosedTags;
676 
677 			if(pos >= data.length)
678 			{
679 				if(strict) {
680 					throw new MarkupException("Gone over the input (is there no root element or did it never close?), chain: " ~ to!string(parentChain));
681 				} else {
682 					if(parentChain.length)
683 						return Ele(1, null, parentChain[0]); // in loose mode, we just assume the document has ended
684 					else
685 						return Ele(4); // signal emptiness upstream
686 				}
687 			}
688 
689 			if(data[pos] != '<') {
690 				return Ele(0, readTextNode(), null);
691 			}
692 
693 			enforce(data[pos] == '<');
694 			pos++;
695 			if(pos == data.length) {
696 				if(strict)
697 					throw new MarkupException("Found trailing < at end of file");
698 				// if not strict, we'll just skip the switch
699 			} else
700 			switch(data[pos]) {
701 				// I don't care about these, so I just want to skip them
702 				case '!': // might be a comment, a doctype, or a special instruction
703 					pos++;
704 
705 						// FIXME: we should store these in the tree too
706 						// though I like having it stripped out tbh.
707 
708 					if(pos == data.length) {
709 						if(strict)
710 							throw new MarkupException("<! opened at end of file");
711 					} else if(data[pos] == '-' && (pos + 1 < data.length) && data[pos+1] == '-') {
712 						// comment
713 						pos += 2;
714 
715 						// FIXME: technically, a comment is anything
716 						// between -- and -- inside a <!> block.
717 						// so in <!-- test -- lol> , the " lol" is NOT a comment
718 						// and should probably be handled differently in here, but for now
719 						// I'll just keep running until --> since that's the common way
720 
721 						auto commentStart = pos;
722 						while(pos+3 < data.length && data[pos..pos+3] != "-->")
723 							pos++;
724 
725 						auto end = commentStart;
726 
727 						if(pos + 3 >= data.length) {
728 							if(strict)
729 								throw new MarkupException("unclosed comment");
730 							end = data.length;
731 							pos = data.length;
732 						} else {
733 							end = pos;
734 							assert(data[pos] == '-');
735 							pos++;
736 							assert(data[pos] == '-');
737 							pos++;
738 							assert(data[pos] == '>');
739 							pos++;
740 						}
741 
742 						if(parseSawComment !is null)
743 							if(parseSawComment(data[commentStart .. end])) {
744 								return Ele(3, new HtmlComment(this, data[commentStart .. end]), null);
745 							}
746 					} else if(pos + 7 <= data.length && data[pos..pos + 7] == "[CDATA[") {
747 						pos += 7;
748 
749 						auto cdataStart = pos;
750 
751 						ptrdiff_t end = -1;
752 						typeof(end) cdataEnd;
753 
754 						if(pos < data.length) {
755 							// cdata isn't allowed to nest, so this should be generally ok, as long as it is found
756 							end = data[pos .. $].indexOf("]]>");
757 						}
758 
759 						if(end == -1) {
760 							if(strict)
761 								throw new MarkupException("Unclosed CDATA section");
762 							end = pos;
763 							cdataEnd = pos;
764 						} else {
765 							cdataEnd = pos + end;
766 							pos = cdataEnd + 3;
767 						}
768 
769 						return Ele(0, new TextNode(this, data[cdataStart .. cdataEnd]), null);
770 					} else {
771 						auto start = pos;
772 						while(pos < data.length && data[pos] != '>')
773 							pos++;
774 
775 						auto bangEnds = pos;
776 						if(pos == data.length) {
777 							if(strict)
778 								throw new MarkupException("unclosed processing instruction (<!xxx>)");
779 						} else pos++; // skipping the >
780 
781 						if(parseSawBangInstruction !is null)
782 							if(parseSawBangInstruction(data[start .. bangEnds])) {
783 								// FIXME: these should be able to modify the parser state,
784 								// doing things like adding entities, somehow.
785 
786 								return Ele(3, new BangInstruction(this, data[start .. bangEnds]), null);
787 							}
788 					}
789 
790 					/*
791 					if(pos < data.length && data[pos] == '>')
792 						pos++; // skip the >
793 					else
794 						assert(!strict);
795 					*/
796 				break;
797 				case '%':
798 				case '?':
799 					/*
800 						Here's what we want to support:
801 
802 						<% asp code %>
803 						<%= asp code %>
804 						<?php php code ?>
805 						<?= php code ?>
806 
807 						The contents don't really matter, just if it opens with
808 						one of the above for, it ends on the two char terminator.
809 
810 						<?something>
811 							this is NOT php code
812 							because I've seen this in the wild: <?EM-dummyText>
813 
814 							This could be php with shorttags which would be cut off
815 							prematurely because if(a >) - that > counts as the close
816 							of the tag, but since dom.d can't tell the difference
817 							between that and the <?EM> real world example, it will
818 							not try to look for the ?> ending.
819 
820 						The difference between this and the asp/php stuff is that it
821 						ends on >, not ?>. ONLY <?php or <?= ends on ?>. The rest end
822 						on >.
823 					*/
824 
825 					char end = data[pos];
826 					auto started = pos;
827 					bool isAsp = end == '%';
828 					int currentIndex = 0;
829 					bool isPhp = false;
830 					bool isEqualTag = false;
831 					int phpCount = 0;
832 
833 				    more:
834 					pos++; // skip the start
835 					if(pos == data.length) {
836 						if(strict)
837 							throw new MarkupException("Unclosed <"~end~" by end of file");
838 					} else {
839 						currentIndex++;
840 						if(currentIndex == 1 && data[pos] == '=') {
841 							if(!isAsp)
842 								isPhp = true;
843 							isEqualTag = true;
844 							goto more;
845 						}
846 						if(currentIndex == 1 && data[pos] == 'p')
847 							phpCount++;
848 						if(currentIndex == 2 && data[pos] == 'h')
849 							phpCount++;
850 						if(currentIndex == 3 && data[pos] == 'p' && phpCount == 2)
851 							isPhp = true;
852 
853 						if(data[pos] == '>') {
854 							if((isAsp || isPhp) && data[pos - 1] != end)
855 								goto more;
856 							// otherwise we're done
857 						} else
858 							goto more;
859 					}
860 
861 					//writefln("%s: %s", isAsp ? "ASP" : isPhp ? "PHP" : "<? ", data[started .. pos]);
862 					auto code = data[started .. pos];
863 
864 
865 					assert((pos < data.length && data[pos] == '>') || (!strict && pos == data.length));
866 					if(pos < data.length)
867 						pos++; // get past the >
868 
869 					if(isAsp && parseSawAspCode !is null) {
870 						if(parseSawAspCode(code)) {
871 							return Ele(3, new AspCode(this, code), null);
872 						}
873 					} else if(isPhp && parseSawPhpCode !is null) {
874 						if(parseSawPhpCode(code)) {
875 							return Ele(3, new PhpCode(this, code), null);
876 						}
877 					} else if(!isAsp && !isPhp && parseSawQuestionInstruction !is null) {
878 						if(parseSawQuestionInstruction(code)) {
879 							return Ele(3, new QuestionInstruction(this, code), null);
880 						}
881 					}
882 				break;
883 				case '/': // closing an element
884 					pos++; // skip the start
885 					auto p = pos;
886 					while(pos < data.length && data[pos] != '>')
887 						pos++;
888 					//writefln("</%s>", data[p..pos]);
889 					if(pos == data.length && data[pos-1] != '>') {
890 						if(strict)
891 							throw new MarkupException("File ended before closing tag had a required >");
892 						else
893 							data ~= ">"; // just hack it in
894 					}
895 					pos++; // skip the '>'
896 
897 					string tname = data[p..pos-1];
898 					if(!strict)
899 						tname = tname.strip;
900 					if(!caseSensitive)
901 						tname = tname.toLower();
902 
903 				return Ele(1, null, tname); // closing tag reports itself here
904 				case ' ': // assume it isn't a real element...
905 					if(strict) {
906 						parseError("bad markup - improperly placed <");
907 						assert(0); // parseError always throws
908 					} else
909 						return Ele(0, TextNode.fromUndecodedString(this, "<"), null);
910 				default:
911 
912 					if(!strict) {
913 						// what about something that kinda looks like a tag, but isn't?
914 						auto nextTag = data[pos .. $].indexOf("<");
915 						auto closeTag = data[pos .. $].indexOf(">");
916 						if(closeTag != -1 && nextTag != -1)
917 							if(nextTag < closeTag) {
918 								// since attribute names cannot possibly have a < in them, we'll look for an equal since it might be an attribute value... and even in garbage mode, it'd have to be a quoted one realistically
919 
920 								auto equal = data[pos .. $].indexOf("=\"");
921 								if(equal != -1 && equal < closeTag) {
922 									// this MIGHT be ok, soldier on
923 								} else {
924 									// definitely no good, this must be a (horribly distorted) text node
925 									pos++; // skip the < we're on - don't want text node to end prematurely
926 									auto node = readTextNode();
927 									node.contents = "<" ~ node.contents; // put this back
928 									return Ele(0, node, null);
929 								}
930 							}
931 					}
932 
933 					string tagName = readTagName();
934 					string[string] attributes;
935 
936 					Ele addTag(bool selfClosed) {
937 						if(selfClosed)
938 							pos++;
939 						else {
940 							if(!strict)
941 								if(tagName.isInArray(selfClosedElements))
942 									// these are de-facto self closed
943 									selfClosed = true;
944 						}
945 
946 						import std.algorithm.comparison;
947 
948 						if(strict) {
949 						enforce(data[pos] == '>', format("got %s when expecting > (possible missing attribute name)\nContext:\n%s", data[pos], data[max(0, pos - 100) .. min(data.length, pos + 100)]));
950 						} else {
951 							// if we got here, it's probably because a slash was in an
952 							// unquoted attribute - don't trust the selfClosed value
953 							if(!selfClosed)
954 								selfClosed = tagName.isInArray(selfClosedElements);
955 
956 							while(pos < data.length && data[pos] != '>')
957 								pos++;
958 
959 							if(pos >= data.length) {
960 								// the tag never closed
961 								assert(data.length != 0);
962 								pos = data.length - 1; // rewinding so it hits the end at the bottom..
963 							}
964 						}
965 
966 						auto whereThisTagStarted = pos; // for better error messages
967 
968 						pos++;
969 
970 						auto e = createElement(tagName);
971 						e.attributes = attributes;
972 						version(dom_node_indexes) {
973 							if(e.dataset.nodeIndex.length == 0)
974 								e.dataset.nodeIndex = to!string(&(e.attributes));
975 						}
976 						e.selfClosed = selfClosed;
977 						e.parseAttributes();
978 
979 
980 						// HACK to handle script and style as a raw data section as it is in HTML browsers
981 						if(!pureXmlMode && (tagName == "script" || tagName == "style")) {
982 							if(!selfClosed) {
983 								string closer = "</" ~ tagName ~ ">";
984 								ptrdiff_t ending;
985 								if(pos >= data.length)
986 									ending = -1;
987 								else
988 									ending = indexOf(data[pos..$], closer);
989 
990 								ending = indexOf(data[pos..$], closer, 0, (loose ? CaseSensitive.no : CaseSensitive.yes));
991 								/*
992 								if(loose && ending == -1 && pos < data.length)
993 									ending = indexOf(data[pos..$], closer.toUpper());
994 								*/
995 								if(ending == -1) {
996 									if(strict)
997 										throw new Exception("tag " ~ tagName ~ " never closed");
998 									else {
999 										// let's call it totally empty and do the rest of the file as text. doing it as html could still result in some weird stuff like if(a<4) being read as <4 being a tag so it comes out if(a<4></4> and other weirdness) It is either a closed script tag or the rest of the file is forfeit.
1000 										if(pos < data.length) {
1001 											e = new TextNode(this, data[pos .. $]);
1002 											pos = data.length;
1003 										}
1004 									}
1005 								} else {
1006 									ending += pos;
1007 									e.innerRawSource = data[pos..ending];
1008 									pos = ending + closer.length;
1009 								}
1010 							}
1011 							return Ele(0, e, null);
1012 						}
1013 
1014 						bool closed = selfClosed;
1015 
1016 						void considerHtmlParagraphHack(Element n) {
1017 							assert(!strict);
1018 							if(e.tagName == "p" && e.tagName == n.tagName) {
1019 								// html lets you write <p> para 1 <p> para 1
1020 								// but in the dom tree, they should be siblings, not children.
1021 								paragraphHackfixRequired = true;
1022 							}
1023 						}
1024 
1025 						//writef("<%s>", tagName);
1026 						while(!closed) {
1027 							Ele n;
1028 							if(strict)
1029 								n = readElement();
1030 							else
1031 								n = readElement(parentChain ~ tagName);
1032 
1033 							if(n.type == 4) return n; // the document is empty
1034 
1035 							if(n.type == 3 && n.element !is null) {
1036 								// special node, append if possible
1037 								if(e !is null)
1038 									processNodeWhileParsing(e, n.element);
1039 								else
1040 									piecesBeforeRoot ~= n.element;
1041 							} else if(n.type == 0) {
1042 								if(!strict)
1043 									considerHtmlParagraphHack(n.element);
1044 								processNodeWhileParsing(e, n.element);
1045 							} else if(n.type == 1) {
1046 								bool found = false;
1047 								if(n.payload != tagName) {
1048 									if(strict)
1049 										parseError(format("mismatched tag: </%s> != <%s> (opened on line %d)", n.payload, tagName, getLineNumber(whereThisTagStarted)));
1050 									else {
1051 										sawImproperNesting = true;
1052 										// this is so we don't drop several levels of awful markup
1053 										if(n.element) {
1054 											if(!strict)
1055 												considerHtmlParagraphHack(n.element);
1056 											processNodeWhileParsing(e, n.element);
1057 											n.element = null;
1058 										}
1059 
1060 										// is the element open somewhere up the chain?
1061 										foreach(i, parent; parentChain)
1062 											if(parent == n.payload) {
1063 												recentAutoClosedTags ~= tagName;
1064 												// just rotating it so we don't inadvertently break stuff with vile crap
1065 												if(recentAutoClosedTags.length > 4)
1066 													recentAutoClosedTags = recentAutoClosedTags[1 .. $];
1067 
1068 												n.element = e;
1069 												return n;
1070 											}
1071 
1072 										// if not, this is a text node; we can't fix it up...
1073 
1074 										// If it's already in the tree somewhere, assume it is closed by algorithm
1075 										// and we shouldn't output it - odds are the user just flipped a couple tags
1076 										foreach(ele; e.tree) {
1077 											if(ele.tagName == n.payload) {
1078 												found = true;
1079 												break;
1080 											}
1081 										}
1082 
1083 										foreach(ele; recentAutoClosedTags) {
1084 											if(ele == n.payload) {
1085 												found = true;
1086 												break;
1087 											}
1088 										}
1089 
1090 										if(!found) // if not found in the tree though, it's probably just text
1091 										processNodeWhileParsing(e, TextNode.fromUndecodedString(this, "</"~n.payload~">"));
1092 									}
1093 								} else {
1094 									if(n.element) {
1095 										if(!strict)
1096 											considerHtmlParagraphHack(n.element);
1097 										processNodeWhileParsing(e, n.element);
1098 									}
1099 								}
1100 
1101 								if(n.payload == tagName) // in strict mode, this is always true
1102 									closed = true;
1103 							} else { /*throw new Exception("wtf " ~ tagName);*/ }
1104 						}
1105 						//writef("</%s>\n", tagName);
1106 						return Ele(0, e, null);
1107 					}
1108 
1109 					// if a tag was opened but not closed by end of file, we can arrive here
1110 					if(!strict && pos >= data.length)
1111 						return addTag(false);
1112 					//else if(strict) assert(0); // should be caught before
1113 
1114 					switch(data[pos]) {
1115 						default: assert(0);
1116 						case '/': // self closing tag
1117 							return addTag(true);
1118 						case '>':
1119 							return addTag(false);
1120 						case ' ':
1121 						case '\t':
1122 						case '\n':
1123 						case '\r':
1124 							// there might be attributes...
1125 							moreAttributes:
1126 							eatWhitespace();
1127 
1128 							// same deal as above the switch....
1129 							if(!strict && pos >= data.length)
1130 								return addTag(false);
1131 
1132 							if(strict && pos >= data.length)
1133 								throw new MarkupException("tag open, didn't find > before end of file");
1134 
1135 							switch(data[pos]) {
1136 								case '/': // self closing tag
1137 									return addTag(true);
1138 								case '>': // closed tag; open -- we now read the contents
1139 									return addTag(false);
1140 								default: // it is an attribute
1141 									string attrName = readAttributeName();
1142 									string attrValue = attrName;
1143 
1144 									bool ateAny = eatWhitespace();
1145 									// the spec allows this too, sigh https://www.w3.org/TR/REC-xml/#NT-Eq
1146 									//if(strict && ateAny)
1147 										//throw new MarkupException("inappropriate whitespace after attribute name");
1148 
1149 									if(pos >= data.length) {
1150 										if(strict)
1151 											assert(0, "this should have thrown in readAttributeName");
1152 										else {
1153 											data ~= ">";
1154 											goto blankValue;
1155 										}
1156 									}
1157 									if(data[pos] == '=') {
1158 										pos++;
1159 
1160 										ateAny = eatWhitespace();
1161 										// the spec actually allows this!
1162 										//if(strict && ateAny)
1163 											//throw new MarkupException("inappropriate whitespace after attribute equals");
1164 
1165 										attrValue = readAttributeValue();
1166 
1167 										eatWhitespace();
1168 									}
1169 
1170 									blankValue:
1171 
1172 									if(strict && attrName in attributes)
1173 										throw new MarkupException("Repeated attribute: " ~ attrName);
1174 
1175 									if(attrName.strip().length)
1176 										attributes[attrName] = attrValue;
1177 									else if(strict) throw new MarkupException("wtf, zero length attribute name");
1178 
1179 									if(!strict && pos < data.length && data[pos] == '<') {
1180 										// this is the broken tag that doesn't have a > at the end
1181 										data = data[0 .. pos] ~ ">" ~ data[pos.. $];
1182 										// let's insert one as a hack
1183 										goto case '>';
1184 									}
1185 
1186 									goto moreAttributes;
1187 							}
1188 					}
1189 			}
1190 
1191 			return Ele(2, null, null); // this is a <! or <? thing that got ignored prolly.
1192 			//assert(0);
1193 		}
1194 
1195 		eatWhitespace();
1196 		Ele r;
1197 		do {
1198 			r = readElement(); // there SHOULD only be one element...
1199 
1200 			if(r.type == 3 && r.element !is null)
1201 				piecesBeforeRoot ~= r.element;
1202 
1203 			if(r.type == 4)
1204 				break; // the document is completely empty...
1205 		} while (r.type != 0 || r.element.nodeType != 1); // we look past the xml prologue and doctype; root only begins on a regular node
1206 
1207 		root = r.element;
1208 		if(root !is null)
1209 			root.parent_ = this;
1210 
1211 		if(!strict) // in strict mode, we'll just ignore stuff after the xml
1212 		while(r.type != 4) {
1213 			r = readElement();
1214 			if(r.type != 4 && r.type != 2) { // if not empty and not ignored
1215 				if(r.element !is null)
1216 					piecesAfterRoot ~= r.element;
1217 			}
1218 		}
1219 
1220 		if(root is null)
1221 		{
1222 			if(strict)
1223 				assert(0, "empty document should be impossible in strict mode");
1224 			else
1225 				parseUtf8(`<html><head></head><body></body></html>`); // fill in a dummy document in loose mode since that's what browsers do
1226 		}
1227 
1228 		if(paragraphHackfixRequired) {
1229 			assert(!strict); // this should never happen in strict mode; it ought to never set the hack flag...
1230 
1231 			// in loose mode, we can see some "bad" nesting (it's valid html, but poorly formed xml).
1232 			// It's hard to handle above though because my code sucks. So, we'll fix it here.
1233 
1234 			// Where to insert based on the parent (for mixed closed/unclosed <p> tags). See #120
1235 			// Kind of inefficient because we can't detect when we recurse back out of a node.
1236 			Element[Element] insertLocations;
1237 			auto iterator = root.tree;
1238 			foreach(ele; iterator) {
1239 				if(ele.parentNode is null)
1240 					continue;
1241 
1242 				if(ele.tagName == "p" && ele.parentNode.tagName == ele.tagName) {
1243 					auto shouldBePreviousSibling = ele.parentNode;
1244 					auto holder = shouldBePreviousSibling.parentNode; // this is the two element's mutual holder...
1245 					if (auto p = holder in insertLocations) {
1246 						shouldBePreviousSibling = *p;
1247 						assert(shouldBePreviousSibling.parentNode is holder);
1248 					}
1249 					ele = holder.insertAfter(shouldBePreviousSibling, ele.removeFromTree());
1250 					insertLocations[holder] = ele;
1251 					iterator.currentKilled(); // the current branch can be skipped; we'll hit it soon anyway since it's now next up.
1252 				}
1253 			}
1254 		}
1255 	}
1256 
1257 	/* end massive parse function */
1258 
1259 	/// Gets the <title> element's innerText, if one exists
1260 	@property string title() {
1261 		bool doesItMatch(Element e) {
1262 			return (e.tagName == "title");
1263 		}
1264 
1265 		auto e = findFirst(&doesItMatch);
1266 		if(e)
1267 			return e.innerText();
1268 		return "";
1269 	}
1270 
1271 	/// Sets the title of the page, creating a <title> element if needed.
1272 	@property void title(string t) {
1273 		bool doesItMatch(Element e) {
1274 			return (e.tagName == "title");
1275 		}
1276 
1277 		auto e = findFirst(&doesItMatch);
1278 
1279 		if(!e) {
1280 			e = createElement("title");
1281 			auto heads = getElementsByTagName("head");
1282 			if(heads.length)
1283 				heads[0].appendChild(e);
1284 		}
1285 
1286 		if(e)
1287 			e.innerText = t;
1288 	}
1289 
1290 	// FIXME: would it work to alias root this; ???? might be a good idea
1291 	/// These functions all forward to the root element. See the documentation in the Element class.
1292 	Element getElementById(string id) {
1293 		return root.getElementById(id);
1294 	}
1295 
1296 	/// ditto
1297 	final SomeElementType requireElementById(SomeElementType = Element)(string id, string file = __FILE__, size_t line = __LINE__)
1298 		if( is(SomeElementType : Element))
1299 		out(ret) { assert(ret !is null); }
1300 	do {
1301 		return root.requireElementById!(SomeElementType)(id, file, line);
1302 	}
1303 
1304 	/// ditto
1305 	final SomeElementType requireSelector(SomeElementType = Element)(string selector, string file = __FILE__, size_t line = __LINE__)
1306 		if( is(SomeElementType : Element))
1307 		out(ret) { assert(ret !is null); }
1308 	do {
1309 		auto e = cast(SomeElementType) querySelector(selector);
1310 		if(e is null)
1311 			throw new ElementNotFoundException(SomeElementType.stringof, selector, this.root, file, line);
1312 		return e;
1313 	}
1314 
1315 	/// ditto
1316 	final MaybeNullElement!SomeElementType optionSelector(SomeElementType = Element)(string selector, string file = __FILE__, size_t line = __LINE__)
1317 		if(is(SomeElementType : Element))
1318 	{
1319 		auto e = cast(SomeElementType) querySelector(selector);
1320 		return MaybeNullElement!SomeElementType(e);
1321 	}
1322 
1323 	/// ditto
1324 	@scriptable
1325 	Element querySelector(string selector) {
1326 		// see comment below on Document.querySelectorAll
1327 		auto s = Selector(selector);//, !loose);
1328 		foreach(ref comp; s.components)
1329 			if(comp.parts.length && comp.parts[0].separation == 0)
1330 				comp.parts[0].separation = -1;
1331 		foreach(e; s.getMatchingElementsLazy(this.root))
1332 			return e;
1333 		return null;
1334 
1335 	}
1336 
1337 	/// ditto
1338 	@scriptable
1339 	Element[] querySelectorAll(string selector) {
1340 		// In standards-compliant code, the document is slightly magical
1341 		// in that it is a pseudoelement at top level. It should actually
1342 		// match the root as one of its children.
1343 		//
1344 		// In versions of dom.d before Dec 29 2019, this worked because
1345 		// querySelectorAll was willing to return itself. With that bug fix
1346 		// (search "arbitrary id asduiwh" in this file for associated unittest)
1347 		// this would have failed. Hence adding back the root if it matches the
1348 		// selector itself.
1349 		//
1350 		// I'd love to do this better later.
1351 
1352 		auto s = Selector(selector);//, !loose);
1353 		foreach(ref comp; s.components)
1354 			if(comp.parts.length && comp.parts[0].separation == 0)
1355 				comp.parts[0].separation = -1;
1356 		return s.getMatchingElements(this.root, null);
1357 	}
1358 
1359 	/// ditto
1360 	deprecated("use querySelectorAll instead")
1361 	Element[] getElementsBySelector(string selector) {
1362 		return root.getElementsBySelector(selector);
1363 	}
1364 
1365 	/// ditto
1366 	@scriptable
1367 	Element[] getElementsByTagName(string tag) {
1368 		return root.getElementsByTagName(tag);
1369 	}
1370 
1371 	/// ditto
1372 	@scriptable
1373 	Element[] getElementsByClassName(string tag) {
1374 		return root.getElementsByClassName(tag);
1375 	}
1376 
1377 	/** FIXME: btw, this could just be a lazy range...... */
1378 	Element getFirstElementByTagName(string tag) {
1379 		if(loose)
1380 			tag = tag.toLower();
1381 		bool doesItMatch(Element e) {
1382 			return e.tagName == tag;
1383 		}
1384 		return findFirst(&doesItMatch);
1385 	}
1386 
1387 	/// This returns the <body> element, if there is one. (It different than Javascript, where it is called 'body', because body is a keyword in D.)
1388 	Element mainBody() {
1389 		return getFirstElementByTagName("body");
1390 	}
1391 
1392 	/// this uses a weird thing... it's [name=] if no colon and
1393 	/// [property=] if colon
1394 	string getMeta(string name) {
1395 		string thing = name.indexOf(":") == -1 ? "name" : "property";
1396 		auto e = querySelector("head meta["~thing~"="~name~"]");
1397 		if(e is null)
1398 			return null;
1399 		return e.content;
1400 	}
1401 
1402 	/// Sets a meta tag in the document header. It is kinda hacky to work easily for both Facebook open graph and traditional html meta tags/
1403 	void setMeta(string name, string value) {
1404 		string thing = name.indexOf(":") == -1 ? "name" : "property";
1405 		auto e = querySelector("head meta["~thing~"="~name~"]");
1406 		if(e is null) {
1407 			e = requireSelector("head").addChild("meta");
1408 			e.setAttribute(thing, name);
1409 		}
1410 
1411 		e.content = value;
1412 	}
1413 
1414 	///.
1415 	Form[] forms() {
1416 		return cast(Form[]) getElementsByTagName("form");
1417 	}
1418 
1419 	///.
1420 	Form createForm()
1421 		out(ret) {
1422 			assert(ret !is null);
1423 		}
1424 	do {
1425 		return cast(Form) createElement("form");
1426 	}
1427 
1428 	///.
1429 	Element createElement(string name) {
1430 		if(loose)
1431 			name = name.toLower();
1432 
1433 		auto e = Element.make(name, null, null, selfClosedElements);
1434 
1435 		return e;
1436 
1437 //		return new Element(this, name, null, selfClosed);
1438 	}
1439 
1440 	///.
1441 	Element createFragment() {
1442 		return new DocumentFragment(this);
1443 	}
1444 
1445 	///.
1446 	Element createTextNode(string content) {
1447 		return new TextNode(this, content);
1448 	}
1449 
1450 
1451 	///.
1452 	Element findFirst(bool delegate(Element) doesItMatch) {
1453 		if(root is null)
1454 			return null;
1455 		Element result;
1456 
1457 		bool goThroughElement(Element e) {
1458 			if(doesItMatch(e)) {
1459 				result = e;
1460 				return true;
1461 			}
1462 
1463 			foreach(child; e.children) {
1464 				if(goThroughElement(child))
1465 					return true;
1466 			}
1467 
1468 			return false;
1469 		}
1470 
1471 		goThroughElement(root);
1472 
1473 		return result;
1474 	}
1475 
1476 	///.
1477 	void clear() {
1478 		root = null;
1479 		loose = false;
1480 	}
1481 
1482 	private string _prolog = "<!DOCTYPE html>\n";
1483 	private bool prologWasSet = false; // set to true if the user changed it
1484 
1485 	/++
1486 		Returns or sets the string before the root element. This is, for example,
1487 		`<!DOCTYPE html>\n` or similar.
1488 	+/
1489 	@property string prolog() const {
1490 		// if the user explicitly changed it, do what they want
1491 		// or if we didn't keep/find stuff from the document itself,
1492 		// we'll use the builtin one as a default.
1493 		if(prologWasSet || piecesBeforeRoot.length == 0)
1494 			return _prolog;
1495 
1496 		string p;
1497 		foreach(e; piecesBeforeRoot)
1498 			p ~= e.toString() ~ "\n";
1499 		return p;
1500 	}
1501 
1502 	/// ditto
1503 	void setProlog(string d) {
1504 		_prolog = d;
1505 		prologWasSet = true;
1506 	}
1507 
1508 	/++
1509 		Returns the document as string form. Please note that if there is anything in [piecesAfterRoot],
1510 		they are discarded. If you want to add them to the file, loop over that and append it yourself
1511 		(but remember xml isn't supposed to have anything after the root element).
1512 	+/
1513 	override string toString() const {
1514 		return prolog ~ root.toString();
1515 	}
1516 
1517 	/++
1518 		Writes it out with whitespace for easier eyeball debugging
1519 
1520 		Do NOT use for anything other than eyeball debugging,
1521 		because whitespace may be significant content in XML.
1522 	+/
1523 	string toPrettyString(bool insertComments = false, int indentationLevel = 0, string indentWith = "\t") const {
1524 		import std.string;
1525 		string s = prolog.strip;
1526 
1527 		/*
1528 		if(insertComments) s ~= "<!--";
1529 		s ~= "\n";
1530 		if(insertComments) s ~= "-->";
1531 		*/
1532 
1533 		s ~= root.toPrettyStringImpl(insertComments, indentationLevel, indentWith);
1534 		foreach(a; piecesAfterRoot)
1535 			s ~= a.toPrettyStringImpl(insertComments, indentationLevel, indentWith);
1536 		return s;
1537 	}
1538 
1539 	/// The root element, like `<html>`. Most the methods on Document forward to this object.
1540 	Element root;
1541 
1542 	/// if these were kept, this is stuff that appeared before the root element, such as <?xml version ?> decls and <!DOCTYPE>s
1543 	Element[] piecesBeforeRoot;
1544 
1545 	/// stuff after the root, only stored in non-strict mode and not used in toString, but available in case you want it
1546 	Element[] piecesAfterRoot;
1547 
1548 	///.
1549 	bool loose;
1550 
1551 
1552 
1553 	// what follows are for mutation events that you can observe
1554 	void delegate(DomMutationEvent)[] eventObservers;
1555 
1556 	void dispatchMutationEvent(DomMutationEvent e) {
1557 		foreach(o; eventObservers)
1558 			o(e);
1559 	}
1560 }
1561 
1562 interface DomParent {
1563 	inout(Document) asDocument() inout;
1564 	inout(Element) asElement() inout;
1565 }
1566 
1567 /++
1568 	This represents almost everything in the DOM and offers a lot of inspection and manipulation functions. Element, or its subclasses, are what makes the dom tree.
1569 +/
1570 /// Group: core_functionality
1571 class Element : DomParent {
1572 	inout(Document) asDocument() inout { return null; }
1573 	inout(Element) asElement() inout { return this; }
1574 
1575 	/// Returns a collection of elements by selector.
1576 	/// See: [Document.opIndex]
1577 	ElementCollection opIndex(string selector) {
1578 		auto e = ElementCollection(this);
1579 		return e[selector];
1580 	}
1581 
1582 	/++
1583 		Returns the child node with the particular index.
1584 
1585 		Be aware that child nodes include text nodes, including
1586 		whitespace-only nodes.
1587 	+/
1588 	Element opIndex(size_t index) {
1589 		if(index >= children.length)
1590 			return null;
1591 		return this.children[index];
1592 	}
1593 
1594 	/// Calls getElementById, but throws instead of returning null if the element is not found. You can also ask for a specific subclass of Element to dynamically cast to, which also throws if it cannot be done.
1595 	final SomeElementType requireElementById(SomeElementType = Element)(string id, string file = __FILE__, size_t line = __LINE__)
1596 	if(
1597 		is(SomeElementType : Element)
1598 	)
1599 	out(ret) {
1600 		assert(ret !is null);
1601 	}
1602 	do {
1603 		auto e = cast(SomeElementType) getElementById(id);
1604 		if(e is null)
1605 			throw new ElementNotFoundException(SomeElementType.stringof, "id=" ~ id, this, file, line);
1606 		return e;
1607 	}
1608 
1609 	/// ditto but with selectors instead of ids
1610 	final SomeElementType requireSelector(SomeElementType = Element)(string selector, string file = __FILE__, size_t line = __LINE__)
1611 	if(
1612 		is(SomeElementType : Element)
1613 	)
1614 	out(ret) {
1615 		assert(ret !is null);
1616 	}
1617 	do {
1618 		auto e = cast(SomeElementType) querySelector(selector);
1619 		if(e is null)
1620 			throw new ElementNotFoundException(SomeElementType.stringof, selector, this, file, line);
1621 		return e;
1622 	}
1623 
1624 
1625 	/++
1626 		If a matching selector is found, it returns that Element. Otherwise, the returned object returns null for all methods.
1627 	+/
1628 	final MaybeNullElement!SomeElementType optionSelector(SomeElementType = Element)(string selector, string file = __FILE__, size_t line = __LINE__)
1629 		if(is(SomeElementType : Element))
1630 	{
1631 		auto e = cast(SomeElementType) querySelector(selector);
1632 		return MaybeNullElement!SomeElementType(e);
1633 	}
1634 
1635 
1636 
1637 	/// get all the classes on this element
1638 	@property string[] classes() const {
1639 		// FIXME: remove blank names
1640 		auto cs = split(className, " ");
1641 		foreach(ref c; cs)
1642 			c = c.strip();
1643 		return cs;
1644 	}
1645 
1646 	/++
1647 		The object [classList] returns.
1648 	+/
1649 	static struct ClassListHelper {
1650 		Element this_;
1651 		this(inout(Element) this_) inout {
1652 			this.this_ = this_;
1653 		}
1654 
1655 		///
1656 		bool contains(string cn) const {
1657 			return this_.hasClass(cn);
1658 		}
1659 
1660 		///
1661 		void add(string cn) {
1662 			this_.addClass(cn);
1663 		}
1664 
1665 		///
1666 		void remove(string cn) {
1667 			this_.removeClass(cn);
1668 		}
1669 
1670 		///
1671 		void toggle(string cn) {
1672 			if(contains(cn))
1673 				remove(cn);
1674 			else
1675 				add(cn);
1676 		}
1677 
1678 		// this thing supposed to be iterable in javascript but idk how i want to do it in D. meh
1679 		/+
1680 		string[] opIndex() const {
1681 			return this_.classes;
1682 		}
1683 		+/
1684 	}
1685 
1686 	/++
1687 		Returns a helper object to work with classes, just like javascript.
1688 
1689 		History:
1690 			Added August 25, 2022
1691 	+/
1692 	@property inout(ClassListHelper) classList() inout {
1693 		return inout(ClassListHelper)(this);
1694 	}
1695 	// FIXME: classList is supposed to whitespace and duplicates when you use it. need to test.
1696 
1697 	unittest {
1698 		Element element = Element.make("div");
1699 		element.classList.add("foo");
1700 		assert(element.classList.contains("foo"));
1701 		element.classList.remove("foo");
1702 		assert(!element.classList.contains("foo"));
1703 		element.classList.toggle("bar");
1704 		assert(element.classList.contains("bar"));
1705 	}
1706 
1707 	/// ditto
1708 	alias classNames = classes;
1709 
1710 
1711 	/// Adds a string to the class attribute. The class attribute is used a lot in CSS.
1712 	@scriptable
1713 	Element addClass(string c) {
1714 		if(hasClass(c))
1715 			return this; // don't add it twice
1716 
1717 		string cn = getAttribute("class");
1718 		if(cn.length == 0) {
1719 			setAttribute("class", c);
1720 			return this;
1721 		} else {
1722 			setAttribute("class", cn ~ " " ~ c);
1723 		}
1724 
1725 		return this;
1726 	}
1727 
1728 	/// Removes a particular class name.
1729 	@scriptable
1730 	Element removeClass(string c) {
1731 		if(!hasClass(c))
1732 			return this;
1733 		string n;
1734 		foreach(name; classes) {
1735 			if(c == name)
1736 				continue; // cut it out
1737 			if(n.length)
1738 				n ~= " ";
1739 			n ~= name;
1740 		}
1741 
1742 		className = n.strip();
1743 
1744 		return this;
1745 	}
1746 
1747 	/// Returns whether the given class appears in this element.
1748 	bool hasClass(string c) const {
1749 		string cn = className;
1750 
1751 		auto idx = cn.indexOf(c);
1752 		if(idx == -1)
1753 			return false;
1754 
1755 		foreach(cla; cn.split(" "))
1756 			if(cla.strip == c)
1757 				return true;
1758 		return false;
1759 
1760 		/*
1761 		int rightSide = idx + c.length;
1762 
1763 		bool checkRight() {
1764 			if(rightSide == cn.length)
1765 				return true; // it's the only class
1766 			else if(iswhite(cn[rightSide]))
1767 				return true;
1768 			return false; // this is a substring of something else..
1769 		}
1770 
1771 		if(idx == 0) {
1772 			return checkRight();
1773 		} else {
1774 			if(!iswhite(cn[idx - 1]))
1775 				return false; // substring
1776 			return checkRight();
1777 		}
1778 
1779 		assert(0);
1780 		*/
1781 	}
1782 
1783 
1784 	/* *******************************
1785 		  DOM Mutation
1786 	*********************************/
1787 	/++
1788 		Family of convenience functions to quickly add a tag with some text or
1789 		other relevant info (for example, it's a src for an <img> element
1790 		instead of inner text). They forward to [Element.make] then calls [appendChild].
1791 
1792 		---
1793 		div.addChild("span", "hello there");
1794 		div.addChild("div", Html("<p>children of the div</p>"));
1795 		---
1796 	+/
1797 	Element addChild(string tagName, string childInfo = null, string childInfo2 = null)
1798 		in {
1799 			assert(tagName !is null);
1800 		}
1801 		out(e) {
1802 			//assert(e.parentNode is this);
1803 			//assert(e.parentDocument is this.parentDocument);
1804 		}
1805 	do {
1806 		auto e = Element.make(tagName, childInfo, childInfo2);
1807 		// FIXME (maybe): if the thing is self closed, we might want to go ahead and
1808 		// return the parent. That will break existing code though.
1809 		return appendChild(e);
1810 	}
1811 
1812 	/// ditto
1813 	Element addChild(Element e) {
1814 		return this.appendChild(e);
1815 	}
1816 
1817 	/// ditto
1818 	Element addChild(string tagName, Element firstChild, string info2 = null)
1819 	in {
1820 		assert(firstChild !is null);
1821 	}
1822 	out(ret) {
1823 		assert(ret !is null);
1824 		assert(ret.parentNode is this);
1825 		assert(firstChild.parentNode is ret);
1826 
1827 		assert(ret.parentDocument is this.parentDocument);
1828 		//assert(firstChild.parentDocument is this.parentDocument);
1829 	}
1830 	do {
1831 		auto e = Element.make(tagName, "", info2);
1832 		e.appendChild(firstChild);
1833 		this.appendChild(e);
1834 		return e;
1835 	}
1836 
1837 	/// ditto
1838 	Element addChild(string tagName, in Html innerHtml, string info2 = null)
1839 	in {
1840 	}
1841 	out(ret) {
1842 		assert(ret !is null);
1843 		assert((cast(DocumentFragment) this !is null) || (ret.parentNode is this), ret.toString);// e.parentNode ? e.parentNode.toString : "null");
1844 		assert(ret.parentDocument is this.parentDocument);
1845 	}
1846 	do {
1847 		auto e = Element.make(tagName, "", info2);
1848 		this.appendChild(e);
1849 		e.innerHTML = innerHtml.source;
1850 		return e;
1851 	}
1852 
1853 
1854 	/// Another convenience function. Adds a child directly after the current one, returning
1855 	/// the new child.
1856 	///
1857 	/// Between this, addChild, and parentNode, you can build a tree as a single expression.
1858 	/// See_Also: [addChild]
1859 	Element addSibling(string tagName, string childInfo = null, string childInfo2 = null)
1860 		in {
1861 			assert(tagName !is null);
1862 			assert(parentNode !is null);
1863 		}
1864 		out(e) {
1865 			assert(e.parentNode is this.parentNode);
1866 			assert(e.parentDocument is this.parentDocument);
1867 		}
1868 	do {
1869 		auto e = Element.make(tagName, childInfo, childInfo2);
1870 		return parentNode.insertAfter(this, e);
1871 	}
1872 
1873 	/// ditto
1874 	Element addSibling(Element e) {
1875 		return parentNode.insertAfter(this, e);
1876 	}
1877 
1878 	/// Convenience function to append text intermixed with other children.
1879 	/// For example: div.addChildren("You can visit my website by ", new Link("mysite.com", "clicking here"), ".");
1880 	/// or div.addChildren("Hello, ", user.name, "!");
1881 	/// See also: appendHtml. This might be a bit simpler though because you don't have to think about escaping.
1882 	void addChildren(T...)(T t) {
1883 		foreach(item; t) {
1884 			static if(is(item : Element))
1885 				appendChild(item);
1886 			else static if (is(isSomeString!(item)))
1887 				appendText(to!string(item));
1888 			else static assert(0, "Cannot pass " ~ typeof(item).stringof ~ " to addChildren");
1889 		}
1890 	}
1891 
1892 	/// Appends the list of children to this element.
1893 	void appendChildren(Element[] children) {
1894 		foreach(ele; children)
1895 			appendChild(ele);
1896 	}
1897 
1898 	/// Removes this element form its current parent and appends it to the given `newParent`.
1899 	void reparent(Element newParent)
1900 		in {
1901 			assert(newParent !is null);
1902 			assert(parentNode !is null);
1903 		}
1904 		out {
1905 			assert(this.parentNode is newParent);
1906 			//assert(isInArray(this, newParent.children));
1907 		}
1908 	do {
1909 		parentNode.removeChild(this);
1910 		newParent.appendChild(this);
1911 	}
1912 
1913 	/**
1914 		Strips this tag out of the document, putting its inner html
1915 		as children of the parent.
1916 
1917 		For example, given: `<p>hello <b>there</b></p>`, if you
1918 		call `stripOut` on the `b` element, you'll be left with
1919 		`<p>hello there<p>`.
1920 
1921 		The idea here is to make it easy to get rid of garbage
1922 		markup you aren't interested in.
1923 	*/
1924 	void stripOut()
1925 		in {
1926 			assert(parentNode !is null);
1927 		}
1928 		out {
1929 			assert(parentNode is null);
1930 			assert(children.length == 0);
1931 		}
1932 	do {
1933 		foreach(c; children)
1934 			c.parentNode = null; // remove the parent
1935 		if(children.length)
1936 			parentNode.replaceChild(this, this.children);
1937 		else
1938 			parentNode.removeChild(this);
1939 		this.children.length = 0; // we reparented them all above
1940 	}
1941 
1942 	/// shorthand for `this.parentNode.removeChild(this)` with `parentNode` `null` check
1943 	/// if the element already isn't in a tree, it does nothing.
1944 	Element removeFromTree()
1945 		in {
1946 
1947 		}
1948 		out(var) {
1949 			assert(this.parentNode is null);
1950 			assert(var is this);
1951 		}
1952 	do {
1953 		if(this.parentNode is null)
1954 			return this;
1955 
1956 		this.parentNode.removeChild(this);
1957 
1958 		return this;
1959 	}
1960 
1961 	/++
1962 		Wraps this element inside the given element.
1963 		It's like `this.replaceWith(what); what.appendchild(this);`
1964 
1965 		Given: `<b>cool</b>`, if you call `b.wrapIn(new Link("site.com", "my site is "));`
1966 		you'll end up with: `<a href="site.com">my site is <b>cool</b></a>`.
1967 	+/
1968 	Element wrapIn(Element what)
1969 		in {
1970 			assert(what !is null);
1971 		}
1972 		out(ret) {
1973 			assert(this.parentNode is what);
1974 			assert(ret is what);
1975 		}
1976 	do {
1977 		this.replaceWith(what);
1978 		what.appendChild(this);
1979 
1980 		return what;
1981 	}
1982 
1983 	/// Replaces this element with something else in the tree.
1984 	Element replaceWith(Element e)
1985 	in {
1986 		assert(this.parentNode !is null);
1987 	}
1988 	do {
1989 		e.removeFromTree();
1990 		this.parentNode.replaceChild(this, e);
1991 		return e;
1992 	}
1993 
1994 	/**
1995 		Fetches the first consecutive text nodes concatenated together.
1996 
1997 
1998 		`firstInnerText` of `<example>some text<span>more text</span></example>` is `some text`. It stops at the first child tag encountered.
1999 
2000 		See_also: [directText], [innerText]
2001 	*/
2002 	string firstInnerText() const {
2003 		string s;
2004 		foreach(child; children) {
2005 			if(child.nodeType != NodeType.Text)
2006 				break;
2007 
2008 			s ~= child.nodeValue();
2009 		}
2010 		return s;
2011 	}
2012 
2013 
2014 	/**
2015 		Returns the text directly under this element.
2016 
2017 
2018 		Unlike [innerText], it does not recurse, and unlike [firstInnerText], it continues
2019 		past child tags. So, `<example>some <b>bold</b> text</example>`
2020 		will return `some  text` because it only gets the text, skipping non-text children.
2021 
2022 		See_also: [firstInnerText], [innerText]
2023 	*/
2024 	@property string directText() {
2025 		string ret;
2026 		foreach(e; children) {
2027 			if(e.nodeType == NodeType.Text)
2028 				ret ~= e.nodeValue();
2029 		}
2030 
2031 		return ret;
2032 	}
2033 
2034 	/**
2035 		Sets the direct text, without modifying other child nodes.
2036 
2037 
2038 		Unlike [innerText], this does *not* remove existing elements in the element.
2039 
2040 		It only replaces the first text node it sees.
2041 
2042 		If there are no text nodes, it calls [appendText].
2043 
2044 		So, given `<div><img />text here</div>`, it will keep the `<img />`, and replace the `text here`.
2045 	*/
2046 	@property void directText(string text) {
2047 		foreach(e; children) {
2048 			if(e.nodeType == NodeType.Text) {
2049 				auto it = cast(TextNode) e;
2050 				it.contents = text;
2051 				return;
2052 			}
2053 		}
2054 
2055 		appendText(text);
2056 	}
2057 
2058 	// do nothing, this is primarily a virtual hook
2059 	// for links and forms
2060 	void setValue(string field, string value) { }
2061 
2062 
2063 	// this is a thing so i can remove observer support if it gets slow
2064 	// I have not implemented all these yet
2065 	private void sendObserverEvent(DomMutationOperations operation, string s1 = null, string s2 = null, Element r = null, Element r2 = null) {
2066 		if(parentDocument is null) return;
2067 		DomMutationEvent me;
2068 		me.operation = operation;
2069 		me.target = this;
2070 		me.relatedString = s1;
2071 		me.relatedString2 = s2;
2072 		me.related = r;
2073 		me.related2 = r2;
2074 		parentDocument.dispatchMutationEvent(me);
2075 	}
2076 
2077 	// putting all the members up front
2078 
2079 	// this ought to be private. don't use it directly.
2080 	Element[] children;
2081 
2082 	/// The name of the tag. Remember, changing this doesn't change the dynamic type of the object.
2083 	string tagName;
2084 
2085 	/// This is where the attributes are actually stored. You should use getAttribute, setAttribute, and hasAttribute instead.
2086 	string[string] attributes;
2087 
2088 	/// In XML, it is valid to write <tag /> for all elements with no children, but that breaks HTML, so I don't do it here.
2089 	/// Instead, this flag tells if it should be. It is based on the source document's notation and a html element list.
2090 	private bool selfClosed;
2091 
2092 	private DomParent parent_;
2093 
2094 	/// Get the parent Document object that contains this element.
2095 	/// It may be null, so remember to check for that.
2096 	@property inout(Document) parentDocument() inout {
2097 		if(this.parent_ is null)
2098 			return null;
2099 		auto p = cast() this.parent_.asElement;
2100 		auto prev = cast() this;
2101 		while(p) {
2102 			prev = p;
2103 			if(p.parent_ is null)
2104 				return null;
2105 			p = cast() p.parent_.asElement;
2106 		}
2107 		return cast(inout) prev.parent_.asDocument;
2108 	}
2109 
2110 	/*deprecated*/ @property void parentDocument(Document doc) {
2111 		parent_ = doc;
2112 	}
2113 
2114 	/// Returns the parent node in the tree this element is attached to.
2115 	inout(Element) parentNode() inout {
2116 		if(parent_ is null)
2117 			return null;
2118 
2119 		auto p = parent_.asElement;
2120 
2121 		if(cast(DocumentFragment) p) {
2122 			if(p.parent_ is null)
2123 				return null;
2124 			else
2125 				return p.parent_.asElement;
2126 		}
2127 
2128 		return p;
2129 	}
2130 
2131 	//protected
2132 	Element parentNode(Element e) {
2133 		parent_ = e;
2134 		return e;
2135 	}
2136 
2137 	// these are here for event handlers. Don't forget that this library never fires events.
2138 	// (I'm thinking about putting this in a version statement so you don't have the baggage. The instance size of this class is 56 bytes right now.)
2139 
2140 	version(dom_with_events) {
2141 		EventHandler[][string] bubblingEventHandlers;
2142 		EventHandler[][string] capturingEventHandlers;
2143 		EventHandler[string] defaultEventHandlers;
2144 
2145 		void addEventListener(string event, EventHandler handler, bool useCapture = false) {
2146 			if(event.length > 2 && event[0..2] == "on")
2147 				event = event[2 .. $];
2148 
2149 			if(useCapture)
2150 				capturingEventHandlers[event] ~= handler;
2151 			else
2152 				bubblingEventHandlers[event] ~= handler;
2153 		}
2154 	}
2155 
2156 
2157 	// and now methods
2158 
2159 	/++
2160 		Convenience function to try to do the right thing for HTML. This is the main way I create elements.
2161 
2162 		History:
2163 			On February 8, 2021, the `selfClosedElements` parameter was added. Previously, it used a private
2164 			immutable global list for HTML. It still defaults to the same list, but you can change it now via
2165 			the parameter.
2166 		See_Also:
2167 			[addChild], [addSibling]
2168 	+/
2169 	static Element make(string tagName, string childInfo = null, string childInfo2 = null, const string[] selfClosedElements = htmlSelfClosedElements) {
2170 		bool selfClosed = tagName.isInArray(selfClosedElements);
2171 
2172 		Element e;
2173 		// want to create the right kind of object for the given tag...
2174 		switch(tagName) {
2175 			case "#text":
2176 				e = new TextNode(null, childInfo);
2177 				return e;
2178 			// break;
2179 			case "table":
2180 				e = new Table(null);
2181 			break;
2182 			case "a":
2183 				e = new Link(null);
2184 			break;
2185 			case "form":
2186 				e = new Form(null);
2187 			break;
2188 			case "tr":
2189 				e = new TableRow(null);
2190 			break;
2191 			case "td", "th":
2192 				e = new TableCell(null, tagName);
2193 			break;
2194 			default:
2195 				e = new Element(null, tagName, null, selfClosed); // parent document should be set elsewhere
2196 		}
2197 
2198 		// make sure all the stuff is constructed properly FIXME: should probably be in all the right constructors too
2199 		e.tagName = tagName;
2200 		e.selfClosed = selfClosed;
2201 
2202 		if(childInfo !is null)
2203 			switch(tagName) {
2204 				/* html5 convenience tags */
2205 				case "audio":
2206 					if(childInfo.length)
2207 						e.addChild("source", childInfo);
2208 					if(childInfo2 !is null)
2209 						e.appendText(childInfo2);
2210 				break;
2211 				case "source":
2212 					e.src = childInfo;
2213 					if(childInfo2 !is null)
2214 						e.type = childInfo2;
2215 				break;
2216 				/* regular html 4 stuff */
2217 				case "img":
2218 					e.src = childInfo;
2219 					if(childInfo2 !is null)
2220 						e.alt = childInfo2;
2221 				break;
2222 				case "link":
2223 					e.href = childInfo;
2224 					if(childInfo2 !is null)
2225 						e.rel = childInfo2;
2226 				break;
2227 				case "option":
2228 					e.innerText = childInfo;
2229 					if(childInfo2 !is null)
2230 						e.value = childInfo2;
2231 				break;
2232 				case "input":
2233 					e.type = "hidden";
2234 					e.name = childInfo;
2235 					if(childInfo2 !is null)
2236 						e.value = childInfo2;
2237 				break;
2238 				case "button":
2239 					e.innerText = childInfo;
2240 					if(childInfo2 !is null)
2241 						e.type = childInfo2;
2242 				break;
2243 				case "a":
2244 					e.innerText = childInfo;
2245 					if(childInfo2 !is null)
2246 						e.href = childInfo2;
2247 				break;
2248 				case "script":
2249 				case "style":
2250 					e.innerRawSource = childInfo;
2251 				break;
2252 				case "meta":
2253 					e.name = childInfo;
2254 					if(childInfo2 !is null)
2255 						e.content = childInfo2;
2256 				break;
2257 				/* generically, assume we were passed text and perhaps class */
2258 				default:
2259 					e.innerText = childInfo;
2260 					if(childInfo2.length)
2261 						e.className = childInfo2;
2262 			}
2263 
2264 		return e;
2265 	}
2266 
2267 	/// ditto
2268 	static Element make(string tagName, in Html innerHtml, string childInfo2 = null) {
2269 		// FIXME: childInfo2 is ignored when info1 is null
2270 		auto m = Element.make(tagName, "not null"[0..0], childInfo2);
2271 		m.innerHTML = innerHtml.source;
2272 		return m;
2273 	}
2274 
2275 	/// ditto
2276 	static Element make(string tagName, Element child, string childInfo2 = null) {
2277 		auto m = Element.make(tagName, cast(string) null, childInfo2);
2278 		m.appendChild(child);
2279 		return m;
2280 	}
2281 
2282 
2283 	/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
2284 	this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
2285 		tagName = _tagName;
2286 		if(_attributes !is null)
2287 			attributes = _attributes;
2288 		selfClosed = _selfClosed;
2289 
2290 		version(dom_node_indexes)
2291 			this.dataset.nodeIndex = to!string(&(this.attributes));
2292 
2293 		assert(_tagName.indexOf(" ") == -1);//, "<" ~ _tagName ~ "> is invalid");
2294 	}
2295 
2296 	/++
2297 		Convenience constructor when you don't care about the parentDocument. Note this might break things on the document.
2298 		Note also that without a parent document, elements are always in strict, case-sensitive mode.
2299 
2300 		History:
2301 			On February 8, 2021, the `selfClosedElements` parameter was added. It defaults to the same behavior as
2302 			before: using the hard-coded list of HTML elements, but it can now be overridden. If you use
2303 			[Document.createElement], it will use the list set for the current document. Otherwise, you can pass
2304 			something here if you like.
2305 	+/
2306 	this(string _tagName, string[string] _attributes = null, const string[] selfClosedElements = htmlSelfClosedElements) {
2307 		tagName = _tagName;
2308 		if(_attributes !is null)
2309 			attributes = _attributes;
2310 		selfClosed = tagName.isInArray(selfClosedElements);
2311 
2312 		// this is meant to reserve some memory. It makes a small, but consistent improvement.
2313 		//children.length = 8;
2314 		//children.length = 0;
2315 
2316 		version(dom_node_indexes)
2317 			this.dataset.nodeIndex = to!string(&(this.attributes));
2318 	}
2319 
2320 	private this(Document _parentDocument) {
2321 		version(dom_node_indexes)
2322 			this.dataset.nodeIndex = to!string(&(this.attributes));
2323 	}
2324 
2325 
2326 	/* *******************************
2327 	       Navigating the DOM
2328 	*********************************/
2329 
2330 	/// Returns the first child of this element. If it has no children, returns null.
2331 	/// Remember, text nodes are children too.
2332 	@property Element firstChild() {
2333 		return children.length ? children[0] : null;
2334 	}
2335 
2336 	/// Returns the last child of the element, or null if it has no children. Remember, text nodes are children too.
2337 	@property Element lastChild() {
2338 		return children.length ? children[$ - 1] : null;
2339 	}
2340 
2341 	// FIXME UNTESTED
2342 	/// the next or previous element you would encounter if you were reading it in the source. May be a text node or other special non-tag object if you enabled them.
2343 	Element nextInSource() {
2344 		auto n = firstChild;
2345 		if(n is null)
2346 			n = nextSibling();
2347 		if(n is null) {
2348 			auto p = this.parentNode;
2349 			while(p !is null && n is null) {
2350 				n = p.nextSibling;
2351 			}
2352 		}
2353 
2354 		return n;
2355 	}
2356 
2357 	/// ditto
2358 	Element previousInSource() {
2359 		auto p = previousSibling;
2360 		if(p is null) {
2361 			auto par = parentNode;
2362 			if(par)
2363 				p = par.lastChild;
2364 			if(p is null)
2365 				p = par;
2366 		}
2367 		return p;
2368 	}
2369 
2370 	/++
2371 		Returns the next or previous sibling that is not a text node. Please note: the behavior with comments is subject to change. Currently, it will return a comment or other nodes if it is in the tree (if you enabled it with [Document.enableAddingSpecialTagsToDom] or [Document.parseSawComment]) and not if you didn't, but the implementation will probably change at some point to skip them regardless.
2372 
2373 		Equivalent to [previousSibling]/[nextSibling]("*").
2374 
2375 		Please note it may return `null`.
2376 	+/
2377 	@property Element previousElementSibling() {
2378 		return previousSibling("*");
2379 	}
2380 
2381 	/// ditto
2382 	@property Element nextElementSibling() {
2383 		return nextSibling("*");
2384 	}
2385 
2386 	/++
2387 		Returns the next or previous sibling matching the `tagName` filter. The default filter of `null` will return the first sibling it sees, even if it is a comment or text node, or anything else. A filter of `"*"` will match any tag with a name. Otherwise, the string must match the [tagName] of the sibling you want to find.
2388 	+/
2389 	@property Element previousSibling(string tagName = null) {
2390 		if(this.parentNode is null)
2391 			return null;
2392 		Element ps = null;
2393 		foreach(e; this.parentNode.childNodes) {
2394 			if(e is this)
2395 				break;
2396 			if(tagName == "*" && e.nodeType != NodeType.Text) {
2397 				ps = e;
2398 			} else if(tagName is null || e.tagName == tagName)
2399 				ps = e;
2400 		}
2401 
2402 		return ps;
2403 	}
2404 
2405 	/// ditto
2406 	@property Element nextSibling(string tagName = null) {
2407 		if(this.parentNode is null)
2408 			return null;
2409 		Element ns = null;
2410 		bool mightBe = false;
2411 		foreach(e; this.parentNode.childNodes) {
2412 			if(e is this) {
2413 				mightBe = true;
2414 				continue;
2415 			}
2416 			if(mightBe) {
2417 				if(tagName == "*" && e.nodeType != NodeType.Text) {
2418 					ns = e;
2419 					break;
2420 				}
2421 				if(tagName is null || e.tagName == tagName) {
2422 					ns = e;
2423 					break;
2424 				}
2425 			}
2426 		}
2427 
2428 		return ns;
2429 	}
2430 
2431 
2432 	/++
2433 		Gets the nearest node, going up the chain, with the given tagName
2434 		May return null or throw. The type `T` will specify a subclass like
2435 		[Form], [Table], or [Link], which it will cast for you when found.
2436 	+/
2437 	T getParent(T = Element)(string tagName = null) if(is(T : Element)) {
2438 		if(tagName is null) {
2439 			static if(is(T == Form))
2440 				tagName = "form";
2441 			else static if(is(T == Table))
2442 				tagName = "table";
2443 			else static if(is(T == Link))
2444 				tagName == "a";
2445 		}
2446 
2447 		auto par = this.parentNode;
2448 		while(par !is null) {
2449 			if(tagName is null || par.tagName == tagName)
2450 				break;
2451 			par = par.parentNode;
2452 		}
2453 
2454 		static if(!is(T == Element)) {
2455 			auto t = cast(T) par;
2456 			if(t is null)
2457 				throw new ElementNotFoundException("", tagName ~ " parent not found", this);
2458 		} else
2459 			auto t = par;
2460 
2461 		return t;
2462 	}
2463 
2464 	/++
2465 		Searches this element and the tree of elements under it for one matching the given `id` attribute.
2466 	+/
2467 	Element getElementById(string id) {
2468 		// FIXME: I use this function a lot, and it's kinda slow
2469 		// not terribly slow, but not great.
2470 		foreach(e; tree)
2471 			if(e.id == id)
2472 				return e;
2473 		return null;
2474 	}
2475 
2476 	/++
2477 		Returns a child element that matches the given `selector`.
2478 
2479 		Note: you can give multiple selectors, separated by commas.
2480 	 	It will return the first match it finds.
2481 
2482 		Tip: to use namespaces, escape the colon in the name:
2483 
2484 		---
2485 			element.querySelector(`ns\:tag`); // the backticks are raw strings then the backslash is interpreted by querySelector
2486 		---
2487 	+/
2488 	@scriptable
2489 	Element querySelector(string selector) {
2490 		Selector s = Selector(selector);
2491 
2492 		foreach(ref comp; s.components)
2493 			if(comp.parts.length && comp.parts[0].separation > 0) {
2494 				// this is illegal in standard dom, but i use it a lot
2495 				// gonna insert a :scope thing
2496 
2497 				SelectorPart part;
2498 				part.separation = -1;
2499 				part.scopeElement = true;
2500 				comp.parts = part ~ comp.parts;
2501 			}
2502 
2503 		foreach(ele; tree)
2504 			if(s.matchesElement(ele, this))
2505 				return ele;
2506 		return null;
2507 	}
2508 
2509 	/// If the element matches the given selector. Previously known as `matchesSelector`.
2510 	@scriptable
2511 	bool matches(string selector) {
2512 		/+
2513 		bool caseSensitiveTags = true;
2514 		if(parentDocument && parentDocument.loose)
2515 			caseSensitiveTags = false;
2516 		+/
2517 
2518 		Selector s = Selector(selector);
2519 		return s.matchesElement(this);
2520 	}
2521 
2522 	/// Returns itself or the closest parent that matches the given selector, or null if none found
2523 	/// See_also: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
2524 	@scriptable
2525 	Element closest(string selector) {
2526 		Element e = this;
2527 		while(e !is null) {
2528 			if(e.matches(selector))
2529 				return e;
2530 			e = e.parentNode;
2531 		}
2532 		return null;
2533 	}
2534 
2535 	/**
2536 		Returns elements that match the given CSS selector
2537 
2538 		* -- all, default if nothing else is there
2539 
2540 		tag#id.class.class.class:pseudo[attrib=what][attrib=what] OP selector
2541 
2542 		It is all additive
2543 
2544 		OP
2545 
2546 		space = descendant
2547 		>     = direct descendant
2548 		+     = sibling (E+F Matches any F element immediately preceded by a sibling element E)
2549 
2550 		[foo]        Foo is present as an attribute
2551 		[foo="warning"]   Matches any E element whose "foo" attribute value is exactly equal to "warning".
2552 		E[foo~="warning"] Matches any E element whose "foo" attribute value is a list of space-separated values, one of which is exactly equal to "warning"
2553 		E[lang|="en"] Matches any E element whose "lang" attribute has a hyphen-separated list of values beginning (from the left) with "en".
2554 
2555 		[item$=sdas] ends with
2556 		[item^-sdsad] begins with
2557 
2558 		Quotes are optional here.
2559 
2560 		Pseudos:
2561 			:first-child
2562 			:last-child
2563 			:link (same as a[href] for our purposes here)
2564 
2565 
2566 		There can be commas separating the selector. A comma separated list result is OR'd onto the main.
2567 
2568 
2569 
2570 		This ONLY cares about elements. text, etc, are ignored
2571 
2572 
2573 		There should be two functions: given element, does it match the selector? and given a selector, give me all the elements
2574 
2575 		The name `getElementsBySelector` was the original name, written back before the name `querySelector` was standardized (this library is older than you might think!), but they do the same thing..
2576 	*/
2577 	@scriptable
2578 	Element[] querySelectorAll(string selector) {
2579 		// FIXME: this function could probably use some performance attention
2580 		// ... but only mildly so according to the profiler in the big scheme of things; probably negligible in a big app.
2581 
2582 
2583 		bool caseSensitiveTags = true;
2584 		if(parentDocument && parentDocument.loose)
2585 			caseSensitiveTags = false;
2586 
2587 		Element[] ret;
2588 		foreach(sel; parseSelectorString(selector, caseSensitiveTags))
2589 			ret ~= sel.getElements(this, null);
2590 		return ret;
2591 	}
2592 
2593 	/// ditto
2594 	alias getElementsBySelector = querySelectorAll;
2595 
2596 	/++
2597 		Returns child elements that have the given class name or tag name.
2598 
2599 		Please note the standard specifies this should return a live node list. This means, in Javascript for example, if you loop over the value returned by getElementsByTagName and getElementsByClassName and remove the elements, the length of the list will decrease. When I implemented this, I figured that was more trouble than it was worth and returned a plain array instead. By the time I had the infrastructure to make it simple, I didn't want to do the breaking change.
2600 
2601 		So these is incompatible with Javascript in the face of live dom mutation and will likely remain so.
2602 	+/
2603 	Element[] getElementsByClassName(string cn) {
2604 		// is this correct?
2605 		return getElementsBySelector("." ~ cn);
2606 	}
2607 
2608 	/// ditto
2609 	Element[] getElementsByTagName(string tag) {
2610 		if(parentDocument && parentDocument.loose)
2611 			tag = tag.toLower();
2612 		Element[] ret;
2613 		foreach(e; tree)
2614 			if(e.tagName == tag)
2615 				ret ~= e;
2616 		return ret;
2617 	}
2618 
2619 
2620 	/* *******************************
2621 	          Attributes
2622 	*********************************/
2623 
2624 	/**
2625 		Gets the given attribute value, or null if the
2626 		attribute is not set.
2627 
2628 		Note that the returned string is decoded, so it no longer contains any xml entities.
2629 	*/
2630 	@scriptable
2631 	string getAttribute(string name) const {
2632 		if(parentDocument && parentDocument.loose)
2633 			name = name.toLower();
2634 		auto e = name in attributes;
2635 		if(e)
2636 			return *e;
2637 		else
2638 			return null;
2639 	}
2640 
2641 	/**
2642 		Sets an attribute. Returns this for easy chaining
2643 	*/
2644 	@scriptable
2645 	Element setAttribute(string name, string value) {
2646 		if(parentDocument && parentDocument.loose)
2647 			name = name.toLower();
2648 
2649 		// I never use this shit legitimately and neither should you
2650 		auto it = name.toLower();
2651 		if(it == "href" || it == "src") {
2652 			auto v = value.strip().toLower();
2653 			if(v.startsWith("vbscript:"))
2654 				value = value[9..$];
2655 			if(v.startsWith("javascript:"))
2656 				value = value[11..$];
2657 		}
2658 
2659 		attributes[name] = value;
2660 
2661 		sendObserverEvent(DomMutationOperations.setAttribute, name, value);
2662 
2663 		return this;
2664 	}
2665 
2666 	/**
2667 		Returns if the attribute exists.
2668 	*/
2669 	@scriptable
2670 	bool hasAttribute(string name) {
2671 		if(parentDocument && parentDocument.loose)
2672 			name = name.toLower();
2673 
2674 		if(name in attributes)
2675 			return true;
2676 		else
2677 			return false;
2678 	}
2679 
2680 	/**
2681 		Removes the given attribute from the element.
2682 	*/
2683 	@scriptable
2684 	Element removeAttribute(string name)
2685 	out(ret) {
2686 		assert(ret is this);
2687 	}
2688 	do {
2689 		if(parentDocument && parentDocument.loose)
2690 			name = name.toLower();
2691 		if(name in attributes)
2692 			attributes.remove(name);
2693 
2694 		sendObserverEvent(DomMutationOperations.removeAttribute, name);
2695 		return this;
2696 	}
2697 
2698 	/**
2699 		Gets or sets the class attribute's contents. Returns
2700 		an empty string if it has no class.
2701 	*/
2702 	@property string className() const {
2703 		auto c = getAttribute("class");
2704 		if(c is null)
2705 			return "";
2706 		return c;
2707 	}
2708 
2709 	/// ditto
2710 	@property Element className(string c) {
2711 		setAttribute("class", c);
2712 		return this;
2713 	}
2714 
2715 	/**
2716 		Provides easy access to common HTML attributes, object style.
2717 
2718 		---
2719 		auto element = Element.make("a");
2720 		a.href = "cool.html"; // this is the same as a.setAttribute("href", "cool.html");
2721 		string where = a.href; // same as a.getAttribute("href");
2722 		---
2723 
2724 	*/
2725 	@property string opDispatch(string name)(string v = null) if(isConvenientAttribute(name)) {
2726 		if(v !is null)
2727 			setAttribute(name, v);
2728 		return getAttribute(name);
2729 	}
2730 
2731 	/**
2732 		Old access to attributes. Use [attrs] instead.
2733 
2734 		DEPRECATED: generally open opDispatch caused a lot of unforeseen trouble with compile time duck typing and UFCS extensions.
2735 		so I want to remove it. A small whitelist of attributes is still allowed, but others are not.
2736 
2737 		Instead, use element.attrs.attribute, element.attrs["attribute"],
2738 		or element.getAttribute("attribute")/element.setAttribute("attribute").
2739 	*/
2740 	@property string opDispatch(string name)(string v = null) if(!isConvenientAttribute(name)) {
2741 		static assert(0, "Don't use " ~ name ~ " direct on Element, instead use element.attrs.attributeName");
2742 	}
2743 
2744 	/*
2745 	// this would be nice for convenience, but it broke the getter above.
2746 	@property void opDispatch(string name)(bool boolean) if(name != "popFront") {
2747 		if(boolean)
2748 			setAttribute(name, name);
2749 		else
2750 			removeAttribute(name);
2751 	}
2752 	*/
2753 
2754 	/**
2755 		Returns the element's children.
2756 	*/
2757 	@property inout(Element[]) childNodes() inout {
2758 		return children;
2759 	}
2760 
2761 	/++
2762 		HTML5's dataset property. It is an alternate view into attributes with the data- prefix.
2763 		Given `<a data-my-property="cool" />`, we get `assert(a.dataset.myProperty == "cool");`
2764 	+/
2765 	@property DataSet dataset() {
2766 		return DataSet(this);
2767 	}
2768 
2769 	/++
2770 		Gives dot/opIndex access to attributes
2771 		---
2772 		ele.attrs.largeSrc = "foo"; // same as ele.setAttribute("largeSrc", "foo")
2773 		---
2774 	+/
2775 	@property AttributeSet attrs() {
2776 		return AttributeSet(this);
2777 	}
2778 
2779 	/++
2780 		Provides both string and object style (like in Javascript) access to the style attribute.
2781 
2782 		---
2783 		element.style.color = "red"; // translates into setting `color: red;` in the `style` attribute
2784 		---
2785 	+/
2786 	@property ElementStyle style() {
2787 		return ElementStyle(this);
2788 	}
2789 
2790 	/++
2791 		This sets the style attribute with a string.
2792 	+/
2793 	@property ElementStyle style(string s) {
2794 		this.setAttribute("style", s);
2795 		return this.style;
2796 	}
2797 
2798 	private void parseAttributes(string[] whichOnes = null) {
2799 /+
2800 		if(whichOnes is null)
2801 			whichOnes = attributes.keys;
2802 		foreach(attr; whichOnes) {
2803 			switch(attr) {
2804 				case "id":
2805 
2806 				break;
2807 				case "class":
2808 
2809 				break;
2810 				case "style":
2811 
2812 				break;
2813 				default:
2814 					// we don't care about it
2815 			}
2816 		}
2817 +/
2818 	}
2819 
2820 
2821 	// if you change something here, it won't apply... FIXME const? but changing it would be nice if it applies to the style attribute too though you should use style there.
2822 
2823 	// the next few methods are for implementing interactive kind of things
2824 	private CssStyle _computedStyle;
2825 
2826 	/// Don't use this. It can try to parse out the style element but it isn't complete and if I get back to it, it won't be for a while.
2827 	@property CssStyle computedStyle() {
2828 		if(_computedStyle is null) {
2829 			auto style = this.getAttribute("style");
2830 		/* we'll treat shitty old html attributes as css here */
2831 			if(this.hasAttribute("width"))
2832 				style ~= "; width: " ~ this.attrs.width;
2833 			if(this.hasAttribute("height"))
2834 				style ~= "; height: " ~ this.attrs.height;
2835 			if(this.hasAttribute("bgcolor"))
2836 				style ~= "; background-color: " ~ this.attrs.bgcolor;
2837 			if(this.tagName == "body" && this.hasAttribute("text"))
2838 				style ~= "; color: " ~ this.attrs.text;
2839 			if(this.hasAttribute("color"))
2840 				style ~= "; color: " ~ this.attrs.color;
2841 		/* done */
2842 
2843 
2844 			_computedStyle = new CssStyle(null, style); // gives at least something to work with
2845 		}
2846 		return _computedStyle;
2847 	}
2848 
2849 	/// These properties are useless in most cases, but if you write a layout engine on top of this lib, they may be good
2850 	version(browser) {
2851 		void* expansionHook; ///ditto
2852 		int offsetWidth; ///ditto
2853 		int offsetHeight; ///ditto
2854 		int offsetLeft; ///ditto
2855 		int offsetTop; ///ditto
2856 		Element offsetParent; ///ditto
2857 		bool hasLayout; ///ditto
2858 		int zIndex; ///ditto
2859 
2860 		///ditto
2861 		int absoluteLeft() {
2862 			int a = offsetLeft;
2863 			auto p = offsetParent;
2864 			while(p) {
2865 				a += p.offsetLeft;
2866 				p = p.offsetParent;
2867 			}
2868 
2869 			return a;
2870 		}
2871 
2872 		///ditto
2873 		int absoluteTop() {
2874 			int a = offsetTop;
2875 			auto p = offsetParent;
2876 			while(p) {
2877 				a += p.offsetTop;
2878 				p = p.offsetParent;
2879 			}
2880 
2881 			return a;
2882 		}
2883 	}
2884 
2885 	// Back to the regular dom functions
2886 
2887     public:
2888 
2889 
2890 	/* *******************************
2891 	          DOM Mutation
2892 	*********************************/
2893 
2894 	/// Removes all inner content from the tag; all child text and elements are gone.
2895 	void removeAllChildren()
2896 		out {
2897 			assert(this.children.length == 0);
2898 		}
2899 	do {
2900 		foreach(child; children)
2901 			child.parentNode = null;
2902 		children = null;
2903 	}
2904 
2905 	/++
2906 		Adds a sibling element before or after this one in the dom.
2907 
2908 		History: added June 13, 2020
2909 	+/
2910 	Element appendSibling(Element e) {
2911 		parentNode.insertAfter(this, e);
2912 		return e;
2913 	}
2914 
2915 	/// ditto
2916 	Element prependSibling(Element e) {
2917 		parentNode.insertBefore(this, e);
2918 		return e;
2919 	}
2920 
2921 
2922     	/++
2923 		Appends the given element to this one. If it already has a parent, it is removed from that tree and moved to this one.
2924 
2925 		See_also: https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild
2926 
2927 		History:
2928 			Prior to 1 Jan 2020 (git tag v4.4.1 and below), it required that the given element must not have a parent already. This was in violation of standard, so it changed the behavior to remove it from the existing parent and instead move it here.
2929 	+/
2930 	Element appendChild(Element e)
2931 		in {
2932 			assert(e !is null);
2933 			assert(e !is this);
2934 		}
2935 		out (ret) {
2936 			assert((cast(DocumentFragment) this !is null) || (e.parentNode is this), e.toString);// e.parentNode ? e.parentNode.toString : "null");
2937 			assert(e.parentDocument is this.parentDocument);
2938 			assert(e is ret);
2939 		}
2940 	do {
2941 		if(e.parentNode !is null)
2942 			e.parentNode.removeChild(e);
2943 
2944 		selfClosed = false;
2945 		if(auto frag = cast(DocumentFragment) e)
2946 			children ~= frag.children;
2947 		else
2948 			children ~= e;
2949 
2950 		e.parentNode = this;
2951 
2952 		/+
2953 		foreach(item; e.tree)
2954 			item.parentDocument = this.parentDocument;
2955 		+/
2956 
2957 		sendObserverEvent(DomMutationOperations.appendChild, null, null, e);
2958 
2959 		return e;
2960 	}
2961 
2962 	/// Inserts the second element to this node, right before the first param
2963 	Element insertBefore(in Element where, Element what)
2964 		in {
2965 			assert(where !is null);
2966 			assert(where.parentNode is this);
2967 			assert(what !is null);
2968 			assert(what.parentNode is null);
2969 		}
2970 		out (ret) {
2971 			assert(where.parentNode is this);
2972 			assert(what.parentNode is this);
2973 
2974 			assert(what.parentDocument is this.parentDocument);
2975 			assert(ret is what);
2976 		}
2977 	do {
2978 		foreach(i, e; children) {
2979 			if(e is where) {
2980 				if(auto frag = cast(DocumentFragment) what) {
2981 					children = children[0..i] ~ frag.children ~ children[i..$];
2982 					foreach(child; frag.children)
2983 						child.parentNode = this;
2984 				} else {
2985 					children = children[0..i] ~ what ~ children[i..$];
2986 				}
2987 				what.parentNode = this;
2988 				return what;
2989 			}
2990 		}
2991 
2992 		return what;
2993 
2994 		assert(0);
2995 	}
2996 
2997 	/++
2998 		Inserts the given element `what` as a sibling of the `this` element, after the element `where` in the parent node.
2999 	+/
3000 	Element insertAfter(in Element where, Element what)
3001 		in {
3002 			assert(where !is null);
3003 			assert(where.parentNode is this);
3004 			assert(what !is null);
3005 			assert(what.parentNode is null);
3006 		}
3007 		out (ret) {
3008 			assert(where.parentNode is this);
3009 			assert(what.parentNode is this);
3010 			assert(what.parentDocument is this.parentDocument);
3011 			assert(ret is what);
3012 		}
3013 	do {
3014 		foreach(i, e; children) {
3015 			if(e is where) {
3016 				if(auto frag = cast(DocumentFragment) what) {
3017 					children = children[0 .. i + 1] ~ what.children ~ children[i + 1 .. $];
3018 					foreach(child; frag.children)
3019 						child.parentNode = this;
3020 				} else
3021 					children = children[0 .. i + 1] ~ what ~ children[i + 1 .. $];
3022 				what.parentNode = this;
3023 				return what;
3024 			}
3025 		}
3026 
3027 		return what;
3028 
3029 		assert(0);
3030 	}
3031 
3032 	/// swaps one child for a new thing. Returns the old child which is now parentless.
3033 	Element swapNode(Element child, Element replacement)
3034 		in {
3035 			assert(child !is null);
3036 			assert(replacement !is null);
3037 			assert(child.parentNode is this);
3038 		}
3039 		out(ret) {
3040 			assert(ret is child);
3041 			assert(ret.parentNode is null);
3042 			assert(replacement.parentNode is this);
3043 			assert(replacement.parentDocument is this.parentDocument);
3044 		}
3045 	do {
3046 		foreach(ref c; this.children)
3047 			if(c is child) {
3048 				c.parentNode = null;
3049 				c = replacement;
3050 				c.parentNode = this;
3051 				return child;
3052 			}
3053 		assert(0);
3054 	}
3055 
3056 
3057 	/++
3058 		Appends the given to the node.
3059 
3060 
3061 		Calling `e.appendText(" hi")` on `<example>text <b>bold</b></example>`
3062 		yields `<example>text <b>bold</b> hi</example>`.
3063 
3064 		See_Also:
3065 			[firstInnerText], [directText], [innerText], [appendChild]
3066 	+/
3067 	@scriptable
3068 	Element appendText(string text) {
3069 		Element e = new TextNode(parentDocument, text);
3070 		appendChild(e);
3071 		return this;
3072 	}
3073 
3074 	/++
3075 		Returns child elements which are of a tag type (excludes text, comments, etc.).
3076 
3077 
3078 		childElements of `<example>text <b>bold</b></example>` is just the `<b>` tag.
3079 
3080 		Params:
3081 			tagName = filter results to only the child elements with the given tag name.
3082 	+/
3083 	@property Element[] childElements(string tagName = null) {
3084 		Element[] ret;
3085 		foreach(c; children)
3086 			if(c.nodeType == 1 && (tagName is null || c.tagName == tagName))
3087 				ret ~= c;
3088 		return ret;
3089 	}
3090 
3091 	/++
3092 		Appends the given html to the element, returning the elements appended
3093 
3094 
3095 		This is similar to `element.innerHTML += "html string";` in Javascript.
3096 	+/
3097 	@scriptable
3098 	Element[] appendHtml(string html) {
3099 		Document d = new Document("<root>" ~ html ~ "</root>");
3100 		return stealChildren(d.root);
3101 	}
3102 
3103 
3104 	/++
3105 		Inserts a child under this element after the element `where`.
3106 	+/
3107 	void insertChildAfter(Element child, Element where)
3108 		in {
3109 			assert(child !is null);
3110 			assert(where !is null);
3111 			assert(where.parentNode is this);
3112 			assert(!selfClosed);
3113 			//assert(isInArray(where, children));
3114 		}
3115 		out {
3116 			assert(child.parentNode is this);
3117 			assert(where.parentNode is this);
3118 			//assert(isInArray(where, children));
3119 			//assert(isInArray(child, children));
3120 		}
3121 	do {
3122 		foreach(ref i, c; children) {
3123 			if(c is where) {
3124 				i++;
3125 				if(auto frag = cast(DocumentFragment) child) {
3126 					children = children[0..i] ~ child.children ~ children[i..$];
3127 					//foreach(child; frag.children)
3128 						//child.parentNode = this;
3129 				} else
3130 					children = children[0..i] ~ child ~ children[i..$];
3131 				child.parentNode = this;
3132 				break;
3133 			}
3134 		}
3135 	}
3136 
3137 	/++
3138 		Reparents all the child elements of `e` to `this`, leaving `e` childless.
3139 
3140 		Params:
3141 			e = the element whose children you want to steal
3142 			position = an existing child element in `this` before which you want the stolen children to be inserted. If `null`, it will append the stolen children at the end of our current children.
3143 	+/
3144 	Element[] stealChildren(Element e, Element position = null)
3145 		in {
3146 			assert(!selfClosed);
3147 			assert(e !is null);
3148 			//if(position !is null)
3149 				//assert(isInArray(position, children));
3150 		}
3151 		out (ret) {
3152 			assert(e.children.length == 0);
3153 			// all the parentNode is this checks fail because DocumentFragments do not appear in the parent tree, they are invisible...
3154 			version(none)
3155 			debug foreach(child; ret) {
3156 				assert(child.parentNode is this);
3157 				assert(child.parentDocument is this.parentDocument);
3158 			}
3159 		}
3160 	do {
3161 		foreach(c; e.children) {
3162 			c.parentNode = this;
3163 		}
3164 		if(position is null)
3165 			children ~= e.children;
3166 		else {
3167 			foreach(i, child; children) {
3168 				if(child is position) {
3169 					children = children[0..i] ~
3170 						e.children ~
3171 						children[i..$];
3172 					break;
3173 				}
3174 			}
3175 		}
3176 
3177 		auto ret = e.children[];
3178 		e.children.length = 0;
3179 
3180 		return ret;
3181 	}
3182 
3183     	/// Puts the current element first in our children list. The given element must not have a parent already.
3184 	Element prependChild(Element e)
3185 		in {
3186 			assert(e.parentNode is null);
3187 			assert(!selfClosed);
3188 		}
3189 		out {
3190 			assert(e.parentNode is this);
3191 			assert(e.parentDocument is this.parentDocument);
3192 			assert(children[0] is e);
3193 		}
3194 	do {
3195 		if(auto frag = cast(DocumentFragment) e) {
3196 			children = e.children ~ children;
3197 			foreach(child; frag.children)
3198 				child.parentNode = this;
3199 		} else
3200 			children = e ~ children;
3201 		e.parentNode = this;
3202 		return e;
3203 	}
3204 
3205 
3206 	/**
3207 		Returns a string containing all child elements, formatted such that it could be pasted into
3208 		an XML file.
3209 	*/
3210 	@property string innerHTML(Appender!string where = appender!string()) const {
3211 		if(children is null)
3212 			return "";
3213 
3214 		auto start = where.data.length;
3215 
3216 		foreach(child; children) {
3217 			assert(child !is null);
3218 
3219 			child.writeToAppender(where);
3220 		}
3221 
3222 		return where.data[start .. $];
3223 	}
3224 
3225 	/**
3226 		Takes some html and replaces the element's children with the tree made from the string.
3227 	*/
3228 	@property Element innerHTML(string html, bool strict = false) {
3229 		if(html.length)
3230 			selfClosed = false;
3231 
3232 		if(html.length == 0) {
3233 			// I often say innerHTML = ""; as a shortcut to clear it out,
3234 			// so let's optimize that slightly.
3235 			removeAllChildren();
3236 			return this;
3237 		}
3238 
3239 		auto doc = new Document();
3240 		doc.parseUtf8("<innerhtml>" ~ html ~ "</innerhtml>", strict, strict); // FIXME: this should preserve the strictness of the parent document
3241 
3242 		children = doc.root.children;
3243 		foreach(c; children) {
3244 			c.parentNode = this;
3245 		}
3246 
3247 		doc.root.children = null;
3248 
3249 		return this;
3250 	}
3251 
3252 	/// ditto
3253 	@property Element innerHTML(Html html) {
3254 		return this.innerHTML = html.source;
3255 	}
3256 
3257 	/**
3258 		Replaces this node with the given html string, which is parsed
3259 
3260 		Note: this invalidates the this reference, since it is removed
3261 		from the tree.
3262 
3263 		Returns the new children that replace this.
3264 	*/
3265 	@property Element[] outerHTML(string html) {
3266 		auto doc = new Document();
3267 		doc.parseUtf8("<innerhtml>" ~ html ~ "</innerhtml>"); // FIXME: needs to preserve the strictness
3268 
3269 		children = doc.root.children;
3270 		foreach(c; children) {
3271 			c.parentNode = this;
3272 		}
3273 
3274 		stripOut();
3275 
3276 		return doc.root.children;
3277 	}
3278 
3279 	/++
3280 		Returns all the html for this element, including the tag itself.
3281 
3282 		This is equivalent to calling toString().
3283 	+/
3284 	@property string outerHTML() {
3285 		return this.toString();
3286 	}
3287 
3288 	/// This sets the inner content of the element *without* trying to parse it.
3289 	/// You can inject any code in there; this serves as an escape hatch from the dom.
3290 	///
3291 	/// The only times you might actually need it are for < style > and < script > tags in html.
3292 	/// Other than that, innerHTML and/or innerText should do the job.
3293 	@property void innerRawSource(string rawSource) {
3294 		children.length = 0;
3295 		auto rs = new RawSource(parentDocument, rawSource);
3296 		children ~= rs;
3297 		rs.parentNode = this;
3298 	}
3299 
3300 	/++
3301 		Replaces the element `find`, which must be a child of `this`, with the element `replace`, which must have no parent.
3302 	+/
3303 	Element replaceChild(Element find, Element replace)
3304 		in {
3305 			assert(find !is null);
3306 			assert(find.parentNode is this);
3307 			assert(replace !is null);
3308 			assert(replace.parentNode is null);
3309 		}
3310 		out(ret) {
3311 			assert(ret is replace);
3312 			assert(replace.parentNode is this);
3313 			assert(replace.parentDocument is this.parentDocument);
3314 			assert(find.parentNode is null);
3315 		}
3316 	do {
3317 		// FIXME
3318 		//if(auto frag = cast(DocumentFragment) replace)
3319 			//return this.replaceChild(frag, replace.children);
3320 		for(int i = 0; i < children.length; i++) {
3321 			if(children[i] is find) {
3322 				replace.parentNode = this;
3323 				children[i].parentNode = null;
3324 				children[i] = replace;
3325 				return replace;
3326 			}
3327 		}
3328 
3329 		throw new Exception("no such child ");// ~  find.toString ~ " among " ~ typeid(this).toString);//.toString ~ " magic \n\n\n" ~ find.parentNode.toString);
3330 	}
3331 
3332 	/**
3333 		Replaces the given element with a whole group.
3334 	*/
3335 	void replaceChild(Element find, Element[] replace)
3336 		in {
3337 			assert(find !is null);
3338 			assert(replace !is null);
3339 			assert(find.parentNode is this);
3340 			debug foreach(r; replace)
3341 				assert(r.parentNode is null);
3342 		}
3343 		out {
3344 			assert(find.parentNode is null);
3345 			assert(children.length >= replace.length);
3346 			debug foreach(child; children)
3347 				assert(child !is find);
3348 			debug foreach(r; replace)
3349 				assert(r.parentNode is this);
3350 		}
3351 	do {
3352 		if(replace.length == 0) {
3353 			removeChild(find);
3354 			return;
3355 		}
3356 		assert(replace.length);
3357 		for(int i = 0; i < children.length; i++) {
3358 			if(children[i] is find) {
3359 				children[i].parentNode = null; // this element should now be dead
3360 				children[i] = replace[0];
3361 				foreach(e; replace) {
3362 					e.parentNode = this;
3363 				}
3364 
3365 				children = .insertAfter(children, i, replace[1..$]);
3366 
3367 				return;
3368 			}
3369 		}
3370 
3371 		throw new Exception("no such child");
3372 	}
3373 
3374 
3375 	/**
3376 		Removes the given child from this list.
3377 
3378 		Returns the removed element.
3379 	*/
3380 	Element removeChild(Element c)
3381 		in {
3382 			assert(c !is null);
3383 			assert(c.parentNode is this);
3384 		}
3385 		out {
3386 			debug foreach(child; children)
3387 				assert(child !is c);
3388 			assert(c.parentNode is null);
3389 		}
3390 	do {
3391 		foreach(i, e; children) {
3392 			if(e is c) {
3393 				children = children[0..i] ~ children [i+1..$];
3394 				c.parentNode = null;
3395 				return c;
3396 			}
3397 		}
3398 
3399 		throw new Exception("no such child");
3400 	}
3401 
3402 	/// This removes all the children from this element, returning the old list.
3403 	Element[] removeChildren()
3404 		out (ret) {
3405 			assert(children.length == 0);
3406 			debug foreach(r; ret)
3407 				assert(r.parentNode is null);
3408 		}
3409 	do {
3410 		Element[] oldChildren = children.dup;
3411 		foreach(c; oldChildren)
3412 			c.parentNode = null;
3413 
3414 		children.length = 0;
3415 
3416 		return oldChildren;
3417 	}
3418 
3419 	/**
3420 		Fetch the inside text, with all tags stripped out.
3421 
3422 		<p>cool <b>api</b> &amp; code dude<p>
3423 		innerText of that is "cool api & code dude".
3424 
3425 		This does not match what real innerText does!
3426 		http://perfectionkills.com/the-poor-misunderstood-innerText/
3427 
3428 		It is more like [textContent].
3429 
3430 		See_Also:
3431 			[visibleText], which is closer to what the real `innerText`
3432 			does.
3433 	*/
3434 	@scriptable
3435 	@property string innerText() const {
3436 		string s;
3437 		foreach(child; children) {
3438 			if(child.nodeType != NodeType.Text)
3439 				s ~= child.innerText;
3440 			else
3441 				s ~= child.nodeValue();
3442 		}
3443 		return s;
3444 	}
3445 
3446 	/// ditto
3447 	alias textContent = innerText;
3448 
3449 	/++
3450 		Gets the element's visible text, similar to how it would look assuming
3451 		the document was HTML being displayed by a browser. This means it will
3452 		attempt whitespace normalization (unless it is a `<pre>` tag), add `\n`
3453 		characters for `<br>` tags, and I reserve the right to make it process
3454 		additional css and tags in the future.
3455 
3456 		If you need specific output, use the more stable [textContent] property
3457 		or iterate yourself with [tree] or a recursive function with [children].
3458 
3459 		History:
3460 			Added March 25, 2022 (dub v10.8)
3461 	+/
3462 	string visibleText() const {
3463 		return this.visibleTextHelper(this.tagName == "pre");
3464 	}
3465 
3466 	private string visibleTextHelper(bool pre) const {
3467 		string result;
3468 		foreach(thing; this.children) {
3469 			if(thing.nodeType == NodeType.Text)
3470 				result ~= pre ? thing.nodeValue : normalizeWhitespace(thing.nodeValue);
3471 			else if(thing.tagName == "br")
3472 				result ~= "\n";
3473 			else
3474 				result ~= thing.visibleTextHelper(pre || thing.tagName == "pre");
3475 		}
3476 		return result;
3477 	}
3478 
3479 	/**
3480 		Sets the inside text, replacing all children. You don't
3481 		have to worry about entity encoding.
3482 	*/
3483 	@scriptable
3484 	@property void innerText(string text) {
3485 		selfClosed = false;
3486 		Element e = new TextNode(parentDocument, text);
3487 		children = [e];
3488 		e.parentNode = this;
3489 	}
3490 
3491 	/**
3492 		Strips this node out of the document, replacing it with the given text
3493 	*/
3494 	@property void outerText(string text) {
3495 		parentNode.replaceChild(this, new TextNode(parentDocument, text));
3496 	}
3497 
3498 	/**
3499 		Same result as innerText; the tag with all inner tags stripped out
3500 	*/
3501 	@property string outerText() const {
3502 		return innerText;
3503 	}
3504 
3505 
3506 	/* *******************************
3507 	          Miscellaneous
3508 	*********************************/
3509 
3510 	/// This is a full clone of the element. Alias for cloneNode(true) now. Don't extend it.
3511 	@property Element cloned()
3512 	/+
3513 		out(ret) {
3514 			// FIXME: not sure why these fail...
3515 			assert(ret.children.length == this.children.length, format("%d %d", ret.children.length, this.children.length));
3516 			assert(ret.tagName == this.tagName);
3517 		}
3518 	do {
3519 	+/
3520 	{
3521 		return this.cloneNode(true);
3522 	}
3523 
3524 	/// Clones the node. If deepClone is true, clone all inner tags too. If false, only do this tag (and its attributes), but it will have no contents.
3525 	Element cloneNode(bool deepClone) {
3526 		auto e = Element.make(this.tagName);
3527 		e.attributes = this.attributes.aadup;
3528 		e.selfClosed = this.selfClosed;
3529 
3530 		if(deepClone) {
3531 			foreach(child; children) {
3532 				e.appendChild(child.cloneNode(true));
3533 			}
3534 		}
3535 
3536 
3537 		return e;
3538 	}
3539 
3540 	/// W3C DOM interface. Only really meaningful on [TextNode] instances, but the interface is present on the base class.
3541 	string nodeValue() const {
3542 		return "";
3543 	}
3544 
3545 	// should return int
3546 	///.
3547 	@property int nodeType() const {
3548 		return 1;
3549 	}
3550 
3551 
3552 	invariant () {
3553 		debug assert(tagName.indexOf(" ") == -1);
3554 
3555 		// commented cuz it gets into recursive pain and eff dat.
3556 		/+
3557 		if(children !is null)
3558 		foreach(child; children) {
3559 		//	assert(parentNode !is null);
3560 			assert(child !is null);
3561 			assert(child.parent_.asElement is this, format("%s is not a parent of %s (it thought it was %s)", tagName, child.tagName, child.parent_.asElement is null ? "null" : child.parent_.asElement.tagName));
3562 			assert(child !is this);
3563 			//assert(child !is parentNode);
3564 		}
3565 		+/
3566 
3567 		/+
3568 		// this isn't helping
3569 		if(parent_ && parent_.asElement) {
3570 			bool found = false;
3571 			foreach(child; parent_.asElement.children)
3572 				if(child is this)
3573 					found = true;
3574 			assert(found, format("%s lists %s as parent, but it is not in children", typeid(this), typeid(this.parent_.asElement)));
3575 		}
3576 		+/
3577 
3578 		/+ // only depend on parentNode's accuracy if you shuffle things around and use the top elements - where the contracts guarantee it on out
3579 		if(parentNode !is null) {
3580 			// if you have a parent, you should share the same parentDocument; this is appendChild()'s job
3581 			auto lol = cast(TextNode) this;
3582 			assert(parentDocument is parentNode.parentDocument, lol is null ? this.tagName : lol.contents);
3583 		}
3584 		+/
3585 		//assert(parentDocument !is null); // no more; if it is present, we use it, but it is not required
3586 		// reason is so you can create these without needing a reference to the document
3587 	}
3588 
3589 	/**
3590 		Turns the whole element, including tag, attributes, and children, into a string which could be pasted into
3591 		an XML file.
3592 	*/
3593 	override string toString() const {
3594 		return writeToAppender();
3595 	}
3596 
3597 	/++
3598 		Returns if the node would be printed to string as `<tag />` or `<tag></tag>`. In other words, if it has no non-empty text nodes and no element nodes. Please note that whitespace text nodes are NOT considered empty; `Html("<tag> </tag>").isEmpty == false`.
3599 
3600 
3601 		The value is undefined if there are comment or processing instruction nodes. The current implementation returns false if it sees those, assuming the nodes haven't been stripped out during parsing. But I'm not married to the current implementation and reserve the right to change it without notice.
3602 
3603 		History:
3604 			Added December 3, 2021 (dub v10.5)
3605 
3606 	+/
3607 	public bool isEmpty() const {
3608 		foreach(child; this.children) {
3609 			// any non-text node is of course not empty since that's a tag
3610 			if(child.nodeType != NodeType.Text)
3611 				return false;
3612 			// or a text node is empty if it is is a null or empty string, so this length check fixes that
3613 			if(child.nodeValue.length)
3614 				return false;
3615 		}
3616 
3617 		return true;
3618 	}
3619 
3620 	protected string toPrettyStringIndent(bool insertComments, int indentationLevel, string indentWith) const {
3621 		if(indentWith is null)
3622 			return null;
3623 
3624 		// at the top we don't have anything to really do
3625 		//if(parent_ is null)
3626 			//return null;
3627 
3628 			// I've used isEmpty before but this other check seems better....
3629 			//|| this.isEmpty())
3630 
3631 		string s;
3632 
3633 		if(insertComments) s ~= "<!--";
3634 		s ~= "\n";
3635 		foreach(indent; 0 .. indentationLevel)
3636 			s ~= indentWith;
3637 		if(insertComments) s ~= "-->";
3638 
3639 		return s;
3640 	}
3641 
3642 	/++
3643 		Writes out with formatting. Be warned: formatting changes the contents. Use ONLY
3644 		for eyeball debugging.
3645 
3646 		$(PITFALL
3647 			This function is not stable. Its interface and output may change without
3648 			notice. The only promise I make is that it will continue to make a best-
3649 			effort attempt at being useful for debugging by human eyes.
3650 
3651 			I have used it in the past for diffing html documents, but even then, it
3652 			might change between versions. If it is useful, great, but beware; this
3653 			use is at your own risk.
3654 		)
3655 
3656 		History:
3657 			On November 19, 2021, I changed this to `final`. If you were overriding it,
3658 			change our override to `toPrettyStringImpl` instead. It now just calls
3659 			`toPrettyStringImpl.strip` to be an entry point for a stand-alone call.
3660 
3661 			If you are calling it as part of another implementation, you might want to
3662 			change that call to `toPrettyStringImpl` as well.
3663 
3664 			I am NOT considering this a breaking change since this function is documented
3665 			to only be used for eyeball debugging anyway, which means the exact format is
3666 			not specified and the override behavior can generally not be relied upon.
3667 
3668 			(And I find it extremely unlikely anyone was subclassing anyway, but if you were,
3669 			email me, and we'll see what we can do. I'd like to know at least.)
3670 
3671 			I reserve the right to make future changes in the future without considering
3672 			them breaking as well.
3673 	+/
3674 	final string toPrettyString(bool insertComments = false, int indentationLevel = 0, string indentWith = "\t") const {
3675 		return toPrettyStringImpl(insertComments, indentationLevel, indentWith).strip;
3676 	}
3677 
3678 	string toPrettyStringImpl(bool insertComments = false, int indentationLevel = 0, string indentWith = "\t") const {
3679 
3680 		// first step is to concatenate any consecutive text nodes to simplify
3681 		// the white space analysis. this changes the tree! but i'm allowed since
3682 		// the comment always says it changes the comments
3683 		//
3684 		// actually i'm not allowed cuz it is const so i will cheat and lie
3685 		/+
3686 		TextNode lastTextChild = null;
3687 		for(int a = 0; a < this.children.length; a++) {
3688 			auto child = this.children[a];
3689 			if(auto tn = cast(TextNode) child) {
3690 				if(lastTextChild) {
3691 					lastTextChild.contents ~= tn.contents;
3692 					for(int b = a; b < this.children.length - 1; b++)
3693 						this.children[b] = this.children[b + 1];
3694 					this.children = this.children[0 .. $-1];
3695 				} else {
3696 					lastTextChild = tn;
3697 				}
3698 			} else {
3699 				lastTextChild = null;
3700 			}
3701 		}
3702 		+/
3703 
3704 		auto inlineElements = (parentDocument is null ? null : parentDocument.inlineElements);
3705 
3706 		const(Element)[] children;
3707 
3708 		TextNode lastTextChild = null;
3709 		for(int a = 0; a < this.children.length; a++) {
3710 			auto child = this.children[a];
3711 			if(auto tn = cast(const(TextNode)) child) {
3712 				if(lastTextChild !is null) {
3713 					lastTextChild.contents ~= tn.contents;
3714 				} else {
3715 					lastTextChild = new TextNode("");
3716 					lastTextChild.parentNode = cast(Element) this;
3717 					lastTextChild.contents ~= tn.contents;
3718 					children ~= lastTextChild;
3719 				}
3720 			} else {
3721 				lastTextChild = null;
3722 				children ~= child;
3723 			}
3724 		}
3725 
3726 		string s = toPrettyStringIndent(insertComments, indentationLevel, indentWith);
3727 
3728 		s ~= "<";
3729 		s ~= tagName;
3730 
3731 		// i sort these for consistent output. might be more legible
3732 		// but especially it keeps it the same for diff purposes.
3733 		import std.algorithm : sort;
3734 		auto keys = sort(attributes.keys);
3735 		foreach(n; keys) {
3736 			auto v = attributes[n];
3737 			s ~= " ";
3738 			s ~= n;
3739 			s ~= "=\"";
3740 			s ~= htmlEntitiesEncode(v);
3741 			s ~= "\"";
3742 		}
3743 
3744 		if(selfClosed){
3745 			s ~= " />";
3746 			return s;
3747 		}
3748 
3749 		s ~= ">";
3750 
3751 		// for simple `<collection><item>text</item><item>text</item></collection>`, let's
3752 		// just keep them on the same line
3753 
3754 		if(isEmpty) {
3755 			// no work needed, this is empty so don't indent just for a blank line
3756 		} else if(children.length == 1 && children[0].isEmpty) {
3757 			// just one empty one, can put it inline too
3758 			s ~= children[0].toString();
3759 		} else if(tagName.isInArray(inlineElements) || allAreInlineHtml(children, inlineElements)) {
3760 			foreach(child; children) {
3761 				s ~= child.toString();//toPrettyString(false, 0, null);
3762 			}
3763 		} else {
3764 			foreach(child; children) {
3765 				assert(child !is null);
3766 
3767 				s ~= child.toPrettyStringImpl(insertComments, indentationLevel + 1, indentWith);
3768 			}
3769 
3770 			s ~= toPrettyStringIndent(insertComments, indentationLevel, indentWith);
3771 		}
3772 
3773 		s ~= "</";
3774 		s ~= tagName;
3775 		s ~= ">";
3776 
3777 		return s;
3778 	}
3779 
3780 	/+
3781 	/// Writes out the opening tag only, if applicable.
3782 	string writeTagOnly(Appender!string where = appender!string()) const {
3783 	+/
3784 
3785 	/// This is the actual implementation used by toString. You can pass it a preallocated buffer to save some time.
3786 	/// Note: the ordering of attributes in the string is undefined.
3787 	/// Returns the string it creates.
3788 	string writeToAppender(Appender!string where = appender!string()) const {
3789 		assert(tagName !is null);
3790 
3791 		where.reserve((this.children.length + 1) * 512);
3792 
3793 		auto start = where.data.length;
3794 
3795 		where.put("<");
3796 		where.put(tagName);
3797 
3798 		import std.algorithm : sort;
3799 		auto keys = sort(attributes.keys);
3800 		foreach(n; keys) {
3801 			auto v = attributes[n]; // I am sorting these for convenience with another project. order of AAs is undefined, so I'm allowed to do it.... and it is still undefined, I might change it back later.
3802 			//assert(v !is null);
3803 			where.put(" ");
3804 			where.put(n);
3805 			where.put("=\"");
3806 			htmlEntitiesEncode(v, where);
3807 			where.put("\"");
3808 		}
3809 
3810 		if(selfClosed){
3811 			where.put(" />");
3812 			return where.data[start .. $];
3813 		}
3814 
3815 		where.put('>');
3816 
3817 		innerHTML(where);
3818 
3819 		where.put("</");
3820 		where.put(tagName);
3821 		where.put('>');
3822 
3823 		return where.data[start .. $];
3824 	}
3825 
3826 	/**
3827 		Returns a lazy range of all its children, recursively.
3828 	*/
3829 	@property ElementStream tree() {
3830 		return new ElementStream(this);
3831 	}
3832 
3833 	// I moved these from Form because they are generally useful.
3834 	// Ideally, I'd put them in arsd.html and use UFCS, but that doesn't work with the opDispatch here.
3835 	// FIXME: add overloads for other label types...
3836 	/++
3837 		Adds a form field to this element, normally a `<input>` but `type` can also be `"textarea"`.
3838 
3839 		This is fairly html specific and the label uses my style. I recommend you view the source before you use it to better understand what it does.
3840 	+/
3841 	/// Tags: HTML, HTML5
3842 	Element addField(string label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) {
3843 		auto fs = this;
3844 		auto i = fs.addChild("label");
3845 
3846 		if(!(type == "checkbox" || type == "radio"))
3847 			i.addChild("span", label);
3848 
3849 		Element input;
3850 		if(type == "textarea")
3851 			input = i.addChild("textarea").
3852 			setAttribute("name", name).
3853 			setAttribute("rows", "6");
3854 		else
3855 			input = i.addChild("input").
3856 			setAttribute("name", name).
3857 			setAttribute("type", type);
3858 
3859 		if(type == "checkbox" || type == "radio")
3860 			i.addChild("span", label);
3861 
3862 		// these are html 5 attributes; you'll have to implement fallbacks elsewhere. In Javascript or maybe I'll add a magic thing to html.d later.
3863 		fieldOptions.applyToElement(input);
3864 		return i;
3865 	}
3866 
3867 	/// ditto
3868 	Element addField(Element label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) {
3869 		auto fs = this;
3870 		auto i = fs.addChild("label");
3871 		i.addChild(label);
3872 		Element input;
3873 		if(type == "textarea")
3874 			input = i.addChild("textarea").
3875 			setAttribute("name", name).
3876 			setAttribute("rows", "6");
3877 		else
3878 			input = i.addChild("input").
3879 			setAttribute("name", name).
3880 			setAttribute("type", type);
3881 
3882 		// these are html 5 attributes; you'll have to implement fallbacks elsewhere. In Javascript or maybe I'll add a magic thing to html.d later.
3883 		fieldOptions.applyToElement(input);
3884 		return i;
3885 	}
3886 
3887 	/// ditto
3888 	Element addField(string label, string name, FormFieldOptions fieldOptions) {
3889 		return addField(label, name, "text", fieldOptions);
3890 	}
3891 
3892 	/// ditto
3893 	Element addField(string label, string name, string[string] options, FormFieldOptions fieldOptions = FormFieldOptions.none) {
3894 		auto fs = this;
3895 		auto i = fs.addChild("label");
3896 		i.addChild("span", label);
3897 		auto sel = i.addChild("select").setAttribute("name", name);
3898 
3899 		foreach(k, opt; options)
3900 			sel.addChild("option", opt, k);
3901 
3902 		// FIXME: implement requirements somehow
3903 
3904 		return i;
3905 	}
3906 
3907 	/// ditto
3908 	Element addSubmitButton(string label = null) {
3909 		auto t = this;
3910 		auto holder = t.addChild("div");
3911 		holder.addClass("submit-holder");
3912 		auto i = holder.addChild("input");
3913 		i.type = "submit";
3914 		if(label.length)
3915 			i.value = label;
3916 		return holder;
3917 	}
3918 
3919 }
3920 // computedStyle could argubaly be removed to bring size down
3921 //pragma(msg, __traits(classInstanceSize, Element));
3922 //pragma(msg, Element.tupleof);
3923 
3924 // FIXME: since Document loosens the input requirements, it should probably be the sub class...
3925 /++
3926 	Specializes Document for handling generic XML. (always uses strict mode, uses xml mime type and file header)
3927 
3928 	History:
3929 		On December 16, 2022, it disabled the special case treatment of `<script>` and `<style>` that [Document]
3930 		does for HTML. To get the old behavior back, add `, true` to your constructor call.
3931 +/
3932 /// Group: core_functionality
3933 class XmlDocument : Document {
3934 	this(string data, bool enableHtmlHacks = false) {
3935 		selfClosedElements = null;
3936 		inlineElements = null;
3937 		contentType = "text/xml; charset=utf-8";
3938 		_prolog = `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n";
3939 
3940 		parseStrict(data, !enableHtmlHacks);
3941 	}
3942 }
3943 
3944 unittest {
3945 	// FIXME: i should also make XmlDocument do different entities than just html too.
3946 	auto str = "<html><style>foo {}</style><script>void function() { a < b; }</script></html>";
3947 	auto document = new Document(str, true, true);
3948 	assert(document.requireSelector("style").children[0].tagName == "#raw");
3949 	assert(document.requireSelector("script").children[0].tagName == "#raw");
3950 	try {
3951 		auto xml = new XmlDocument(str);
3952 		assert(0);
3953 	} catch(MarkupException e) {
3954 		// failure expected, script special case is not valid XML without a dtd (which isn't here)
3955 	}
3956 	//assert(xml.requireSelector("style").children[0].tagName == "#raw");
3957 	//assert(xml.requireSelector("script").children[0].tagName == "#raw");
3958 }
3959 
3960 
3961 
3962 import std.string;
3963 
3964 /* domconvenience follows { */
3965 
3966 /// finds comments that match the given txt. Case insensitive, strips whitespace.
3967 /// Group: core_functionality
3968 Element[] findComments(Document document, string txt) {
3969 	return findComments(document.root, txt);
3970 }
3971 
3972 /// ditto
3973 Element[] findComments(Element element, string txt) {
3974 	txt = txt.strip().toLower();
3975 	Element[] ret;
3976 
3977 	foreach(comment; element.getElementsByTagName("#comment")) {
3978 		string t = comment.nodeValue().strip().toLower();
3979 		if(t == txt)
3980 			ret ~= comment;
3981 	}
3982 
3983 	return ret;
3984 }
3985 
3986 /// An option type that propagates null. See: [Element.optionSelector]
3987 /// Group: implementations
3988 struct MaybeNullElement(SomeElementType) {
3989 	this(SomeElementType ele) {
3990 		this.element = ele;
3991 	}
3992 	SomeElementType element;
3993 
3994 	/// Forwards to the element, wit a null check inserted that propagates null.
3995 	auto opDispatch(string method, T...)(T args) {
3996 		alias type = typeof(__traits(getMember, element, method)(args));
3997 		static if(is(type : Element)) {
3998 			if(element is null)
3999 				return MaybeNullElement!type(null);
4000 			return __traits(getMember, element, method)(args);
4001 		} else static if(is(type == string)) {
4002 			if(element is null)
4003 				return cast(string) null;
4004 			return __traits(getMember, element, method)(args);
4005 		} else static if(is(type == void)) {
4006 			if(element is null)
4007 				return;
4008 			__traits(getMember, element, method)(args);
4009 		} else {
4010 			static assert(0);
4011 		}
4012 	}
4013 
4014 	/// Allows implicit casting to the wrapped element.
4015 	alias element this;
4016 }
4017 
4018 /++
4019 	A collection of elements which forwards methods to the children.
4020 +/
4021 /// Group: implementations
4022 struct ElementCollection {
4023 	///
4024 	this(Element e) {
4025 		elements = [e];
4026 	}
4027 
4028 	///
4029 	this(Element e, string selector) {
4030 		elements = e.querySelectorAll(selector);
4031 	}
4032 
4033 	///
4034 	this(Element[] e) {
4035 		elements = e;
4036 	}
4037 
4038 	Element[] elements;
4039 	//alias elements this; // let it implicitly convert to the underlying array
4040 
4041 	///
4042 	ElementCollection opIndex(string selector) {
4043 		ElementCollection ec;
4044 		foreach(e; elements)
4045 			ec.elements ~= e.getElementsBySelector(selector);
4046 		return ec;
4047 	}
4048 
4049 	///
4050 	Element opIndex(int i) {
4051 		return elements[i];
4052 	}
4053 
4054 	/// if you slice it, give the underlying array for easy forwarding of the
4055 	/// collection to range expecting algorithms or looping over.
4056 	Element[] opSlice() {
4057 		return elements;
4058 	}
4059 
4060 	/// And input range primitives so we can foreach over this
4061 	void popFront() {
4062 		elements = elements[1..$];
4063 	}
4064 
4065 	/// ditto
4066 	Element front() {
4067 		return elements[0];
4068 	}
4069 
4070 	/// ditto
4071 	bool empty() {
4072 		return !elements.length;
4073 	}
4074 
4075 	/++
4076 		Collects strings from the collection, concatenating them together
4077 		Kinda like running reduce and ~= on it.
4078 
4079 		---
4080 		document["p"].collect!"innerText";
4081 		---
4082 	+/
4083 	string collect(string method)(string separator = "") {
4084 		string text;
4085 		foreach(e; elements) {
4086 			text ~= mixin("e." ~ method);
4087 			text ~= separator;
4088 		}
4089 		return text;
4090 	}
4091 
4092 	/// Forward method calls to each individual [Element|element] of the collection
4093 	/// returns this so it can be chained.
4094 	ElementCollection opDispatch(string name, T...)(T t) {
4095 		foreach(e; elements) {
4096 			mixin("e." ~ name)(t);
4097 		}
4098 		return this;
4099 	}
4100 
4101 	/++
4102 		Calls [Element.wrapIn] on each member of the collection, but clones the argument `what` for each one.
4103 	+/
4104 	ElementCollection wrapIn(Element what) {
4105 		foreach(e; elements) {
4106 			e.wrapIn(what.cloneNode(false));
4107 		}
4108 
4109 		return this;
4110 	}
4111 
4112 	/// Concatenates two ElementCollection together.
4113 	ElementCollection opBinary(string op : "~")(ElementCollection rhs) {
4114 		return ElementCollection(this.elements ~ rhs.elements);
4115 	}
4116 }
4117 
4118 
4119 /// this puts in operators and opDispatch to handle string indexes and properties, forwarding to get and set functions.
4120 /// Group: implementations
4121 mixin template JavascriptStyleDispatch() {
4122 	///
4123 	string opDispatch(string name)(string v = null) if(name != "popFront") { // popFront will make this look like a range. Do not want.
4124 		if(v !is null)
4125 			return set(name, v);
4126 		return get(name);
4127 	}
4128 
4129 	///
4130 	string opIndex(string key) const {
4131 		return get(key);
4132 	}
4133 
4134 	///
4135 	string opIndexAssign(string value, string field) {
4136 		return set(field, value);
4137 	}
4138 
4139 	// FIXME: doesn't seem to work
4140 	string* opBinary(string op)(string key)  if(op == "in") {
4141 		return key in fields;
4142 	}
4143 }
4144 
4145 /// A proxy object to do the Element class' dataset property. See Element.dataset for more info.
4146 ///
4147 /// Do not create this object directly.
4148 /// Group: implementations
4149 struct DataSet {
4150 	///
4151 	this(Element e) {
4152 		this._element = e;
4153 	}
4154 
4155 	private Element _element;
4156 	///
4157 	string set(string name, string value) {
4158 		_element.setAttribute("data-" ~ unCamelCase(name), value);
4159 		return value;
4160 	}
4161 
4162 	///
4163 	string get(string name) const {
4164 		return _element.getAttribute("data-" ~ unCamelCase(name));
4165 	}
4166 
4167 	///
4168 	mixin JavascriptStyleDispatch!();
4169 }
4170 
4171 /// Proxy object for attributes which will replace the main opDispatch eventually
4172 /// Group: implementations
4173 struct AttributeSet {
4174 	/// Generally, you shouldn't create this yourself, since you can use [Element.attrs] instead.
4175 	this(Element e) {
4176 		this._element = e;
4177 	}
4178 
4179 	private Element _element;
4180 	/++
4181 		Sets a `value` for attribute with `name`. If the attribute doesn't exist, this will create it, even if `value` is `null`.
4182 	+/
4183 	string set(string name, string value) {
4184 		_element.setAttribute(name, value);
4185 		return value;
4186 	}
4187 
4188 	/++
4189 		Provides support for testing presence of an attribute with the `in` operator.
4190 
4191 		History:
4192 			Added December 16, 2020 (dub v10.10)
4193 	+/
4194 	auto opBinaryRight(string op : "in")(string name) const
4195 	{
4196 		return name in _element.attributes;
4197 	}
4198 	///
4199 	unittest
4200 	{
4201 		auto doc = new XmlDocument(`<test attr="test"/>`);
4202 		assert("attr" in doc.root.attrs);
4203 		assert("test" !in doc.root.attrs);
4204 	}
4205 
4206 	/++
4207 		Returns the value of attribute `name`, or `null` if doesn't exist
4208 	+/
4209 	string get(string name) const {
4210 		return _element.getAttribute(name);
4211 	}
4212 
4213 	///
4214 	mixin JavascriptStyleDispatch!();
4215 }
4216 
4217 
4218 
4219 /// for style, i want to be able to set it with a string like a plain attribute,
4220 /// but also be able to do properties Javascript style.
4221 
4222 /// Group: implementations
4223 struct ElementStyle {
4224 	this(Element parent) {
4225 		_element = parent;
4226 	}
4227 
4228 	Element _element;
4229 
4230 	@property ref inout(string) _attribute() inout {
4231 		auto s = "style" in _element.attributes;
4232 		if(s is null) {
4233 			auto e = cast() _element; // const_cast
4234 			e.attributes["style"] = ""; // we need something to reference
4235 			s = cast(inout) ("style" in e.attributes);
4236 		}
4237 
4238 		assert(s !is null);
4239 		return *s;
4240 	}
4241 
4242 	alias _attribute this; // this is meant to allow element.style = element.style ~ " string "; to still work.
4243 
4244 	string set(string name, string value) {
4245 		if(name.length == 0)
4246 			return value;
4247 		if(name == "cssFloat")
4248 			name = "float";
4249 		else
4250 			name = unCamelCase(name);
4251 		auto r = rules();
4252 		r[name] = value;
4253 
4254 		_attribute = "";
4255 		foreach(k, v; r) {
4256 			if(v is null || v.length == 0) /* css can't do empty rules anyway so we'll use that to remove */
4257 				continue;
4258 			if(_attribute.length)
4259 				_attribute ~= " ";
4260 			_attribute ~= k ~ ": " ~ v ~ ";";
4261 		}
4262 
4263 		_element.setAttribute("style", _attribute); // this is to trigger the observer call
4264 
4265 		return value;
4266 	}
4267 	string get(string name) const {
4268 		if(name == "cssFloat")
4269 			name = "float";
4270 		else
4271 			name = unCamelCase(name);
4272 		auto r = rules();
4273 		if(name in r)
4274 			return r[name];
4275 		return null;
4276 	}
4277 
4278 	string[string] rules() const {
4279 		string[string] ret;
4280 		foreach(rule;  _attribute.split(";")) {
4281 			rule = rule.strip();
4282 			if(rule.length == 0)
4283 				continue;
4284 			auto idx = rule.indexOf(":");
4285 			if(idx == -1)
4286 				ret[rule] = "";
4287 			else {
4288 				auto name = rule[0 .. idx].strip();
4289 				auto value = rule[idx + 1 .. $].strip();
4290 
4291 				ret[name] = value;
4292 			}
4293 		}
4294 
4295 		return ret;
4296 	}
4297 
4298 	mixin JavascriptStyleDispatch!();
4299 }
4300 
4301 /// Converts a camel cased propertyName to a css style dashed property-name
4302 string unCamelCase(string a) {
4303 	string ret;
4304 	foreach(c; a)
4305 		if((c >= 'A' && c <= 'Z'))
4306 			ret ~= "-" ~ toLower("" ~ c)[0];
4307 		else
4308 			ret ~= c;
4309 	return ret;
4310 }
4311 
4312 /// Translates a css style property-name to a camel cased propertyName
4313 string camelCase(string a) {
4314 	string ret;
4315 	bool justSawDash = false;
4316 	foreach(c; a)
4317 		if(c == '-') {
4318 			justSawDash = true;
4319 		} else {
4320 			if(justSawDash) {
4321 				justSawDash = false;
4322 				ret ~= toUpper("" ~ c);
4323 			} else
4324 				ret ~= c;
4325 		}
4326 	return ret;
4327 }
4328 
4329 
4330 
4331 
4332 
4333 
4334 
4335 
4336 
4337 // domconvenience ends }
4338 
4339 
4340 
4341 
4342 
4343 
4344 
4345 
4346 
4347 
4348 
4349 // @safe:
4350 
4351 // NOTE: do *NOT* override toString on Element subclasses. It won't work.
4352 // Instead, override writeToAppender();
4353 
4354 // FIXME: should I keep processing instructions like <?blah ?> and <!-- blah --> (comments too lol)? I *want* them stripped out of most my output, but I want to be able to parse and create them too.
4355 
4356 // Stripping them is useful for reading php as html.... but adding them
4357 // is good for building php.
4358 
4359 // I need to maintain compatibility with the way it is now too.
4360 
4361 import std.string;
4362 import std.exception;
4363 import std.uri;
4364 import std.array;
4365 import std.range;
4366 
4367 //import std.stdio;
4368 
4369 // tag soup works for most the crap I know now! If you have two bad closing tags back to back, it might erase one, but meh
4370 // that's rarer than the flipped closing tags that hack fixes so I'm ok with it. (Odds are it should be erased anyway; it's
4371 // most likely a typo so I say kill kill kill.
4372 
4373 
4374 /++
4375 	This might belong in another module, but it represents a file with a mime type and some data.
4376 	Document implements this interface with type = text/html (see Document.contentType for more info)
4377 	and data = document.toString, so you can return Documents anywhere web.d expects FileResources.
4378 +/
4379 /// Group: bonus_functionality
4380 interface FileResource {
4381 	/// the content-type of the file. e.g. "text/html; charset=utf-8" or "image/png"
4382 	@property string contentType() const;
4383 	/// the data
4384 	immutable(ubyte)[] getData() const;
4385 	/++
4386 		filename, return null if none
4387 
4388 		History:
4389 			Added December 25, 2020
4390 	+/
4391 	@property string filename() const;
4392 }
4393 
4394 
4395 
4396 
4397 ///.
4398 /// Group: bonus_functionality
4399 enum NodeType { Text = 3 }
4400 
4401 
4402 /// You can use this to do an easy null check or a dynamic cast+null check on any element.
4403 /// Group: core_functionality
4404 T require(T = Element, string file = __FILE__, int line = __LINE__)(Element e) if(is(T : Element))
4405 	in {}
4406 	out(ret) { assert(ret !is null); }
4407 do {
4408 	auto ret = cast(T) e;
4409 	if(ret is null)
4410 		throw new ElementNotFoundException(T.stringof, "passed value", e, file, line);
4411 	return ret;
4412 }
4413 
4414 
4415 ///.
4416 /// Group: core_functionality
4417 class DocumentFragment : Element {
4418 	///.
4419 	this(Document _parentDocument) {
4420 		tagName = "#fragment";
4421 		super(_parentDocument);
4422 	}
4423 
4424 	/++
4425 		Creates a document fragment from the given HTML. Note that the HTML is assumed to close all tags contained inside it.
4426 
4427 		Since: March 29, 2018 (or git tagged v2.1.0)
4428 	+/
4429 	this(Html html) {
4430 		this(null);
4431 
4432 		this.innerHTML = html.source;
4433 	}
4434 
4435 	///.
4436 	override string writeToAppender(Appender!string where = appender!string()) const {
4437 		return this.innerHTML(where);
4438 	}
4439 
4440 	override string toPrettyStringImpl(bool insertComments, int indentationLevel, string indentWith) const {
4441 		string s;
4442 		foreach(child; children)
4443 			s ~= child.toPrettyStringImpl(insertComments, indentationLevel, indentWith);
4444 		return s;
4445 	}
4446 
4447 	/// DocumentFragments don't really exist in a dom, so they ignore themselves in parent nodes
4448 	/*
4449 	override inout(Element) parentNode() inout {
4450 		return children.length ? children[0].parentNode : null;
4451 	}
4452 	*/
4453 	/+
4454 	override Element parentNode(Element p) {
4455 		this.parentNode = p;
4456 		foreach(child; children)
4457 			child.parentNode = p;
4458 		return p;
4459 	}
4460 	+/
4461 }
4462 
4463 /// Given text, encode all html entities on it - &, <, >, and ". This function also
4464 /// encodes all 8 bit characters as entities, thus ensuring the resultant text will work
4465 /// even if your charset isn't set right. You can suppress with by setting encodeNonAscii = false
4466 ///
4467 /// The output parameter can be given to append to an existing buffer. You don't have to
4468 /// pass one; regardless, the return value will be usable for you, with just the data encoded.
4469 /// Group: core_functionality
4470 string htmlEntitiesEncode(string data, Appender!string output = appender!string(), bool encodeNonAscii = true) {
4471 	// if there's no entities, we can save a lot of time by not bothering with the
4472 	// decoding loop. This check cuts the net toString time by better than half in my test.
4473 	// let me know if it made your tests worse though, since if you use an entity in just about
4474 	// every location, the check will add time... but I suspect the average experience is like mine
4475 	// since the check gives up as soon as it can anyway.
4476 
4477 	bool shortcut = true;
4478 	foreach(char c; data) {
4479 		// non ascii chars are always higher than 127 in utf8; we'd better go to the full decoder if we see it.
4480 		if(c == '<' || c == '>' || c == '"' || c == '&' || (encodeNonAscii && cast(uint) c > 127)) {
4481 			shortcut = false; // there's actual work to be done
4482 			break;
4483 		}
4484 	}
4485 
4486 	if(shortcut) {
4487 		output.put(data);
4488 		return data;
4489 	}
4490 
4491 	auto start = output.data.length;
4492 
4493 	output.reserve(data.length + 64); // grab some extra space for the encoded entities
4494 
4495 	foreach(dchar d; data) {
4496 		if(d == '&')
4497 			output.put("&amp;");
4498 		else if (d == '<')
4499 			output.put("&lt;");
4500 		else if (d == '>')
4501 			output.put("&gt;");
4502 		else if (d == '\"')
4503 			output.put("&quot;");
4504 //		else if (d == '\'')
4505 //			output.put("&#39;"); // if you are in an attribute, it might be important to encode for the same reason as double quotes
4506 			// FIXME: should I encode apostrophes too? as &#39;... I could also do space but if your html is so bad that it doesn't
4507 			// quote attributes at all, maybe you deserve the xss. Encoding spaces will make everything really ugly so meh
4508 			// idk about apostrophes though. Might be worth it, might not.
4509 		else if (!encodeNonAscii || (d < 128 && d > 0))
4510 			output.put(d);
4511 		else
4512 			output.put("&#" ~ std.conv.to!string(cast(int) d) ~ ";");
4513 	}
4514 
4515 	//assert(output !is null); // this fails on empty attributes.....
4516 	return output.data[start .. $];
4517 
4518 //	data = data.replace("\u00a0", "&nbsp;");
4519 }
4520 
4521 /// An alias for htmlEntitiesEncode; it works for xml too
4522 /// Group: core_functionality
4523 string xmlEntitiesEncode(string data) {
4524 	return htmlEntitiesEncode(data);
4525 }
4526 
4527 /// This helper function is used for decoding html entities. It has a hard-coded list of entities and characters.
4528 /// Group: core_functionality
4529 dchar parseEntity(in dchar[] entity) {
4530 
4531 	char[128] buffer;
4532 	int bpos;
4533 	foreach(char c; entity[1 .. $-1])
4534 		buffer[bpos++] = c;
4535 	char[] entityAsString = buffer[0 .. bpos];
4536 
4537 	int min = 0;
4538 	int max = cast(int) availableEntities.length;
4539 
4540 	keep_looking:
4541 	if(min + 1 < max) {
4542 		int spot = (max - min) / 2 + min;
4543 		if(availableEntities[spot] == entityAsString) {
4544 			return availableEntitiesValues[spot];
4545 		} else if(entityAsString < availableEntities[spot]) {
4546 			max = spot;
4547 			goto keep_looking;
4548 		} else {
4549 			min = spot;
4550 			goto keep_looking;
4551 		}
4552 	}
4553 
4554 	switch(entity[1..$-1]) {
4555 		case "quot":
4556 			return '"';
4557 		case "apos":
4558 			return '\'';
4559 		case "lt":
4560 			return '<';
4561 		case "gt":
4562 			return '>';
4563 		case "amp":
4564 			return '&';
4565 		// the next are html rather than xml
4566 
4567 		// and handling numeric entities
4568 		default:
4569 			if(entity[1] == '#') {
4570 				if(entity[2] == 'x' /*|| (!strict && entity[2] == 'X')*/) {
4571 					auto hex = entity[3..$-1];
4572 
4573 					auto p = intFromHex(to!string(hex).toLower());
4574 					return cast(dchar) p;
4575 				} else {
4576 					auto decimal = entity[2..$-1];
4577 
4578 					// dealing with broken html entities
4579 					while(decimal.length && (decimal[0] < '0' || decimal[0] >   '9'))
4580 						decimal = decimal[1 .. $];
4581 
4582 					while(decimal.length && (decimal[$-1] < '0' || decimal[$-1] >   '9'))
4583 						decimal = decimal[0 .. $ - 1];
4584 
4585 					if(decimal.length == 0)
4586 						return ' '; // this is really broken html
4587 					// done with dealing with broken stuff
4588 
4589 					auto p = std.conv.to!int(decimal);
4590 					return cast(dchar) p;
4591 				}
4592 			} else
4593 				return '\ufffd'; // replacement character diamond thing
4594 	}
4595 
4596 	assert(0);
4597 }
4598 
4599 unittest {
4600 	// not in the binary search
4601 	assert(parseEntity("&quot;"d) == '"');
4602 
4603 	// numeric value
4604 	assert(parseEntity("&#x0534;") == '\u0534');
4605 
4606 	// not found at all
4607 	assert(parseEntity("&asdasdasd;"d) == '\ufffd');
4608 
4609 	// random values in the bin search
4610 	assert(parseEntity("&Tab;"d) == '\t');
4611 	assert(parseEntity("&raquo;"d) == '\&raquo;');
4612 
4613 	// near the middle and edges of the bin search
4614 	assert(parseEntity("&ascr;"d) == '\U0001d4b6');
4615 	assert(parseEntity("&ast;"d) == '\u002a');
4616 	assert(parseEntity("&AElig;"d) == '\u00c6');
4617 	assert(parseEntity("&zwnj;"d) == '\u200c');
4618 }
4619 
4620 import std.utf;
4621 import std.stdio;
4622 
4623 /// This takes a string of raw HTML and decodes the entities into a nice D utf-8 string.
4624 /// By default, it uses loose mode - it will try to return a useful string from garbage input too.
4625 /// Set the second parameter to true if you'd prefer it to strictly throw exceptions on garbage input.
4626 /// Group: core_functionality
4627 string htmlEntitiesDecode(string data, bool strict = false) {
4628 	// this check makes a *big* difference; about a 50% improvement of parse speed on my test.
4629 	if(data.indexOf("&") == -1) // all html entities begin with &
4630 		return data; // if there are no entities in here, we can return the original slice and save some time
4631 
4632 	char[] a; // this seems to do a *better* job than appender!
4633 
4634 	char[4] buffer;
4635 
4636 	bool tryingEntity = false;
4637 	bool tryingNumericEntity = false;
4638 	bool tryingHexEntity = false;
4639 	dchar[16] entityBeingTried;
4640 	int entityBeingTriedLength = 0;
4641 	int entityAttemptIndex = 0;
4642 
4643 	foreach(dchar ch; data) {
4644 		if(tryingEntity) {
4645 			entityAttemptIndex++;
4646 			entityBeingTried[entityBeingTriedLength++] = ch;
4647 
4648 			if(entityBeingTriedLength == 2 && ch == '#') {
4649 				tryingNumericEntity = true;
4650 				continue;
4651 			} else if(tryingNumericEntity && entityBeingTriedLength == 3 && ch == 'x') {
4652 				tryingHexEntity = true;
4653 				continue;
4654 			}
4655 
4656 			// I saw some crappy html in the wild that looked like &0&#1111; this tries to handle that.
4657 			if(ch == '&') {
4658 				if(strict)
4659 					throw new Exception("unterminated entity; & inside another at " ~ to!string(entityBeingTried[0 .. entityBeingTriedLength]));
4660 
4661 				// if not strict, let's try to parse both.
4662 
4663 				if(entityBeingTried[0 .. entityBeingTriedLength] == "&&") {
4664 					a ~= "&"; // double amp means keep the first one, still try to parse the next one
4665 				} else {
4666 					auto ch2 = parseEntity(entityBeingTried[0 .. entityBeingTriedLength]);
4667 					if(ch2 == '\ufffd') { // either someone put this in intentionally (lol) or we failed to get it
4668 						// but either way, just abort and keep the plain text
4669 						foreach(char c; entityBeingTried[0 .. entityBeingTriedLength - 1]) // cut off the & we're on now
4670 							a ~= c;
4671 					} else {
4672 						a ~= buffer[0.. std.utf.encode(buffer, ch2)];
4673 					}
4674 				}
4675 
4676 				// tryingEntity is still true
4677 				goto new_entity;
4678 			} else
4679 			if(ch == ';') {
4680 				tryingEntity = false;
4681 				a ~= buffer[0.. std.utf.encode(buffer, parseEntity(entityBeingTried[0 .. entityBeingTriedLength]))];
4682 			} else if(ch == ' ') {
4683 				// e.g. you &amp i
4684 				if(strict)
4685 					throw new Exception("unterminated entity at " ~ to!string(entityBeingTried[0 .. entityBeingTriedLength]));
4686 				else {
4687 					tryingEntity = false;
4688 					a ~= to!(char[])(entityBeingTried[0 .. entityBeingTriedLength - 1]);
4689 					a ~= buffer[0 .. std.utf.encode(buffer, ch)];
4690 				}
4691 			} else {
4692 				if(tryingNumericEntity) {
4693 					if(ch < '0' || ch > '9') {
4694 						if(tryingHexEntity) {
4695 							if(ch < 'A')
4696 								goto trouble;
4697 							if(ch > 'Z' && ch < 'a')
4698 								goto trouble;
4699 							if(ch > 'z')
4700 								goto trouble;
4701 						} else {
4702 							trouble:
4703 							if(strict)
4704 								throw new Exception("unterminated entity at " ~ to!string(entityBeingTried[0 .. entityBeingTriedLength]));
4705 							tryingEntity = false;
4706 							a ~= buffer[0.. std.utf.encode(buffer, parseEntity(entityBeingTried[0 .. entityBeingTriedLength]))];
4707 							a ~= ch;
4708 							continue;
4709 						}
4710 					}
4711 				}
4712 
4713 
4714 				if(entityAttemptIndex >= 9) {
4715 					done:
4716 					if(strict)
4717 						throw new Exception("unterminated entity at " ~ to!string(entityBeingTried[0 .. entityBeingTriedLength]));
4718 					else {
4719 						tryingEntity = false;
4720 						a ~= to!(char[])(entityBeingTried[0 .. entityBeingTriedLength]);
4721 					}
4722 				}
4723 			}
4724 		} else {
4725 			if(ch == '&') {
4726 				new_entity:
4727 				tryingEntity = true;
4728 				tryingNumericEntity = false;
4729 				tryingHexEntity = false;
4730 				entityBeingTriedLength = 0;
4731 				entityBeingTried[entityBeingTriedLength++] = ch;
4732 				entityAttemptIndex = 0;
4733 			} else {
4734 				a ~= buffer[0 .. std.utf.encode(buffer, ch)];
4735 			}
4736 		}
4737 	}
4738 
4739 	if(tryingEntity) {
4740 		if(strict)
4741 			throw new Exception("unterminated entity at " ~ to!string(entityBeingTried[0 .. entityBeingTriedLength]));
4742 
4743 		// otherwise, let's try to recover, at least so we don't drop any data
4744 		a ~= to!string(entityBeingTried[0 .. entityBeingTriedLength]);
4745 		// FIXME: what if we have "cool &amp"? should we try to parse it?
4746 	}
4747 
4748 	return cast(string) a; // assumeUnique is actually kinda slow, lol
4749 }
4750 
4751 unittest {
4752 	// error recovery
4753 	assert(htmlEntitiesDecode("&lt;&foo") == "<&foo"); // unterminated turned back to thing
4754 	assert(htmlEntitiesDecode("&lt&foo") == "<&foo"); // semi-terminated... parse and carry on (is this really sane?)
4755 	assert(htmlEntitiesDecode("loc&#61en_us&tracknum&#61;111") == "loc=en_us&tracknum=111"); // a bit of both, seen in a real life email
4756 	assert(htmlEntitiesDecode("&amp test") == "&amp test"); // unterminated, just abort
4757 
4758 	// in strict mode all of these should fail
4759 	try { assert(htmlEntitiesDecode("&lt;&foo", true) == "<&foo"); assert(0); } catch(Exception e) { }
4760 	try { assert(htmlEntitiesDecode("&lt&foo", true) == "<&foo"); assert(0); } catch(Exception e) { }
4761 	try { assert(htmlEntitiesDecode("loc&#61en_us&tracknum&#61;111", true) == "<&foo"); assert(0); } catch(Exception e) { }
4762 	try { assert(htmlEntitiesDecode("&amp test", true) == "& test"); assert(0); } catch(Exception e) { }
4763 
4764 	// correct cases that should pass the same in strict or loose mode
4765 	foreach(strict; [false, true]) {
4766 		assert(htmlEntitiesDecode("&amp;hello&raquo; win", strict) == "&hello\&raquo; win");
4767 	}
4768 }
4769 
4770 /// Group: implementations
4771 abstract class SpecialElement : Element {
4772 	this(Document _parentDocument) {
4773 		super(_parentDocument);
4774 	}
4775 
4776 	///.
4777 	override Element appendChild(Element e) {
4778 		assert(0, "Cannot append to a special node");
4779 	}
4780 
4781 	///.
4782 	@property override int nodeType() const {
4783 		return 100;
4784 	}
4785 }
4786 
4787 ///.
4788 /// Group: implementations
4789 class RawSource : SpecialElement {
4790 	///.
4791 	this(Document _parentDocument, string s) {
4792 		super(_parentDocument);
4793 		source = s;
4794 		tagName = "#raw";
4795 	}
4796 
4797 	///.
4798 	override string nodeValue() const {
4799 		return this.toString();
4800 	}
4801 
4802 	///.
4803 	override string writeToAppender(Appender!string where = appender!string()) const {
4804 		where.put(source);
4805 		return source;
4806 	}
4807 
4808 	override string toPrettyStringImpl(bool, int, string) const {
4809 		return source;
4810 	}
4811 
4812 
4813 	override RawSource cloneNode(bool deep) {
4814 		return new RawSource(parentDocument, source);
4815 	}
4816 
4817 	///.
4818 	string source;
4819 }
4820 
4821 /// Group: implementations
4822 abstract class ServerSideCode : SpecialElement {
4823 	this(Document _parentDocument, string type) {
4824 		super(_parentDocument);
4825 		tagName = "#" ~ type;
4826 	}
4827 
4828 	///.
4829 	override string nodeValue() const {
4830 		return this.source;
4831 	}
4832 
4833 	///.
4834 	override string writeToAppender(Appender!string where = appender!string()) const {
4835 		auto start = where.data.length;
4836 		where.put("<");
4837 		where.put(source);
4838 		where.put(">");
4839 		return where.data[start .. $];
4840 	}
4841 
4842 	override string toPrettyStringImpl(bool, int, string) const {
4843 		return "<" ~ source ~ ">";
4844 	}
4845 
4846 	///.
4847 	string source;
4848 }
4849 
4850 ///.
4851 /// Group: implementations
4852 class PhpCode : ServerSideCode {
4853 	///.
4854 	this(Document _parentDocument, string s) {
4855 		super(_parentDocument, "php");
4856 		source = s;
4857 	}
4858 
4859 	override PhpCode cloneNode(bool deep) {
4860 		return new PhpCode(parentDocument, source);
4861 	}
4862 }
4863 
4864 ///.
4865 /// Group: implementations
4866 class AspCode : ServerSideCode {
4867 	///.
4868 	this(Document _parentDocument, string s) {
4869 		super(_parentDocument, "asp");
4870 		source = s;
4871 	}
4872 
4873 	override AspCode cloneNode(bool deep) {
4874 		return new AspCode(parentDocument, source);
4875 	}
4876 }
4877 
4878 ///.
4879 /// Group: implementations
4880 class BangInstruction : SpecialElement {
4881 	///.
4882 	this(Document _parentDocument, string s) {
4883 		super(_parentDocument);
4884 		source = s;
4885 		tagName = "#bpi";
4886 	}
4887 
4888 	///.
4889 	override string nodeValue() const {
4890 		return this.source;
4891 	}
4892 
4893 	override BangInstruction cloneNode(bool deep) {
4894 		return new BangInstruction(parentDocument, source);
4895 	}
4896 
4897 	///.
4898 	override string writeToAppender(Appender!string where = appender!string()) const {
4899 		auto start = where.data.length;
4900 		where.put("<!");
4901 		where.put(source);
4902 		where.put(">");
4903 		return where.data[start .. $];
4904 	}
4905 
4906 	override string toPrettyStringImpl(bool, int, string) const {
4907 		string s;
4908 		s ~= "<!";
4909 		s ~= source;
4910 		s ~= ">";
4911 		return s;
4912 	}
4913 
4914 	///.
4915 	string source;
4916 }
4917 
4918 ///.
4919 /// Group: implementations
4920 class QuestionInstruction : SpecialElement {
4921 	///.
4922 	this(Document _parentDocument, string s) {
4923 		super(_parentDocument);
4924 		source = s;
4925 		tagName = "#qpi";
4926 	}
4927 
4928 	override QuestionInstruction cloneNode(bool deep) {
4929 		return new QuestionInstruction(parentDocument, source);
4930 	}
4931 
4932 	///.
4933 	override string nodeValue() const {
4934 		return this.source;
4935 	}
4936 
4937 	///.
4938 	override string writeToAppender(Appender!string where = appender!string()) const {
4939 		auto start = where.data.length;
4940 		where.put("<");
4941 		where.put(source);
4942 		where.put(">");
4943 		return where.data[start .. $];
4944 	}
4945 
4946 	override string toPrettyStringImpl(bool, int, string) const {
4947 		string s;
4948 		s ~= "<";
4949 		s ~= source;
4950 		s ~= ">";
4951 		return s;
4952 	}
4953 
4954 
4955 	///.
4956 	string source;
4957 }
4958 
4959 ///.
4960 /// Group: implementations
4961 class HtmlComment : SpecialElement {
4962 	///.
4963 	this(Document _parentDocument, string s) {
4964 		super(_parentDocument);
4965 		source = s;
4966 		tagName = "#comment";
4967 	}
4968 
4969 	override HtmlComment cloneNode(bool deep) {
4970 		return new HtmlComment(parentDocument, source);
4971 	}
4972 
4973 	///.
4974 	override string nodeValue() const {
4975 		return this.source;
4976 	}
4977 
4978 	///.
4979 	override string writeToAppender(Appender!string where = appender!string()) const {
4980 		auto start = where.data.length;
4981 		where.put("<!--");
4982 		where.put(source);
4983 		where.put("-->");
4984 		return where.data[start .. $];
4985 	}
4986 
4987 	override string toPrettyStringImpl(bool, int, string) const {
4988 		string s;
4989 		s ~= "<!--";
4990 		s ~= source;
4991 		s ~= "-->";
4992 		return s;
4993 	}
4994 
4995 
4996 	///.
4997 	string source;
4998 }
4999 
5000 
5001 
5002 
5003 ///.
5004 /// Group: implementations
5005 class TextNode : Element {
5006   public:
5007 	///.
5008 	this(Document _parentDocument, string e) {
5009 		super(_parentDocument);
5010 		contents = e;
5011 		tagName = "#text";
5012 	}
5013 
5014 	///
5015 	this(string e) {
5016 		this(null, e);
5017 	}
5018 
5019 	string opDispatch(string name)(string v = null) if(0) { return null; } // text nodes don't have attributes
5020 
5021 	///.
5022 	static TextNode fromUndecodedString(Document _parentDocument, string html) {
5023 		auto e = new TextNode(_parentDocument, "");
5024 		e.contents = htmlEntitiesDecode(html, _parentDocument is null ? false : !_parentDocument.loose);
5025 		return e;
5026 	}
5027 
5028 	///.
5029 	override @property TextNode cloneNode(bool deep) {
5030 		auto n = new TextNode(parentDocument, contents);
5031 		return n;
5032 	}
5033 
5034 	///.
5035 	override string nodeValue() const {
5036 		return this.contents; //toString();
5037 	}
5038 
5039 	///.
5040 	@property override int nodeType() const {
5041 		return NodeType.Text;
5042 	}
5043 
5044 	///.
5045 	override string writeToAppender(Appender!string where = appender!string()) const {
5046 		string s;
5047 		if(contents.length)
5048 			s = htmlEntitiesEncode(contents, where);
5049 		else
5050 			s = "";
5051 
5052 		assert(s !is null);
5053 		return s;
5054 	}
5055 
5056 	override string toPrettyStringImpl(bool insertComments = false, int indentationLevel = 0, string indentWith = "\t") const {
5057 		string s;
5058 
5059 		string contents = this.contents;
5060 		// we will first collapse the whitespace per html
5061 		// sort of. note this can break stuff yo!!!!
5062 		if(this.parentNode is null || this.parentNode.tagName != "pre") {
5063 			string n = "";
5064 			bool lastWasWhitespace = indentationLevel > 0;
5065 			foreach(char c; contents) {
5066 				if(c.isSimpleWhite) {
5067 					if(!lastWasWhitespace)
5068 						n ~= ' ';
5069 					lastWasWhitespace = true;
5070 				} else {
5071 					n ~= c;
5072 					lastWasWhitespace = false;
5073 				}
5074 			}
5075 
5076 			contents = n;
5077 		}
5078 
5079 		if(this.parentNode !is null && this.parentNode.tagName != "p") {
5080 			contents = contents.strip;
5081 		}
5082 
5083 		auto e = htmlEntitiesEncode(contents);
5084 		import std.algorithm.iteration : splitter;
5085 		bool first = true;
5086 		foreach(line; splitter(e, "\n")) {
5087 			if(first) {
5088 				s ~= toPrettyStringIndent(insertComments, indentationLevel, indentWith);
5089 				first = false;
5090 			} else {
5091 				s ~= "\n";
5092 				if(insertComments)
5093 					s ~= "<!--";
5094 				foreach(i; 0 .. indentationLevel)
5095 					s ~= "\t";
5096 				if(insertComments)
5097 					s ~= "-->";
5098 			}
5099 			s ~= line.stripRight;
5100 		}
5101 		return s;
5102 	}
5103 
5104 	///.
5105 	override Element appendChild(Element e) {
5106 		assert(0, "Cannot append to a text node");
5107 	}
5108 
5109 	///.
5110 	string contents;
5111 	// alias contents content; // I just mistype this a lot,
5112 }
5113 
5114 /**
5115 	There are subclasses of Element offering improved helper
5116 	functions for the element in HTML.
5117 */
5118 
5119 /++
5120 	Represents a HTML link. This provides some convenience methods for manipulating query strings, but otherwise is sthe same Element interface.
5121 
5122 	Please note this object may not be used for all `<a>` tags.
5123 +/
5124 /// Group: implementations
5125 class Link : Element {
5126 
5127 	/++
5128 		Constructs `<a href="that href">that text</a>`.
5129 	+/
5130 	this(string href, string text) {
5131 		super("a");
5132 		setAttribute("href", href);
5133 		innerText = text;
5134 	}
5135 
5136 	/// ditto
5137 	this(Document _parentDocument) {
5138 		super(_parentDocument);
5139 		this.tagName = "a";
5140 	}
5141 
5142 /+
5143 	/// Returns everything in the href EXCEPT the query string
5144 	@property string targetSansQuery() {
5145 
5146 	}
5147 
5148 	///.
5149 	@property string domainName() {
5150 
5151 	}
5152 
5153 	///.
5154 	@property string path
5155 +/
5156 	/// This gets a variable from the URL's query string.
5157 	string getValue(string name) {
5158 		auto vars = variablesHash();
5159 		if(name in vars)
5160 			return vars[name];
5161 		return null;
5162 	}
5163 
5164 	private string[string] variablesHash() {
5165 		string href = getAttribute("href");
5166 		if(href is null)
5167 			return null;
5168 
5169 		auto ques = href.indexOf("?");
5170 		string str = "";
5171 		if(ques != -1) {
5172 			str = href[ques+1..$];
5173 
5174 			auto fragment = str.indexOf("#");
5175 			if(fragment != -1)
5176 				str = str[0..fragment];
5177 		}
5178 
5179 		string[] variables = str.split("&");
5180 
5181 		string[string] hash;
5182 
5183 		foreach(var; variables) {
5184 			auto index = var.indexOf("=");
5185 			if(index == -1)
5186 				hash[var] = "";
5187 			else {
5188 				hash[decodeComponent(var[0..index])] = decodeComponent(var[index + 1 .. $]);
5189 			}
5190 		}
5191 
5192 		return hash;
5193 	}
5194 
5195 	/// Replaces all the stuff after a ? in the link at once with the given assoc array values.
5196 	/*private*/ void updateQueryString(string[string] vars) {
5197 		string href = getAttribute("href");
5198 
5199 		auto question = href.indexOf("?");
5200 		if(question != -1)
5201 			href = href[0..question];
5202 
5203 		string frag = "";
5204 		auto fragment = href.indexOf("#");
5205 		if(fragment != -1) {
5206 			frag = href[fragment..$];
5207 			href = href[0..fragment];
5208 		}
5209 
5210 		string query = "?";
5211 		bool first = true;
5212 		foreach(name, value; vars) {
5213 			if(!first)
5214 				query ~= "&";
5215 			else
5216 				first = false;
5217 
5218 			query ~= encodeComponent(name);
5219 			if(value.length)
5220 				query ~= "=" ~ encodeComponent(value);
5221 		}
5222 
5223 		if(query != "?")
5224 			href ~= query;
5225 
5226 		href ~= frag;
5227 
5228 		setAttribute("href", href);
5229 	}
5230 
5231 	/// Sets or adds the variable with the given name to the given value
5232 	/// It automatically URI encodes the values and takes care of the ? and &.
5233 	override void setValue(string name, string variable) {
5234 		auto vars = variablesHash();
5235 		vars[name] = variable;
5236 
5237 		updateQueryString(vars);
5238 	}
5239 
5240 	/// Removes the given variable from the query string
5241 	void removeValue(string name) {
5242 		auto vars = variablesHash();
5243 		vars.remove(name);
5244 
5245 		updateQueryString(vars);
5246 	}
5247 
5248 	/*
5249 	///.
5250 	override string toString() {
5251 
5252 	}
5253 
5254 	///.
5255 	override string getAttribute(string name) {
5256 		if(name == "href") {
5257 
5258 		} else
5259 			return super.getAttribute(name);
5260 	}
5261 	*/
5262 }
5263 
5264 /++
5265 	Represents a HTML form. This slightly specializes Element to add a few more convenience methods for adding and extracting form data.
5266 
5267 	Please note this object may not be used for all `<form>` tags.
5268 +/
5269 /// Group: implementations
5270 class Form : Element {
5271 
5272 	///.
5273 	this(Document _parentDocument) {
5274 		super(_parentDocument);
5275 		tagName = "form";
5276 	}
5277 
5278 	/// Overrides of the base class implementations that more confirm to *my* conventions when writing form html.
5279 	override Element addField(string label, string name, string type = "text", FormFieldOptions fieldOptions = FormFieldOptions.none) {
5280 		auto t = this.querySelector("fieldset div");
5281 		if(t is null)
5282 			return super.addField(label, name, type, fieldOptions);
5283 		else
5284 			return t.addField(label, name, type, fieldOptions);
5285 	}
5286 
5287 	/// ditto
5288 	override Element addField(string label, string name, FormFieldOptions fieldOptions) {
5289 		auto type = "text";
5290 		auto t = this.querySelector("fieldset div");
5291 		if(t is null)
5292 			return super.addField(label, name, type, fieldOptions);
5293 		else
5294 			return t.addField(label, name, type, fieldOptions);
5295 	}
5296 
5297 	/// ditto
5298 	override Element addField(string label, string name, string[string] options, FormFieldOptions fieldOptions = FormFieldOptions.none) {
5299 		auto t = this.querySelector("fieldset div");
5300 		if(t is null)
5301 			return super.addField(label, name, options, fieldOptions);
5302 		else
5303 			return t.addField(label, name, options, fieldOptions);
5304 	}
5305 
5306 	/// ditto
5307 	override void setValue(string field, string value) {
5308 		setValue(field, value, true);
5309 	}
5310 
5311 	// FIXME: doesn't handle arrays; multiple fields can have the same name
5312 
5313 	/// Set's the form field's value. For input boxes, this sets the value attribute. For
5314 	/// textareas, it sets the innerText. For radio boxes and select boxes, it removes
5315 	/// the checked/selected attribute from all, and adds it to the one matching the value.
5316 	/// For checkboxes, if the value is non-null and not empty, it checks the box.
5317 
5318 	/// If you set a value that doesn't exist, it throws an exception if makeNew is false.
5319 	/// Otherwise, it makes a new input with type=hidden to keep the value.
5320 	void setValue(string field, string value, bool makeNew) {
5321 		auto eles = getField(field);
5322 		if(eles.length == 0) {
5323 			if(makeNew) {
5324 				addInput(field, value);
5325 				return;
5326 			} else
5327 				throw new Exception("form field does not exist");
5328 		}
5329 
5330 		if(eles.length == 1) {
5331 			auto e = eles[0];
5332 			switch(e.tagName) {
5333 				default: assert(0);
5334 				case "textarea":
5335 					e.innerText = value;
5336 				break;
5337 				case "input":
5338 					string type = e.getAttribute("type");
5339 					if(type is null) {
5340 						e.value = value;
5341 						return;
5342 					}
5343 					switch(type) {
5344 						case "checkbox":
5345 						case "radio":
5346 							if(value.length && value != "false")
5347 								e.setAttribute("checked", "checked");
5348 							else
5349 								e.removeAttribute("checked");
5350 						break;
5351 						default:
5352 							e.value = value;
5353 							return;
5354 					}
5355 				break;
5356 				case "select":
5357 					bool found = false;
5358 					foreach(child; e.tree) {
5359 						if(child.tagName != "option")
5360 							continue;
5361 						string val = child.getAttribute("value");
5362 						if(val is null)
5363 							val = child.innerText;
5364 						if(val == value) {
5365 							child.setAttribute("selected", "selected");
5366 							found = true;
5367 						} else
5368 							child.removeAttribute("selected");
5369 					}
5370 
5371 					if(!found) {
5372 						e.addChild("option", value)
5373 						.setAttribute("selected", "selected");
5374 					}
5375 				break;
5376 			}
5377 		} else {
5378 			// assume radio boxes
5379 			foreach(e; eles) {
5380 				string val = e.getAttribute("value");
5381 				//if(val is null)
5382 				//	throw new Exception("don't know what to do with radio boxes with null value");
5383 				if(val == value)
5384 					e.setAttribute("checked", "checked");
5385 				else
5386 					e.removeAttribute("checked");
5387 			}
5388 		}
5389 	}
5390 
5391 	/// This takes an array of strings and adds hidden <input> elements for each one of them. Unlike setValue,
5392 	/// it makes no attempt to find and modify existing elements in the form to the new values.
5393 	void addValueArray(string key, string[] arrayOfValues) {
5394 		foreach(arr; arrayOfValues)
5395 			addChild("input", key, arr);
5396 	}
5397 
5398 	/// Gets the value of the field; what would be given if it submitted right now. (so
5399 	/// it handles select boxes and radio buttons too). For checkboxes, if a value isn't
5400 	/// given, but it is checked, it returns "checked", since null and "" are indistinguishable
5401 	string getValue(string field) {
5402 		auto eles = getField(field);
5403 		if(eles.length == 0)
5404 			return "";
5405 		if(eles.length == 1) {
5406 			auto e = eles[0];
5407 			switch(e.tagName) {
5408 				default: assert(0);
5409 				case "input":
5410 					if(e.type == "checkbox") {
5411 						if(e.checked)
5412 							return e.value.length ? e.value : "checked";
5413 						return "";
5414 					} else
5415 						return e.value;
5416 				case "textarea":
5417 					return e.innerText;
5418 				case "select":
5419 					foreach(child; e.tree) {
5420 						if(child.tagName != "option")
5421 							continue;
5422 						if(child.selected)
5423 							return child.value;
5424 					}
5425 				break;
5426 			}
5427 		} else {
5428 			// assuming radio
5429 			foreach(e; eles) {
5430 				if(e.checked)
5431 					return e.value;
5432 			}
5433 		}
5434 
5435 		return "";
5436 	}
5437 
5438 	// FIXME: doesn't handle multiple elements with the same name (except radio buttons)
5439 	/++
5440 		Returns the form's contents in application/x-www-form-urlencoded format.
5441 
5442 		Bugs:
5443 			Doesn't handle repeated elements of the same name nor files.
5444 	+/
5445 	string getPostableData() {
5446 		bool[string] namesDone;
5447 
5448 		string ret;
5449 		bool outputted = false;
5450 
5451 		foreach(e; getElementsBySelector("[name]")) {
5452 			if(e.name in namesDone)
5453 				continue;
5454 
5455 			if(outputted)
5456 				ret ~= "&";
5457 			else
5458 				outputted = true;
5459 
5460 			ret ~= std.uri.encodeComponent(e.name) ~ "=" ~ std.uri.encodeComponent(getValue(e.name));
5461 
5462 			namesDone[e.name] = true;
5463 		}
5464 
5465 		return ret;
5466 	}
5467 
5468 	/// Gets the actual elements with the given name
5469 	Element[] getField(string name) {
5470 		Element[] ret;
5471 		foreach(e; tree) {
5472 			if(e.name == name)
5473 				ret ~= e;
5474 		}
5475 		return ret;
5476 	}
5477 
5478 	/// Grabs the <label> with the given for tag, if there is one.
5479 	Element getLabel(string forId) {
5480 		foreach(e; tree)
5481 			if(e.tagName == "label" && e.getAttribute("for") == forId)
5482 				return e;
5483 		return null;
5484 	}
5485 
5486 	/// Adds a new INPUT field to the end of the form with the given attributes.
5487 	Element addInput(string name, string value, string type = "hidden") {
5488 		auto e = new Element(parentDocument, "input", null, true);
5489 		e.name = name;
5490 		e.value = value;
5491 		e.type = type;
5492 
5493 		appendChild(e);
5494 
5495 		return e;
5496 	}
5497 
5498 	/// Removes the given field from the form. It finds the element and knocks it right out.
5499 	void removeField(string name) {
5500 		foreach(e; getField(name))
5501 			e.parentNode.removeChild(e);
5502 	}
5503 
5504 	/+
5505 	/// Returns all form members.
5506 	@property Element[] elements() {
5507 
5508 	}
5509 
5510 	///.
5511 	string opDispatch(string name)(string v = null)
5512 		// filter things that should actually be attributes on the form
5513 		if( name != "method" && name != "action" && name != "enctype"
5514 		 && name != "style"  && name != "name" && name != "id" && name != "class")
5515 	{
5516 
5517 	}
5518 	+/
5519 /+
5520 	void submit() {
5521 		// take its elements and submit them through http
5522 	}
5523 +/
5524 }
5525 
5526 import std.conv;
5527 
5528 /++
5529 	Represents a HTML table. Has some convenience methods for working with tabular data.
5530 +/
5531 /// Group: implementations
5532 class Table : Element {
5533 
5534 	/// You can make this yourself but you'd generally get one of these object out of a html parse or [Element.make] call.
5535 	this(Document _parentDocument) {
5536 		super(_parentDocument);
5537 		tagName = "table";
5538 	}
5539 
5540 	/++
5541 		Creates an element with the given type and content. The argument can be an Element, Html, or other data which is converted to text with `to!string`
5542 
5543 		The element is $(I not) appended to the table.
5544 	+/
5545 	Element th(T)(T t) {
5546 		Element e;
5547 		if(parentDocument !is null)
5548 			e = parentDocument.createElement("th");
5549 		else
5550 			e = Element.make("th");
5551 		static if(is(T == Html))
5552 			e.innerHTML = t;
5553 		else static if(is(T : Element))
5554 			e.appendChild(t);
5555 		else
5556 			e.innerText = to!string(t);
5557 		return e;
5558 	}
5559 
5560 	/// ditto
5561 	Element td(T)(T t) {
5562 		Element e;
5563 		if(parentDocument !is null)
5564 			e = parentDocument.createElement("td");
5565 		else
5566 			e = Element.make("td");
5567 		static if(is(T == Html))
5568 			e.innerHTML = t;
5569 		else static if(is(T : Element))
5570 			e.appendChild(t);
5571 		else
5572 			e.innerText = to!string(t);
5573 		return e;
5574 	}
5575 
5576 	/++
5577 		Passes each argument to the [th] method for `appendHeaderRow` or [td] method for the others, appends them all to the `<tbody>` element for `appendRow`, `<thead>` element for `appendHeaderRow`, or a `<tfoot>` element for `appendFooterRow`, and ensures it is appended it to the table.
5578 	+/
5579 	Element appendHeaderRow(T...)(T t) {
5580 		return appendRowInternal("th", "thead", t);
5581 	}
5582 
5583 	/// ditto
5584 	Element appendFooterRow(T...)(T t) {
5585 		return appendRowInternal("td", "tfoot", t);
5586 	}
5587 
5588 	/// ditto
5589 	Element appendRow(T...)(T t) {
5590 		return appendRowInternal("td", "tbody", t);
5591 	}
5592 
5593 	/++
5594 		Takes each argument as a class name and calls [Element.addClass] for each element in the column associated with that index.
5595 
5596 		Please note this does not use the html `<col>` element.
5597 	+/
5598 	void addColumnClasses(string[] classes...) {
5599 		auto grid = getGrid();
5600 		foreach(row; grid)
5601 		foreach(i, cl; classes) {
5602 			if(cl.length)
5603 			if(i < row.length)
5604 				row[i].addClass(cl);
5605 		}
5606 	}
5607 
5608 	private Element appendRowInternal(T...)(string innerType, string findType, T t) {
5609 		Element row = Element.make("tr");
5610 
5611 		foreach(e; t) {
5612 			static if(is(typeof(e) : Element)) {
5613 				if(e.tagName == "td" || e.tagName == "th")
5614 					row.appendChild(e);
5615 				else {
5616 					Element a = Element.make(innerType);
5617 
5618 					a.appendChild(e);
5619 
5620 					row.appendChild(a);
5621 				}
5622 			} else static if(is(typeof(e) == Html)) {
5623 				Element a = Element.make(innerType);
5624 				a.innerHTML = e.source;
5625 				row.appendChild(a);
5626 			} else static if(is(typeof(e) == Element[])) {
5627 				Element a = Element.make(innerType);
5628 				foreach(ele; e)
5629 					a.appendChild(ele);
5630 				row.appendChild(a);
5631 			} else static if(is(typeof(e) == string[])) {
5632 				foreach(ele; e) {
5633 					Element a = Element.make(innerType);
5634 					a.innerText = to!string(ele);
5635 					row.appendChild(a);
5636 				}
5637 			} else {
5638 				Element a = Element.make(innerType);
5639 				a.innerText = to!string(e);
5640 				row.appendChild(a);
5641 			}
5642 		}
5643 
5644 		foreach(e; children) {
5645 			if(e.tagName == findType) {
5646 				e.appendChild(row);
5647 				return row;
5648 			}
5649 		}
5650 
5651 		// the type was not found if we are here... let's add it so it is well-formed
5652 		auto lol = this.addChild(findType);
5653 		lol.appendChild(row);
5654 
5655 		return row;
5656 	}
5657 
5658 	/// Returns the `<caption>` element of the table, creating one if it isn't there.
5659 	Element captionElement() {
5660 		Element cap;
5661 		foreach(c; children) {
5662 			if(c.tagName == "caption") {
5663 				cap = c;
5664 				break;
5665 			}
5666 		}
5667 
5668 		if(cap is null) {
5669 			cap = Element.make("caption");
5670 			appendChild(cap);
5671 		}
5672 
5673 		return cap;
5674 	}
5675 
5676 	/// Returns or sets the text inside the `<caption>` element, creating that element if it isnt' there.
5677 	@property string caption() {
5678 		return captionElement().innerText;
5679 	}
5680 
5681 	/// ditto
5682 	@property void caption(string text) {
5683 		captionElement().innerText = text;
5684 	}
5685 
5686 	/// Gets the logical layout of the table as a rectangular grid of
5687 	/// cells. It considers rowspan and colspan. A cell with a large
5688 	/// span is represented in the grid by being referenced several times.
5689 	/// The tablePortition parameter can get just a <thead>, <tbody>, or
5690 	/// <tfoot> portion if you pass one.
5691 	///
5692 	/// Note: the rectangular grid might include null cells.
5693 	///
5694 	/// This is kinda expensive so you should call once when you want the grid,
5695 	/// then do lookups on the returned array.
5696 	TableCell[][] getGrid(Element tablePortition = null)
5697 		in {
5698 			if(tablePortition is null)
5699 				assert(tablePortition is null);
5700 			else {
5701 				assert(tablePortition !is null);
5702 				assert(tablePortition.parentNode is this);
5703 				assert(
5704 					tablePortition.tagName == "tbody"
5705 					||
5706 					tablePortition.tagName == "tfoot"
5707 					||
5708 					tablePortition.tagName == "thead"
5709 				);
5710 			}
5711 		}
5712 	do {
5713 		if(tablePortition is null)
5714 			tablePortition = this;
5715 
5716 		TableCell[][] ret;
5717 
5718 		// FIXME: will also return rows of sub tables!
5719 		auto rows = tablePortition.getElementsByTagName("tr");
5720 		ret.length = rows.length;
5721 
5722 		int maxLength = 0;
5723 
5724 		int insertCell(int row, int position, TableCell cell) {
5725 			if(row >= ret.length)
5726 				return position; // not supposed to happen - a rowspan is prolly too big.
5727 
5728 			if(position == -1) {
5729 				position++;
5730 				foreach(item; ret[row]) {
5731 					if(item is null)
5732 						break;
5733 					position++;
5734 				}
5735 			}
5736 
5737 			if(position < ret[row].length)
5738 				ret[row][position] = cell;
5739 			else
5740 				foreach(i; ret[row].length .. position + 1) {
5741 					if(i == position)
5742 						ret[row] ~= cell;
5743 					else
5744 						ret[row] ~= null;
5745 				}
5746 			return position;
5747 		}
5748 
5749 		foreach(i, rowElement; rows) {
5750 			auto row = cast(TableRow) rowElement;
5751 			assert(row !is null);
5752 			assert(i < ret.length);
5753 
5754 			int position = 0;
5755 			foreach(cellElement; rowElement.childNodes) {
5756 				auto cell = cast(TableCell) cellElement;
5757 				if(cell is null)
5758 					continue;
5759 
5760 				// FIXME: colspan == 0 or rowspan == 0
5761 				// is supposed to mean fill in the rest of
5762 				// the table, not skip it
5763 				foreach(int j; 0 .. cell.colspan) {
5764 					foreach(int k; 0 .. cell.rowspan)
5765 						// if the first row, always append.
5766 						insertCell(k + cast(int) i, k == 0 ? -1 : position, cell);
5767 					position++;
5768 				}
5769 			}
5770 
5771 			if(ret[i].length > maxLength)
5772 				maxLength = cast(int) ret[i].length;
5773 		}
5774 
5775 		// want to ensure it's rectangular
5776 		foreach(ref r; ret) {
5777 			foreach(i; r.length .. maxLength)
5778 				r ~= null;
5779 		}
5780 
5781 		return ret;
5782 	}
5783 }
5784 
5785 /// Represents a table row element - a <tr>
5786 /// Group: implementations
5787 class TableRow : Element {
5788 	///.
5789 	this(Document _parentDocument) {
5790 		super(_parentDocument);
5791 		tagName = "tr";
5792 	}
5793 
5794 	// FIXME: the standard says there should be a lot more in here,
5795 	// but meh, I never use it and it's a pain to implement.
5796 }
5797 
5798 /// Represents anything that can be a table cell - <td> or <th> html.
5799 /// Group: implementations
5800 class TableCell : Element {
5801 	///.
5802 	this(Document _parentDocument, string _tagName) {
5803 		super(_parentDocument, _tagName);
5804 	}
5805 
5806 	/// Gets and sets the row/colspan attributes as integers
5807 	@property int rowspan() const {
5808 		int ret = 1;
5809 		auto it = getAttribute("rowspan");
5810 		if(it.length)
5811 			ret = to!int(it);
5812 		return ret;
5813 	}
5814 
5815 	/// ditto
5816 	@property int colspan() const {
5817 		int ret = 1;
5818 		auto it = getAttribute("colspan");
5819 		if(it.length)
5820 			ret = to!int(it);
5821 		return ret;
5822 	}
5823 
5824 	/// ditto
5825 	@property int rowspan(int i) {
5826 		setAttribute("rowspan", to!string(i));
5827 		return i;
5828 	}
5829 
5830 	/// ditto
5831 	@property int colspan(int i) {
5832 		setAttribute("colspan", to!string(i));
5833 		return i;
5834 	}
5835 
5836 }
5837 
5838 
5839 /// This is thrown on parse errors.
5840 /// Group: implementations
5841 class MarkupException : Exception {
5842 
5843 	///.
5844 	this(string message, string file = __FILE__, size_t line = __LINE__) {
5845 		super(message, file, line);
5846 	}
5847 }
5848 
5849 /// This is used when you are using one of the require variants of navigation, and no matching element can be found in the tree.
5850 /// Group: implementations
5851 class ElementNotFoundException : Exception {
5852 
5853 	/// type == kind of element you were looking for and search == a selector describing the search.
5854 	this(string type, string search, Element searchContext, string file = __FILE__, size_t line = __LINE__) {
5855 		this.searchContext = searchContext;
5856 		super("Element of type '"~type~"' matching {"~search~"} not found.", file, line);
5857 	}
5858 
5859 	Element searchContext;
5860 }
5861 
5862 /// The html struct is used to differentiate between regular text nodes and html in certain functions
5863 ///
5864 /// Easiest way to construct it is like this: `auto html = Html("<p>hello</p>");`
5865 /// Group: core_functionality
5866 struct Html {
5867 	/// This string holds the actual html. Use it to retrieve the contents.
5868 	string source;
5869 }
5870 
5871 // for the observers
5872 enum DomMutationOperations {
5873 	setAttribute,
5874 	removeAttribute,
5875 	appendChild, // tagname, attributes[], innerHTML
5876 	insertBefore,
5877 	truncateChildren,
5878 	removeChild,
5879 	appendHtml,
5880 	replaceHtml,
5881 	appendText,
5882 	replaceText,
5883 	replaceTextOnly
5884 }
5885 
5886 // and for observers too
5887 struct DomMutationEvent {
5888 	DomMutationOperations operation;
5889 	Element target;
5890 	Element related; // what this means differs with the operation
5891 	Element related2;
5892 	string relatedString;
5893 	string relatedString2;
5894 }
5895 
5896 
5897 private immutable static string[] htmlSelfClosedElements = [
5898 	// html 4
5899 	"area","base","br","col","hr","img","input","link","meta","param",
5900 
5901 	// html 5
5902 	"embed","source","track","wbr"
5903 ];
5904 
5905 private immutable static string[] htmlInlineElements = [
5906 	"span", "strong", "em", "b", "i", "a"
5907 ];
5908 
5909 
5910 static import std.conv;
5911 
5912 /// helper function for decoding html entities
5913 int intFromHex(string hex) {
5914 	int place = 1;
5915 	int value = 0;
5916 	for(sizediff_t a = hex.length - 1; a >= 0; a--) {
5917 		int v;
5918 		char q = hex[a];
5919 		if( q >= '0' && q <= '9')
5920 			v = q - '0';
5921 		else if (q >= 'a' && q <= 'f')
5922 			v = q - 'a' + 10;
5923 		else if (q >= 'A' && q <= 'F')
5924 			v = q - 'A' + 10;
5925 		else throw new Exception("Illegal hex character: " ~ q);
5926 
5927 		value += v * place;
5928 
5929 		place *= 16;
5930 	}
5931 
5932 	return value;
5933 }
5934 
5935 
5936 // CSS selector handling
5937 
5938 // EXTENSIONS
5939 // dd - dt means get the dt directly before that dd (opposite of +)                  NOT IMPLEMENTED
5940 // dd -- dt means rewind siblings until you hit a dt, go as far as you need to       NOT IMPLEMENTED
5941 // dt < dl means get the parent of that dt iff it is a dl (usable for "get a dt that are direct children of dl")
5942 // dt << dl  means go as far up as needed to find a dl (you have an element and want its containers)      NOT IMPLEMENTED
5943 // :first  means to stop at the first hit, don't do more (so p + p == p ~ p:first
5944 
5945 
5946 
5947 // CSS4 draft currently says you can change the subject (the element actually returned) by putting a ! at the end of it.
5948 // That might be useful to implement, though I do have parent selectors too.
5949 
5950 		///.
5951 		static immutable string[] selectorTokens = [
5952 			// It is important that the 2 character possibilities go first here for accurate lexing
5953 		    "~=", "*=", "|=", "^=", "$=", "!=",
5954 		    "::", ">>",
5955 		    "<<", // my any-parent extension (reciprocal of whitespace)
5956 		    // " - ", // previous-sibling extension (whitespace required to disambiguate tag-names)
5957 		    ".", ">", "+", "*", ":", "[", "]", "=", "\"", "#", ",", " ", "~", "<", "(", ")"
5958 		]; // other is white space or a name.
5959 
5960 		///.
5961 		sizediff_t idToken(string str, sizediff_t position) {
5962 			sizediff_t tid = -1;
5963 			char c = str[position];
5964 			foreach(a, token; selectorTokens)
5965 
5966 				if(c == token[0]) {
5967 					if(token.length > 1) {
5968 						if(position + 1 >= str.length   ||   str[position+1] != token[1])
5969 							continue; // not this token
5970 					}
5971 					tid = a;
5972 					break;
5973 				}
5974 			return tid;
5975 		}
5976 
5977 	/// Parts of the CSS selector implementation
5978 	// look, ma, no phobos!
5979 	// new lexer by ketmar
5980 	string[] lexSelector (string selstr) {
5981 
5982 		static sizediff_t idToken (string str, size_t stpos) {
5983 			char c = str[stpos];
5984 			foreach (sizediff_t tidx, immutable token; selectorTokens) {
5985 				if (c == token[0]) {
5986 					if (token.length > 1) {
5987 						assert(token.length == 2, token); // we don't have 3-char tokens yet
5988 						if (str.length-stpos < 2 || str[stpos+1] != token[1]) continue;
5989 					}
5990 					return tidx;
5991 				}
5992 			}
5993 			return -1;
5994 		}
5995 
5996 		// skip spaces and comments
5997 		static string removeLeadingBlanks (string str) {
5998 			size_t curpos = 0;
5999 			while (curpos < str.length) {
6000 				immutable char ch = str[curpos];
6001 				// this can overflow on 4GB strings on 32-bit; 'cmon, don't be silly, nobody cares!
6002 				if (ch == '/' && str.length-curpos > 1 && str[curpos+1] == '*') {
6003 					// comment
6004 					curpos += 2;
6005 					while (curpos < str.length) {
6006 						if (str[curpos] == '*' && str.length-curpos > 1 && str[curpos+1] == '/') {
6007 							curpos += 2;
6008 							break;
6009 						}
6010 						++curpos;
6011 					}
6012 				} else if (ch < 32) { // The < instead of <= is INTENTIONAL. See note from adr below.
6013 					++curpos;
6014 
6015 					// FROM ADR: This does NOT catch ' '! Spaces have semantic meaning in CSS! While
6016 					// "foo bar" is clear, and can only have one meaning, consider ".foo .bar".
6017 					// That is not the same as ".foo.bar". If the space is stripped, important
6018 					// information is lost, despite the tokens being separatable anyway.
6019 					//
6020 					// The parser really needs to be aware of the presence of a space.
6021 				} else {
6022 					break;
6023 				}
6024 			}
6025 			return str[curpos..$];
6026 		}
6027 
6028 		static bool isBlankAt() (string str, size_t pos) {
6029 			// we should consider unicode spaces too, but... unicode sux anyway.
6030 			return
6031 				(pos < str.length && // in string
6032 				 (str[pos] <= 32 || // space
6033 					(str.length-pos > 1 && str[pos] == '/' && str[pos+1] == '*'))); // comment
6034 		}
6035 
6036 		string[] tokens;
6037 		// lexx it!
6038 		while ((selstr = removeLeadingBlanks(selstr)).length > 0) {
6039 			if(selstr[0] == '\"' || selstr[0] == '\'') {
6040 				auto end = selstr[0];
6041 				auto pos = 1;
6042 				bool escaping;
6043 				while(pos < selstr.length && !escaping && selstr[pos] != end) {
6044 					if(escaping)
6045 						escaping = false;
6046 					else if(selstr[pos] == '\\')
6047 						escaping = true;
6048 					pos++;
6049 				}
6050 
6051 				// FIXME: do better unescaping
6052 				tokens ~= selstr[1 .. pos].replace(`\"`, `"`).replace(`\'`, `'`).replace(`\\`, `\`);
6053 				if(pos+1 >= selstr.length)
6054 					assert(0, selstr);
6055 				selstr = selstr[pos + 1.. $];
6056 				continue;
6057 			}
6058 
6059 
6060 			// no tokens starts with escape
6061 			immutable tid = idToken(selstr, 0);
6062 			if (tid >= 0) {
6063 				// special token
6064 				tokens ~= selectorTokens[tid]; // it's funnier this way
6065 				selstr = selstr[selectorTokens[tid].length..$];
6066 				continue;
6067 			}
6068 			// from start to space or special token
6069 			size_t escapePos = size_t.max;
6070 			size_t curpos = 0; // i can has chizburger^w escape at the start
6071 			while (curpos < selstr.length) {
6072 				if (selstr[curpos] == '\\') {
6073 					// this is escape, just skip it and next char
6074 					if (escapePos == size_t.max) escapePos = curpos;
6075 					curpos = (selstr.length-curpos >= 2 ? curpos+2 : selstr.length);
6076 				} else {
6077 					if (isBlankAt(selstr, curpos) || idToken(selstr, curpos) >= 0) break;
6078 					++curpos;
6079 				}
6080 			}
6081 			// identifier
6082 			if (escapePos != size_t.max) {
6083 				// i hate it when it happens
6084 				string id = selstr[0..escapePos];
6085 				while (escapePos < curpos) {
6086 					if (curpos-escapePos < 2) break;
6087 					id ~= selstr[escapePos+1]; // escaped char
6088 					escapePos += 2;
6089 					immutable stp = escapePos;
6090 					while (escapePos < curpos && selstr[escapePos] != '\\') ++escapePos;
6091 					if (escapePos > stp) id ~= selstr[stp..escapePos];
6092 				}
6093 				if (id.length > 0) tokens ~= id;
6094 			} else {
6095 				tokens ~= selstr[0..curpos];
6096 			}
6097 			selstr = selstr[curpos..$];
6098 		}
6099 		return tokens;
6100 	}
6101 	version(unittest_domd_lexer) unittest {
6102 		assert(lexSelector(r" test\=me  /*d*/") == [r"test=me"]);
6103 		assert(lexSelector(r"div/**/. id") == ["div", ".", "id"]);
6104 		assert(lexSelector(r" < <") == ["<", "<"]);
6105 		assert(lexSelector(r" <<") == ["<<"]);
6106 		assert(lexSelector(r" <</") == ["<<", "/"]);
6107 		assert(lexSelector(r" <</*") == ["<<"]);
6108 		assert(lexSelector(r" <\</*") == ["<", "<"]);
6109 		assert(lexSelector(r"heh\") == ["heh"]);
6110 		assert(lexSelector(r"alice \") == ["alice"]);
6111 		assert(lexSelector(r"alice,is#best") == ["alice", ",", "is", "#", "best"]);
6112 	}
6113 
6114 	/// ditto
6115 	struct SelectorPart {
6116 		string tagNameFilter; ///.
6117 		string[] attributesPresent; /// [attr]
6118 		string[2][] attributesEqual; /// [attr=value]
6119 		string[2][] attributesStartsWith; /// [attr^=value]
6120 		string[2][] attributesEndsWith; /// [attr$=value]
6121 		// split it on space, then match to these
6122 		string[2][] attributesIncludesSeparatedBySpaces; /// [attr~=value]
6123 		// split it on dash, then match to these
6124 		string[2][] attributesIncludesSeparatedByDashes; /// [attr|=value]
6125 		string[2][] attributesInclude; /// [attr*=value]
6126 		string[2][] attributesNotEqual; /// [attr!=value] -- extension by me
6127 
6128 		string[] hasSelectors; /// :has(this)
6129 		string[] notSelectors; /// :not(this)
6130 
6131 		string[] isSelectors; /// :is(this)
6132 		string[] whereSelectors; /// :where(this)
6133 
6134 		ParsedNth[] nthOfType; /// .
6135 		ParsedNth[] nthLastOfType; /// .
6136 		ParsedNth[] nthChild; /// .
6137 
6138 		bool firstChild; ///.
6139 		bool lastChild; ///.
6140 
6141 		bool firstOfType; /// .
6142 		bool lastOfType; /// .
6143 
6144 		bool emptyElement; ///.
6145 		bool whitespaceOnly; ///
6146 		bool oddChild; ///.
6147 		bool evenChild; ///.
6148 
6149 		bool scopeElement; /// the css :scope thing; matches just the `this` element. NOT IMPLEMENTED
6150 
6151 		bool rootElement; ///.
6152 
6153 		int separation = -1; /// -1 == only itself; the null selector, 0 == tree, 1 == childNodes, 2 == childAfter, 3 == youngerSibling, 4 == parentOf
6154 
6155 		bool isCleanSlateExceptSeparation() {
6156 			auto cp = this;
6157 			cp.separation = -1;
6158 			return cp is SelectorPart.init;
6159 		}
6160 
6161 		///.
6162 		string toString() {
6163 			string ret;
6164 			switch(separation) {
6165 				default: assert(0);
6166 				case -1: break;
6167 				case 0: ret ~= " "; break;
6168 				case 1: ret ~= " > "; break;
6169 				case 2: ret ~= " + "; break;
6170 				case 3: ret ~= " ~ "; break;
6171 				case 4: ret ~= " < "; break;
6172 			}
6173 			ret ~= tagNameFilter;
6174 			foreach(a; attributesPresent) ret ~= "[" ~ a ~ "]";
6175 			foreach(a; attributesEqual) ret ~= "[" ~ a[0] ~ "=\"" ~ a[1] ~ "\"]";
6176 			foreach(a; attributesEndsWith) ret ~= "[" ~ a[0] ~ "$=\"" ~ a[1] ~ "\"]";
6177 			foreach(a; attributesStartsWith) ret ~= "[" ~ a[0] ~ "^=\"" ~ a[1] ~ "\"]";
6178 			foreach(a; attributesNotEqual) ret ~= "[" ~ a[0] ~ "!=\"" ~ a[1] ~ "\"]";
6179 			foreach(a; attributesInclude) ret ~= "[" ~ a[0] ~ "*=\"" ~ a[1] ~ "\"]";
6180 			foreach(a; attributesIncludesSeparatedByDashes) ret ~= "[" ~ a[0] ~ "|=\"" ~ a[1] ~ "\"]";
6181 			foreach(a; attributesIncludesSeparatedBySpaces) ret ~= "[" ~ a[0] ~ "~=\"" ~ a[1] ~ "\"]";
6182 
6183 			foreach(a; notSelectors) ret ~= ":not(" ~ a ~ ")";
6184 			foreach(a; hasSelectors) ret ~= ":has(" ~ a ~ ")";
6185 
6186 			foreach(a; isSelectors) ret ~= ":is(" ~ a ~ ")";
6187 			foreach(a; whereSelectors) ret ~= ":where(" ~ a ~ ")";
6188 
6189 			foreach(a; nthChild) ret ~= ":nth-child(" ~ a.toString ~ ")";
6190 			foreach(a; nthOfType) ret ~= ":nth-of-type(" ~ a.toString ~ ")";
6191 			foreach(a; nthLastOfType) ret ~= ":nth-last-of-type(" ~ a.toString ~ ")";
6192 
6193 			if(firstChild) ret ~= ":first-child";
6194 			if(lastChild) ret ~= ":last-child";
6195 			if(firstOfType) ret ~= ":first-of-type";
6196 			if(lastOfType) ret ~= ":last-of-type";
6197 			if(emptyElement) ret ~= ":empty";
6198 			if(whitespaceOnly) ret ~= ":whitespace-only";
6199 			if(oddChild) ret ~= ":odd-child";
6200 			if(evenChild) ret ~= ":even-child";
6201 			if(rootElement) ret ~= ":root";
6202 			if(scopeElement) ret ~= ":scope";
6203 
6204 			return ret;
6205 		}
6206 
6207 		// USEFUL
6208 		/// Returns true if the given element matches this part
6209 		bool matchElement(Element e, Element scopeElementNow = null) {
6210 			// FIXME: this can be called a lot of times, and really add up in times according to the profiler.
6211 			// Each individual call is reasonably fast already, but it adds up.
6212 			if(e is null) return false;
6213 			if(e.nodeType != 1) return false;
6214 
6215 			if(tagNameFilter != "" && tagNameFilter != "*")
6216 				if(e.tagName != tagNameFilter)
6217 					return false;
6218 			if(firstChild) {
6219 				if(e.parentNode is null)
6220 					return false;
6221 				if(e.parentNode.childElements[0] !is e)
6222 					return false;
6223 			}
6224 			if(lastChild) {
6225 				if(e.parentNode is null)
6226 					return false;
6227 				auto ce = e.parentNode.childElements;
6228 				if(ce[$-1] !is e)
6229 					return false;
6230 			}
6231 			if(firstOfType) {
6232 				if(e.parentNode is null)
6233 					return false;
6234 				auto ce = e.parentNode.childElements;
6235 				foreach(c; ce) {
6236 					if(c.tagName == e.tagName) {
6237 						if(c is e)
6238 							return true;
6239 						else
6240 							return false;
6241 					}
6242 				}
6243 			}
6244 			if(lastOfType) {
6245 				if(e.parentNode is null)
6246 					return false;
6247 				auto ce = e.parentNode.childElements;
6248 				foreach_reverse(c; ce) {
6249 					if(c.tagName == e.tagName) {
6250 						if(c is e)
6251 							return true;
6252 						else
6253 							return false;
6254 					}
6255 				}
6256 			}
6257 			if(scopeElement) {
6258 				if(e !is scopeElementNow)
6259 					return false;
6260 			}
6261 			if(emptyElement) {
6262 				if(e.isEmpty())
6263 					return false;
6264 			}
6265 			if(whitespaceOnly) {
6266 				if(e.innerText.strip.length)
6267 					return false;
6268 			}
6269 			if(rootElement) {
6270 				if(e.parentNode !is null)
6271 					return false;
6272 			}
6273 			if(oddChild || evenChild) {
6274 				if(e.parentNode is null)
6275 					return false;
6276 				foreach(i, child; e.parentNode.childElements) {
6277 					if(child is e) {
6278 						if(oddChild && !(i&1))
6279 							return false;
6280 						if(evenChild && (i&1))
6281 							return false;
6282 						break;
6283 					}
6284 				}
6285 			}
6286 
6287 			bool matchWithSeparator(string attr, string value, string separator) {
6288 				foreach(s; attr.split(separator))
6289 					if(s == value)
6290 						return true;
6291 				return false;
6292 			}
6293 
6294 			foreach(a; attributesPresent)
6295 				if(a !in e.attributes)
6296 					return false;
6297 			foreach(a; attributesEqual)
6298 				if(a[0] !in e.attributes || e.attributes[a[0]] != a[1])
6299 					return false;
6300 			foreach(a; attributesNotEqual)
6301 				// FIXME: maybe it should say null counts... this just bit me.
6302 				// I did [attr][attr!=value] to work around.
6303 				//
6304 				// if it's null, it's not equal, right?
6305 				//if(a[0] !in e.attributes || e.attributes[a[0]] == a[1])
6306 				if(e.getAttribute(a[0]) == a[1])
6307 					return false;
6308 			foreach(a; attributesInclude)
6309 				if(a[0] !in e.attributes || (e.attributes[a[0]].indexOf(a[1]) == -1))
6310 					return false;
6311 			foreach(a; attributesStartsWith)
6312 				if(a[0] !in e.attributes || !e.attributes[a[0]].startsWith(a[1]))
6313 					return false;
6314 			foreach(a; attributesEndsWith)
6315 				if(a[0] !in e.attributes || !e.attributes[a[0]].endsWith(a[1]))
6316 					return false;
6317 			foreach(a; attributesIncludesSeparatedBySpaces)
6318 				if(a[0] !in e.attributes || !matchWithSeparator(e.attributes[a[0]], a[1], " "))
6319 					return false;
6320 			foreach(a; attributesIncludesSeparatedByDashes)
6321 				if(a[0] !in e.attributes || !matchWithSeparator(e.attributes[a[0]], a[1], "-"))
6322 					return false;
6323 			foreach(a; hasSelectors) {
6324 				if(e.querySelector(a) is null)
6325 					return false;
6326 			}
6327 			foreach(a; notSelectors) {
6328 				auto sel = Selector(a);
6329 				if(sel.matchesElement(e))
6330 					return false;
6331 			}
6332 			foreach(a; isSelectors) {
6333 				auto sel = Selector(a);
6334 				if(!sel.matchesElement(e))
6335 					return false;
6336 			}
6337 			foreach(a; whereSelectors) {
6338 				auto sel = Selector(a);
6339 				if(!sel.matchesElement(e))
6340 					return false;
6341 			}
6342 
6343 			foreach(a; nthChild) {
6344 				if(e.parentNode is null)
6345 					return false;
6346 
6347 				auto among = e.parentNode.childElements;
6348 
6349 				if(!a.solvesFor(among, e))
6350 					return false;
6351 			}
6352 			foreach(a; nthOfType) {
6353 				if(e.parentNode is null)
6354 					return false;
6355 
6356 				auto among = e.parentNode.childElements(e.tagName);
6357 
6358 				if(!a.solvesFor(among, e))
6359 					return false;
6360 			}
6361 			foreach(a; nthLastOfType) {
6362 				if(e.parentNode is null)
6363 					return false;
6364 
6365 				auto among = retro(e.parentNode.childElements(e.tagName));
6366 
6367 				if(!a.solvesFor(among, e))
6368 					return false;
6369 			}
6370 
6371 			return true;
6372 		}
6373 	}
6374 
6375 	struct ParsedNth {
6376 		int multiplier;
6377 		int adder;
6378 
6379 		string of;
6380 
6381 		this(string text) {
6382 			auto original = text;
6383 			consumeWhitespace(text);
6384 			if(text.startsWith("odd")) {
6385 				multiplier = 2;
6386 				adder = 1;
6387 
6388 				text = text[3 .. $];
6389 			} else if(text.startsWith("even")) {
6390 				multiplier = 2;
6391 				adder = 1;
6392 
6393 				text = text[4 .. $];
6394 			} else {
6395 				int n = (text.length && text[0] == 'n') ? 1 : parseNumber(text);
6396 				consumeWhitespace(text);
6397 				if(text.length && text[0] == 'n') {
6398 					multiplier = n;
6399 					text = text[1 .. $];
6400 					consumeWhitespace(text);
6401 					if(text.length) {
6402 						if(text[0] == '+') {
6403 							text = text[1 .. $];
6404 							adder = parseNumber(text);
6405 						} else if(text[0] == '-') {
6406 							text = text[1 .. $];
6407 							adder = -parseNumber(text);
6408 						} else if(text[0] == 'o') {
6409 							// continue, this is handled below
6410 						} else
6411 							throw new Exception("invalid css string at " ~ text ~ " in " ~ original);
6412 					}
6413 				} else {
6414 					adder = n;
6415 				}
6416 			}
6417 
6418 			consumeWhitespace(text);
6419 			if(text.startsWith("of")) {
6420 				text = text[2 .. $];
6421 				consumeWhitespace(text);
6422 				of = text[0 .. $];
6423 			}
6424 		}
6425 
6426 		string toString() {
6427 			return format("%dn%s%d%s%s", multiplier, adder >= 0 ? "+" : "", adder, of.length ? " of " : "", of);
6428 		}
6429 
6430 		bool solvesFor(R)(R elements, Element e) {
6431 			int idx = 1;
6432 			bool found = false;
6433 			foreach(ele; elements) {
6434 				if(of.length) {
6435 					auto sel = Selector(of);
6436 					if(!sel.matchesElement(ele))
6437 						continue;
6438 				}
6439 				if(ele is e) {
6440 					found = true;
6441 					break;
6442 				}
6443 				idx++;
6444 			}
6445 			if(!found) return false;
6446 
6447 			// multiplier* n + adder = idx
6448 			// if there is a solution for integral n, it matches
6449 
6450 			idx -= adder;
6451 			if(multiplier) {
6452 				if(idx % multiplier == 0)
6453 					return true;
6454 			} else {
6455 				return idx == 0;
6456 			}
6457 			return false;
6458 		}
6459 
6460 		private void consumeWhitespace(ref string text) {
6461 			while(text.length && text[0] == ' ')
6462 				text = text[1 .. $];
6463 		}
6464 
6465 		private int parseNumber(ref string text) {
6466 			consumeWhitespace(text);
6467 			if(text.length == 0) return 0;
6468 			bool negative = text[0] == '-';
6469 			if(text[0] == '+')
6470 				text = text[1 .. $];
6471 			if(negative) text = text[1 .. $];
6472 			int i = 0;
6473 			while(i < text.length && (text[i] >= '0' && text[i] <= '9'))
6474 				i++;
6475 			if(i == 0)
6476 				return 0;
6477 			int cool = to!int(text[0 .. i]);
6478 			text = text[i .. $];
6479 			return negative ? -cool : cool;
6480 		}
6481 	}
6482 
6483 	// USEFUL
6484 	/// ditto
6485 	Element[] getElementsBySelectorParts(Element start, SelectorPart[] parts, Element scopeElementNow = null) {
6486 		Element[] ret;
6487 		if(!parts.length) {
6488 			return [start]; // the null selector only matches the start point; it
6489 				// is what terminates the recursion
6490 		}
6491 
6492 		auto part = parts[0];
6493 		//writeln("checking ", part, " against ", start, " with ", part.separation);
6494 		switch(part.separation) {
6495 			default: assert(0);
6496 			case -1:
6497 			case 0: // tree
6498 				foreach(e; start.tree) {
6499 					if(part.separation == 0 && start is e)
6500 						continue; // space doesn't match itself!
6501 					if(part.matchElement(e, scopeElementNow)) {
6502 						ret ~= getElementsBySelectorParts(e, parts[1..$], scopeElementNow);
6503 					}
6504 				}
6505 			break;
6506 			case 1: // children
6507 				foreach(e; start.childNodes) {
6508 					if(part.matchElement(e, scopeElementNow)) {
6509 						ret ~= getElementsBySelectorParts(e, parts[1..$], scopeElementNow);
6510 					}
6511 				}
6512 			break;
6513 			case 2: // next-sibling
6514 				auto e = start.nextSibling("*");
6515 				if(part.matchElement(e, scopeElementNow))
6516 					ret ~= getElementsBySelectorParts(e, parts[1..$], scopeElementNow);
6517 			break;
6518 			case 3: // younger sibling
6519 				auto tmp = start.parentNode;
6520 				if(tmp !is null) {
6521 					sizediff_t pos = -1;
6522 					auto children = tmp.childElements;
6523 					foreach(i, child; children) {
6524 						if(child is start) {
6525 							pos = i;
6526 							break;
6527 						}
6528 					}
6529 					assert(pos != -1);
6530 					foreach(e; children[pos+1..$]) {
6531 						if(part.matchElement(e, scopeElementNow))
6532 							ret ~= getElementsBySelectorParts(e, parts[1..$], scopeElementNow);
6533 					}
6534 				}
6535 			break;
6536 			case 4: // immediate parent node, an extension of mine to walk back up the tree
6537 				auto e = start.parentNode;
6538 				if(part.matchElement(e, scopeElementNow)) {
6539 					ret ~= getElementsBySelectorParts(e, parts[1..$], scopeElementNow);
6540 				}
6541 				/*
6542 					Example of usefulness:
6543 
6544 					Consider you have an HTML table. If you want to get all rows that have a th, you can do:
6545 
6546 					table th < tr
6547 
6548 					Get all th descendants of the table, then walk back up the tree to fetch their parent tr nodes
6549 				*/
6550 			break;
6551 			case 5: // any parent note, another extension of mine to go up the tree (backward of the whitespace operator)
6552 				/*
6553 					Like with the < operator, this is best used to find some parent of a particular known element.
6554 
6555 					Say you have an anchor inside a
6556 				*/
6557 		}
6558 
6559 		return ret;
6560 	}
6561 
6562 	/++
6563 		Represents a parsed CSS selector. You never have to use this directly, but you can if you know it is going to be reused a lot to avoid a bit of repeat parsing.
6564 
6565 		See_Also:
6566 			$(LIST
6567 				* [Element.querySelector]
6568 				* [Element.querySelectorAll]
6569 				* [Element.matches]
6570 				* [Element.closest]
6571 				* [Document.querySelector]
6572 				* [Document.querySelectorAll]
6573 			)
6574 	+/
6575 	/// Group: core_functionality
6576 	struct Selector {
6577 		SelectorComponent[] components;
6578 		string original;
6579 		/++
6580 			Parses the selector string and constructs the usable structure.
6581 		+/
6582 		this(string cssSelector) {
6583 			components = parseSelectorString(cssSelector);
6584 			original = cssSelector;
6585 		}
6586 
6587 		/++
6588 			Returns true if the given element matches this selector,
6589 			considered relative to an arbitrary element.
6590 
6591 			You can do a form of lazy [Element.querySelectorAll|querySelectorAll] by using this
6592 			with [std.algorithm.iteration.filter]:
6593 
6594 			---
6595 			Selector sel = Selector("foo > bar");
6596 			auto lazySelectorRange = element.tree.filter!(e => sel.matchElement(e))(document.root);
6597 			---
6598 		+/
6599 		bool matchesElement(Element e, Element relativeTo = null) {
6600 			foreach(component; components)
6601 				if(component.matchElement(e, relativeTo))
6602 					return true;
6603 
6604 			return false;
6605 		}
6606 
6607 		/++
6608 			Reciprocal of [Element.querySelectorAll]
6609 		+/
6610 		Element[] getMatchingElements(Element start, Element relativeTo = null) {
6611 			Element[] ret;
6612 			foreach(component; components)
6613 				ret ~= getElementsBySelectorParts(start, component.parts, relativeTo);
6614 			return removeDuplicates(ret);
6615 		}
6616 
6617 		/++
6618 			Like [getMatchingElements], but returns a lazy range. Be careful
6619 			about mutating the dom as you iterate through this.
6620 		+/
6621 		auto getMatchingElementsLazy(Element start, Element relativeTo = null) {
6622 			import std.algorithm.iteration;
6623 			return start.tree.filter!(a => this.matchesElement(a, relativeTo));
6624 		}
6625 
6626 
6627 		/// Returns the string this was built from
6628 		string toString() {
6629 			return original;
6630 		}
6631 
6632 		/++
6633 			Returns a string from the parsed result
6634 
6635 
6636 			(may not match the original, this is mostly for debugging right now but in the future might be useful for pretty-printing)
6637 		+/
6638 		string parsedToString() {
6639 			string ret;
6640 
6641 			foreach(idx, component; components) {
6642 				if(idx) ret ~= ", ";
6643 				ret ~= component.toString();
6644 			}
6645 
6646 			return ret;
6647 		}
6648 	}
6649 
6650 	///.
6651 	struct SelectorComponent {
6652 		///.
6653 		SelectorPart[] parts;
6654 
6655 		///.
6656 		string toString() {
6657 			string ret;
6658 			foreach(part; parts)
6659 				ret ~= part.toString();
6660 			return ret;
6661 		}
6662 
6663 		// USEFUL
6664 		///.
6665 		Element[] getElements(Element start, Element relativeTo = null) {
6666 			return removeDuplicates(getElementsBySelectorParts(start, parts, relativeTo));
6667 		}
6668 
6669 		// USEFUL (but not implemented)
6670 		/// If relativeTo == null, it assumes the root of the parent document.
6671 		bool matchElement(Element e, Element relativeTo = null) {
6672 			if(e is null) return false;
6673 			Element where = e;
6674 			int lastSeparation = -1;
6675 
6676 			auto lparts = parts;
6677 
6678 			if(parts.length && parts[0].separation > 0) {
6679 				throw new Exception("invalid selector");
6680 			/+
6681 				// if it starts with a non-trivial separator, inject
6682 				// a "*" matcher to act as a root. for cases like document.querySelector("> body")
6683 				// which implies html
6684 
6685 				// however, if it is a child-matching selector and there are no children,
6686 				// bail out early as it obviously cannot match.
6687 				bool hasNonTextChildren = false;
6688 				foreach(c; e.children)
6689 					if(c.nodeType != 3) {
6690 						hasNonTextChildren = true;
6691 						break;
6692 					}
6693 				if(!hasNonTextChildren)
6694 					return false;
6695 
6696 				// there is probably a MUCH better way to do this.
6697 				auto dummy = SelectorPart.init;
6698 				dummy.tagNameFilter = "*";
6699 				dummy.separation = 0;
6700 				lparts = dummy ~ lparts;
6701 			+/
6702 			}
6703 
6704 			foreach(part; retro(lparts)) {
6705 
6706 				 // writeln("matching ", where, " with ", part, " via ", lastSeparation);
6707 				 // writeln(parts);
6708 
6709 				if(lastSeparation == -1) {
6710 					if(!part.matchElement(where, relativeTo))
6711 						return false;
6712 				} else if(lastSeparation == 0) { // generic parent
6713 					// need to go up the whole chain
6714 					where = where.parentNode;
6715 
6716 					while(where !is null) {
6717 						if(part.matchElement(where, relativeTo))
6718 							break;
6719 
6720 						if(where is relativeTo)
6721 							return false;
6722 
6723 						where = where.parentNode;
6724 					}
6725 
6726 					if(where is null)
6727 						return false;
6728 				} else if(lastSeparation == 1) { // the > operator
6729 					where = where.parentNode;
6730 
6731 					if(!part.matchElement(where, relativeTo))
6732 						return false;
6733 				} else if(lastSeparation == 2) { // the + operator
6734 				//writeln("WHERE", where, " ", part);
6735 					where = where.previousSibling("*");
6736 
6737 					if(!part.matchElement(where, relativeTo))
6738 						return false;
6739 				} else if(lastSeparation == 3) { // the ~ operator
6740 					where = where.previousSibling("*");
6741 					while(where !is null) {
6742 						if(part.matchElement(where, relativeTo))
6743 							break;
6744 
6745 						if(where is relativeTo)
6746 							return false;
6747 
6748 						where = where.previousSibling("*");
6749 					}
6750 
6751 					if(where is null)
6752 						return false;
6753 				} else if(lastSeparation == 4) { // my bad idea extension < operator, don't use this anymore
6754 					// FIXME
6755 				}
6756 
6757 				lastSeparation = part.separation;
6758 
6759 				/*
6760 					/+
6761 					I commented this to magically make unittest pass and I think the reason it works
6762 					when commented is that I inject a :scope iff there's a selector at top level now
6763 					and if not, it follows the (frankly stupid) w3c standard behavior at arbitrary id
6764 					asduiwh . but me injecting the :scope also acts as a terminating condition.
6765 
6766 					tbh this prolly needs like a trillion more tests.
6767 					+/
6768 				if(where is relativeTo)
6769 					return false; // at end of line, if we aren't done by now, the match fails
6770 				*/
6771 			}
6772 			return true; // if we got here, it is a success
6773 		}
6774 
6775 		// the string should NOT have commas. Use parseSelectorString for that instead
6776 		///.
6777 		static SelectorComponent fromString(string selector) {
6778 			return parseSelector(lexSelector(selector));
6779 		}
6780 	}
6781 
6782 	///.
6783 	SelectorComponent[] parseSelectorString(string selector, bool caseSensitiveTags = true) {
6784 		SelectorComponent[] ret;
6785 		auto tokens = lexSelector(selector); // this will parse commas too
6786 		// and now do comma-separated slices (i haz phobosophobia!)
6787 		int parensCount = 0;
6788 		while (tokens.length > 0) {
6789 			size_t end = 0;
6790 			while (end < tokens.length && (parensCount > 0 || tokens[end] != ",")) {
6791 				if(tokens[end] == "(") parensCount++;
6792 				if(tokens[end] == ")") parensCount--;
6793 				++end;
6794 			}
6795 			if (end > 0) ret ~= parseSelector(tokens[0..end], caseSensitiveTags);
6796 			if (tokens.length-end < 2) break;
6797 			tokens = tokens[end+1..$];
6798 		}
6799 		return ret;
6800 	}
6801 
6802 	///.
6803 	SelectorComponent parseSelector(string[] tokens, bool caseSensitiveTags = true) {
6804 		SelectorComponent s;
6805 
6806 		SelectorPart current;
6807 		void commit() {
6808 			// might as well skip null items
6809 			if(!current.isCleanSlateExceptSeparation()) {
6810 				s.parts ~= current;
6811 				current = current.init; // start right over
6812 			}
6813 		}
6814 		enum State {
6815 			Starting,
6816 			ReadingClass,
6817 			ReadingId,
6818 			ReadingAttributeSelector,
6819 			ReadingAttributeComparison,
6820 			ExpectingAttributeCloser,
6821 			ReadingPseudoClass,
6822 			ReadingAttributeValue,
6823 
6824 			SkippingFunctionalSelector,
6825 		}
6826 		State state = State.Starting;
6827 		string attributeName, attributeValue, attributeComparison;
6828 		int parensCount;
6829 		foreach(idx, token; tokens) {
6830 			string readFunctionalSelector() {
6831 				string s;
6832 				if(tokens[idx + 1] != "(")
6833 					throw new Exception("parse error");
6834 				int pc = 1;
6835 				foreach(t; tokens[idx + 2 .. $]) {
6836 					if(t == "(")
6837 						pc++;
6838 					if(t == ")")
6839 						pc--;
6840 					if(pc == 0)
6841 						break;
6842 					s ~= t;
6843 				}
6844 
6845 				return s;
6846 			}
6847 
6848 			sizediff_t tid = -1;
6849 			foreach(i, item; selectorTokens)
6850 				if(token == item) {
6851 					tid = i;
6852 					break;
6853 				}
6854 			final switch(state) {
6855 				case State.Starting: // fresh, might be reading an operator or a tagname
6856 					if(tid == -1) {
6857 						if(!caseSensitiveTags)
6858 							token = token.toLower();
6859 
6860 						if(current.isCleanSlateExceptSeparation()) {
6861 							current.tagNameFilter = token;
6862 							// default thing, see comment under "*" below
6863 							if(current.separation == -1) current.separation = 0;
6864 						} else {
6865 							// if it was already set, we must see two thingies
6866 							// separated by whitespace...
6867 							commit();
6868 							current.separation = 0; // tree
6869 							current.tagNameFilter = token;
6870 						}
6871 					} else {
6872 						// Selector operators
6873 						switch(token) {
6874 							case "*":
6875 								current.tagNameFilter = "*";
6876 								// the idea here is if we haven't actually set a separation
6877 								// yet (e.g. the > operator), it should assume the generic
6878 								// whitespace (descendant) mode to avoid matching self with -1
6879 								if(current.separation == -1) current.separation = 0;
6880 							break;
6881 							case " ":
6882 								// If some other separation has already been set,
6883 								// this is irrelevant whitespace, so we should skip it.
6884 								// this happens in the case of "foo > bar" for example.
6885 								if(current.isCleanSlateExceptSeparation() && current.separation > 0)
6886 									continue;
6887 								commit();
6888 								current.separation = 0; // tree
6889 							break;
6890 							case ">>":
6891 								commit();
6892 								current.separation = 0; // alternate syntax for tree from html5 css
6893 							break;
6894 							case ">":
6895 								commit();
6896 								current.separation = 1; // child
6897 							break;
6898 							case "+":
6899 								commit();
6900 								current.separation = 2; // sibling directly after
6901 							break;
6902 							case "~":
6903 								commit();
6904 								current.separation = 3; // any sibling after
6905 							break;
6906 							case "<":
6907 								commit();
6908 								current.separation = 4; // immediate parent of
6909 							break;
6910 							case "[":
6911 								state = State.ReadingAttributeSelector;
6912 								if(current.separation == -1) current.separation = 0;
6913 							break;
6914 							case ".":
6915 								state = State.ReadingClass;
6916 								if(current.separation == -1) current.separation = 0;
6917 							break;
6918 							case "#":
6919 								state = State.ReadingId;
6920 								if(current.separation == -1) current.separation = 0;
6921 							break;
6922 							case ":":
6923 							case "::":
6924 								state = State.ReadingPseudoClass;
6925 								if(current.separation == -1) current.separation = 0;
6926 							break;
6927 
6928 							default:
6929 								assert(0, token);
6930 						}
6931 					}
6932 				break;
6933 				case State.ReadingClass:
6934 					current.attributesIncludesSeparatedBySpaces ~= ["class", token];
6935 					state = State.Starting;
6936 				break;
6937 				case State.ReadingId:
6938 					current.attributesEqual ~= ["id", token];
6939 					state = State.Starting;
6940 				break;
6941 				case State.ReadingPseudoClass:
6942 					switch(token) {
6943 						case "first-of-type":
6944 							current.firstOfType = true;
6945 						break;
6946 						case "last-of-type":
6947 							current.lastOfType = true;
6948 						break;
6949 						case "only-of-type":
6950 							current.firstOfType = true;
6951 							current.lastOfType = true;
6952 						break;
6953 						case "first-child":
6954 							current.firstChild = true;
6955 						break;
6956 						case "last-child":
6957 							current.lastChild = true;
6958 						break;
6959 						case "only-child":
6960 							current.firstChild = true;
6961 							current.lastChild = true;
6962 						break;
6963 						case "scope":
6964 							current.scopeElement = true;
6965 						break;
6966 						case "empty":
6967 							// one with no children
6968 							current.emptyElement = true;
6969 						break;
6970 						case "whitespace-only":
6971 							current.whitespaceOnly = true;
6972 						break;
6973 						case "link":
6974 							current.attributesPresent ~= "href";
6975 						break;
6976 						case "root":
6977 							current.rootElement = true;
6978 						break;
6979 						case "nth-child":
6980 							current.nthChild ~= ParsedNth(readFunctionalSelector());
6981 							state = State.SkippingFunctionalSelector;
6982 						continue;
6983 						case "nth-of-type":
6984 							current.nthOfType ~= ParsedNth(readFunctionalSelector());
6985 							state = State.SkippingFunctionalSelector;
6986 						continue;
6987 						case "nth-last-of-type":
6988 							current.nthLastOfType ~= ParsedNth(readFunctionalSelector());
6989 							state = State.SkippingFunctionalSelector;
6990 						continue;
6991 						case "is":
6992 							state = State.SkippingFunctionalSelector;
6993 							current.isSelectors ~= readFunctionalSelector();
6994 						continue; // now the rest of the parser skips past the parens we just handled
6995 						case "where":
6996 							state = State.SkippingFunctionalSelector;
6997 							current.whereSelectors ~= readFunctionalSelector();
6998 						continue; // now the rest of the parser skips past the parens we just handled
6999 						case "not":
7000 							state = State.SkippingFunctionalSelector;
7001 							current.notSelectors ~= readFunctionalSelector();
7002 						continue; // now the rest of the parser skips past the parens we just handled
7003 						case "has":
7004 							state = State.SkippingFunctionalSelector;
7005 							current.hasSelectors ~= readFunctionalSelector();
7006 						continue; // now the rest of the parser skips past the parens we just handled
7007 						// back to standards though not quite right lol
7008 						case "disabled":
7009 							current.attributesPresent ~= "disabled";
7010 						break;
7011 						case "checked":
7012 							current.attributesPresent ~= "checked";
7013 						break;
7014 
7015 						case "visited", "active", "hover", "target", "focus", "selected":
7016 							current.attributesPresent ~= "nothing";
7017 							// FIXME
7018 						/+
7019 						// extensions not implemented
7020 						//case "text": // takes the text in the element and wraps it in an element, returning it
7021 						+/
7022 							goto case;
7023 						case "before", "after":
7024 							current.attributesPresent ~= "FIXME";
7025 
7026 						break;
7027 						// My extensions
7028 						case "odd-child":
7029 							current.oddChild = true;
7030 						break;
7031 						case "even-child":
7032 							current.evenChild = true;
7033 						break;
7034 						default:
7035 							//if(token.indexOf("lang") == -1)
7036 							//assert(0, token);
7037 						break;
7038 					}
7039 					state = State.Starting;
7040 				break;
7041 				case State.SkippingFunctionalSelector:
7042 					if(token == "(") {
7043 						parensCount++;
7044 					} else if(token == ")") {
7045 						parensCount--;
7046 					}
7047 
7048 					if(parensCount == 0)
7049 						state = State.Starting;
7050 				break;
7051 				case State.ReadingAttributeSelector:
7052 					attributeName = token;
7053 					attributeComparison = null;
7054 					attributeValue = null;
7055 					state = State.ReadingAttributeComparison;
7056 				break;
7057 				case State.ReadingAttributeComparison:
7058 					// FIXME: these things really should be quotable in the proper lexer...
7059 					if(token != "]") {
7060 						if(token.indexOf("=") == -1) {
7061 							// not a comparison; consider it
7062 							// part of the attribute
7063 							attributeValue ~= token;
7064 						} else {
7065 							attributeComparison = token;
7066 							state = State.ReadingAttributeValue;
7067 						}
7068 						break;
7069 					}
7070 					goto case;
7071 				case State.ExpectingAttributeCloser:
7072 					if(token != "]") {
7073 						// not the closer; consider it part of comparison
7074 						if(attributeComparison == "")
7075 							attributeName ~= token;
7076 						else
7077 							attributeValue ~= token;
7078 						break;
7079 					}
7080 
7081 					// Selector operators
7082 					switch(attributeComparison) {
7083 						default: assert(0);
7084 						case "":
7085 							current.attributesPresent ~= attributeName;
7086 						break;
7087 						case "=":
7088 							current.attributesEqual ~= [attributeName, attributeValue];
7089 						break;
7090 						case "|=":
7091 							current.attributesIncludesSeparatedByDashes ~= [attributeName, attributeValue];
7092 						break;
7093 						case "~=":
7094 							current.attributesIncludesSeparatedBySpaces ~= [attributeName, attributeValue];
7095 						break;
7096 						case "$=":
7097 							current.attributesEndsWith ~= [attributeName, attributeValue];
7098 						break;
7099 						case "^=":
7100 							current.attributesStartsWith ~= [attributeName, attributeValue];
7101 						break;
7102 						case "*=":
7103 							current.attributesInclude ~= [attributeName, attributeValue];
7104 						break;
7105 						case "!=":
7106 							current.attributesNotEqual ~= [attributeName, attributeValue];
7107 						break;
7108 					}
7109 
7110 					state = State.Starting;
7111 				break;
7112 				case State.ReadingAttributeValue:
7113 					attributeValue = token;
7114 					state = State.ExpectingAttributeCloser;
7115 				break;
7116 			}
7117 		}
7118 
7119 		commit();
7120 
7121 		return s;
7122 	}
7123 
7124 ///.
7125 Element[] removeDuplicates(Element[] input) {
7126 	Element[] ret;
7127 
7128 	bool[Element] already;
7129 	foreach(e; input) {
7130 		if(e in already) continue;
7131 		already[e] = true;
7132 		ret ~= e;
7133 	}
7134 
7135 	return ret;
7136 }
7137 
7138 // done with CSS selector handling
7139 
7140 
7141 // FIXME: use the better parser from html.d
7142 /// This is probably not useful to you unless you're writing a browser or something like that.
7143 /// It represents a *computed* style, like what the browser gives you after applying stylesheets, inline styles, and html attributes.
7144 /// From here, you can start to make a layout engine for the box model and have a css aware browser.
7145 class CssStyle {
7146 	///.
7147 	this(string rule, string content) {
7148 		rule = rule.strip();
7149 		content = content.strip();
7150 
7151 		if(content.length == 0)
7152 			return;
7153 
7154 		originatingRule = rule;
7155 		originatingSpecificity = getSpecificityOfRule(rule); // FIXME: if there's commas, this won't actually work!
7156 
7157 		foreach(part; content.split(";")) {
7158 			part = part.strip();
7159 			if(part.length == 0)
7160 				continue;
7161 			auto idx = part.indexOf(":");
7162 			if(idx == -1)
7163 				continue;
7164 				//throw new Exception("Bad css rule (no colon): " ~ part);
7165 
7166 			Property p;
7167 
7168 			p.name = part[0 .. idx].strip();
7169 			p.value = part[idx + 1 .. $].replace("! important", "!important").replace("!important", "").strip(); // FIXME don't drop important
7170 			p.givenExplicitly = true;
7171 			p.specificity = originatingSpecificity;
7172 
7173 			properties ~= p;
7174 		}
7175 
7176 		foreach(property; properties)
7177 			expandShortForm(property, originatingSpecificity);
7178 	}
7179 
7180 	///.
7181 	Specificity getSpecificityOfRule(string rule) {
7182 		Specificity s;
7183 		if(rule.length == 0) { // inline
7184 		//	s.important = 2;
7185 		} else {
7186 			// FIXME
7187 		}
7188 
7189 		return s;
7190 	}
7191 
7192 	string originatingRule; ///.
7193 	Specificity originatingSpecificity; ///.
7194 
7195 	///.
7196 	union Specificity {
7197 		uint score; ///.
7198 		// version(little_endian)
7199 		///.
7200 		struct {
7201 			ubyte tags; ///.
7202 			ubyte classes; ///.
7203 			ubyte ids; ///.
7204 			ubyte important; /// 0 = none, 1 = stylesheet author, 2 = inline style, 3 = user important
7205 		}
7206 	}
7207 
7208 	///.
7209 	struct Property {
7210 		bool givenExplicitly; /// this is false if for example the user said "padding" and this is "padding-left"
7211 		string name; ///.
7212 		string value; ///.
7213 		Specificity specificity; ///.
7214 		// do we care about the original source rule?
7215 	}
7216 
7217 	///.
7218 	Property[] properties;
7219 
7220 	///.
7221 	string opDispatch(string nameGiven)(string value = null) if(nameGiven != "popFront") {
7222 		string name = unCamelCase(nameGiven);
7223 		if(value is null)
7224 			return getValue(name);
7225 		else
7226 			return setValue(name, value, 0x02000000 /* inline specificity */);
7227 	}
7228 
7229 	/// takes dash style name
7230 	string getValue(string name) {
7231 		foreach(property; properties)
7232 			if(property.name == name)
7233 				return property.value;
7234 		return null;
7235 	}
7236 
7237 	/// takes dash style name
7238 	string setValue(string name, string value, Specificity newSpecificity, bool explicit = true) {
7239 		value = value.replace("! important", "!important");
7240 		if(value.indexOf("!important") != -1) {
7241 			newSpecificity.important = 1; // FIXME
7242 			value = value.replace("!important", "").strip();
7243 		}
7244 
7245 		foreach(ref property; properties)
7246 			if(property.name == name) {
7247 				if(newSpecificity.score >= property.specificity.score) {
7248 					property.givenExplicitly = explicit;
7249 					expandShortForm(property, newSpecificity);
7250 					return (property.value = value);
7251 				} else {
7252 					if(name == "display")
7253 					{}//writeln("Not setting ", name, " to ", value, " because ", newSpecificity.score, " < ", property.specificity.score);
7254 					return value; // do nothing - the specificity is too low
7255 				}
7256 			}
7257 
7258 		// it's not here...
7259 
7260 		Property p;
7261 		p.givenExplicitly = true;
7262 		p.name = name;
7263 		p.value = value;
7264 		p.specificity = originatingSpecificity;
7265 
7266 		properties ~= p;
7267 		expandShortForm(p, originatingSpecificity);
7268 
7269 		return value;
7270 	}
7271 
7272 	private void expandQuadShort(string name, string value, Specificity specificity) {
7273 		auto parts = value.split(" ");
7274 		switch(parts.length) {
7275 			case 1:
7276 				setValue(name ~"-left", parts[0], specificity, false);
7277 				setValue(name ~"-right", parts[0], specificity, false);
7278 				setValue(name ~"-top", parts[0], specificity, false);
7279 				setValue(name ~"-bottom", parts[0], specificity, false);
7280 			break;
7281 			case 2:
7282 				setValue(name ~"-left", parts[1], specificity, false);
7283 				setValue(name ~"-right", parts[1], specificity, false);
7284 				setValue(name ~"-top", parts[0], specificity, false);
7285 				setValue(name ~"-bottom", parts[0], specificity, false);
7286 			break;
7287 			case 3:
7288 				setValue(name ~"-top", parts[0], specificity, false);
7289 				setValue(name ~"-right", parts[1], specificity, false);
7290 				setValue(name ~"-bottom", parts[2], specificity, false);
7291 				setValue(name ~"-left", parts[2], specificity, false);
7292 
7293 			break;
7294 			case 4:
7295 				setValue(name ~"-top", parts[0], specificity, false);
7296 				setValue(name ~"-right", parts[1], specificity, false);
7297 				setValue(name ~"-bottom", parts[2], specificity, false);
7298 				setValue(name ~"-left", parts[3], specificity, false);
7299 			break;
7300 			default:
7301 				assert(0, value);
7302 		}
7303 	}
7304 
7305 	///.
7306 	void expandShortForm(Property p, Specificity specificity) {
7307 		switch(p.name) {
7308 			case "margin":
7309 			case "padding":
7310 				expandQuadShort(p.name, p.value, specificity);
7311 			break;
7312 			case "border":
7313 			case "outline":
7314 				setValue(p.name ~ "-left", p.value, specificity, false);
7315 				setValue(p.name ~ "-right", p.value, specificity, false);
7316 				setValue(p.name ~ "-top", p.value, specificity, false);
7317 				setValue(p.name ~ "-bottom", p.value, specificity, false);
7318 			break;
7319 
7320 			case "border-top":
7321 			case "border-bottom":
7322 			case "border-left":
7323 			case "border-right":
7324 			case "outline-top":
7325 			case "outline-bottom":
7326 			case "outline-left":
7327 			case "outline-right":
7328 
7329 			default: {}
7330 		}
7331 	}
7332 
7333 	///.
7334 	override string toString() {
7335 		string ret;
7336 		if(originatingRule.length)
7337 			ret = originatingRule ~ " {";
7338 
7339 		foreach(property; properties) {
7340 			if(!property.givenExplicitly)
7341 				continue; // skip the inferred shit
7342 
7343 			if(originatingRule.length)
7344 				ret ~= "\n\t";
7345 			else
7346 				ret ~= " ";
7347 
7348 			ret ~= property.name ~ ": " ~ property.value ~ ";";
7349 		}
7350 
7351 		if(originatingRule.length)
7352 			ret ~= "\n}\n";
7353 
7354 		return ret;
7355 	}
7356 }
7357 
7358 string cssUrl(string url) {
7359 	return "url(\"" ~ url ~ "\")";
7360 }
7361 
7362 /// This probably isn't useful, unless you're writing a browser or something like that.
7363 /// You might want to look at arsd.html for css macro, nesting, etc., or just use standard css
7364 /// as text.
7365 ///
7366 /// The idea, however, is to represent a kind of CSS object model, complete with specificity,
7367 /// that you can apply to your documents to build the complete computedStyle object.
7368 class StyleSheet {
7369 	///.
7370 	CssStyle[] rules;
7371 
7372 	///.
7373 	this(string source) {
7374 		// FIXME: handle @ rules and probably could improve lexer
7375 		// add nesting?
7376 		int state;
7377 		string currentRule;
7378 		string currentValue;
7379 
7380 		string* currentThing = &currentRule;
7381 		foreach(c; source) {
7382 			handle: switch(state) {
7383 				default: assert(0);
7384 				case 0: // starting - we assume we're reading a rule
7385 					switch(c) {
7386 						case '@':
7387 							state = 4;
7388 						break;
7389 						case '/':
7390 							state = 1;
7391 						break;
7392 						case '{':
7393 							currentThing = &currentValue;
7394 						break;
7395 						case '}':
7396 							if(currentThing is &currentValue) {
7397 								rules ~= new CssStyle(currentRule, currentValue);
7398 
7399 								currentRule = "";
7400 								currentValue = "";
7401 
7402 								currentThing = &currentRule;
7403 							} else {
7404 								// idk what is going on here.
7405 								// check sveit.com to reproduce
7406 								currentRule = "";
7407 								currentValue = "";
7408 							}
7409 						break;
7410 						default:
7411 							(*currentThing) ~= c;
7412 					}
7413 				break;
7414 				case 1: // expecting *
7415 					if(c == '*')
7416 						state = 2;
7417 					else {
7418 						state = 0;
7419 						(*currentThing) ~= "/" ~ c;
7420 					}
7421 				break;
7422 				case 2: // inside comment
7423 					if(c == '*')
7424 						state = 3;
7425 				break;
7426 				case 3: // expecting / to end comment
7427 					if(c == '/')
7428 						state = 0;
7429 					else
7430 						state = 2; // it's just a comment so no need to append
7431 				break;
7432 				case 4:
7433 					if(c == '{')
7434 						state = 5;
7435 					if(c == ';')
7436 						state = 0; // just skipping import
7437 				break;
7438 				case 5:
7439 					if(c == '}')
7440 						state = 0; // skipping font face probably
7441 			}
7442 		}
7443 	}
7444 
7445 	/// Run through the document and apply this stylesheet to it. The computedStyle member will be accurate after this call
7446 	void apply(Document document) {
7447 		foreach(rule; rules) {
7448 			if(rule.originatingRule.length == 0)
7449 				continue; // this shouldn't happen here in a stylesheet
7450 			foreach(element; document.querySelectorAll(rule.originatingRule)) {
7451 				// note: this should be a different object than the inline style
7452 				// since givenExplicitly is likely destroyed here
7453 				auto current = element.computedStyle;
7454 
7455 				foreach(item; rule.properties)
7456 					current.setValue(item.name, item.value, item.specificity);
7457 			}
7458 		}
7459 	}
7460 }
7461 
7462 
7463 /// This is kinda private; just a little utility container for use by the ElementStream class.
7464 final class Stack(T) {
7465 	this() {
7466 		internalLength = 0;
7467 		arr = initialBuffer[];
7468 	}
7469 
7470 	///.
7471 	void push(T t) {
7472 		if(internalLength >= arr.length) {
7473 			auto oldarr = arr;
7474 			if(arr.length < 4096)
7475 				arr = new T[arr.length * 2];
7476 			else
7477 				arr = new T[arr.length + 4096];
7478 			arr[0 .. oldarr.length] = oldarr[];
7479 		}
7480 
7481 		arr[internalLength] = t;
7482 		internalLength++;
7483 	}
7484 
7485 	///.
7486 	T pop() {
7487 		assert(internalLength);
7488 		internalLength--;
7489 		return arr[internalLength];
7490 	}
7491 
7492 	///.
7493 	T peek() {
7494 		assert(internalLength);
7495 		return arr[internalLength - 1];
7496 	}
7497 
7498 	///.
7499 	@property bool empty() {
7500 		return internalLength ? false : true;
7501 	}
7502 
7503 	///.
7504 	private T[] arr;
7505 	private size_t internalLength;
7506 	private T[64] initialBuffer;
7507 	// the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep),
7508 	// using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push()
7509 	// function thanks to this, and push() was actually one of the slowest individual functions in the code!
7510 }
7511 
7512 /// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively.
7513 final class ElementStream {
7514 
7515 	///.
7516 	@property Element front() {
7517 		return current.element;
7518 	}
7519 
7520 	/// Use Element.tree instead.
7521 	this(Element start) {
7522 		current.element = start;
7523 		current.childPosition = -1;
7524 		isEmpty = false;
7525 		stack = new Stack!(Current);
7526 	}
7527 
7528 	/*
7529 		Handle it
7530 		handle its children
7531 
7532 	*/
7533 
7534 	///.
7535 	void popFront() {
7536 	    more:
7537 	    	if(isEmpty) return;
7538 
7539 		// FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times)
7540 
7541 		current.childPosition++;
7542 		if(current.childPosition >= current.element.children.length) {
7543 			if(stack.empty())
7544 				isEmpty = true;
7545 			else {
7546 				current = stack.pop();
7547 				goto more;
7548 			}
7549 		} else {
7550 			stack.push(current);
7551 			current.element = current.element.children[current.childPosition];
7552 			current.childPosition = -1;
7553 		}
7554 	}
7555 
7556 	/// You should call this when you remove an element from the tree. It then doesn't recurse into that node and adjusts the current position, keeping the range stable.
7557 	void currentKilled() {
7558 		if(stack.empty) // should never happen
7559 			isEmpty = true;
7560 		else {
7561 			current = stack.pop();
7562 			current.childPosition--; // when it is killed, the parent is brought back a lil so when we popFront, this is then right
7563 		}
7564 	}
7565 
7566 	///.
7567 	@property bool empty() {
7568 		return isEmpty;
7569 	}
7570 
7571 	private:
7572 
7573 	struct Current {
7574 		Element element;
7575 		int childPosition;
7576 	}
7577 
7578 	Current current;
7579 
7580 	Stack!(Current) stack;
7581 
7582 	bool isEmpty;
7583 }
7584 
7585 
7586 
7587 // unbelievable.
7588 // Don't use any of these in your own code. Instead, try to use phobos or roll your own, as I might kill these at any time.
7589 sizediff_t indexOfBytes(immutable(ubyte)[] haystack, immutable(ubyte)[] needle) {
7590 	static import std.algorithm;
7591 	auto found = std.algorithm.find(haystack, needle);
7592 	if(found.length == 0)
7593 		return -1;
7594 	return haystack.length - found.length;
7595 }
7596 
7597 private T[] insertAfter(T)(T[] arr, int position, T[] what) {
7598 	assert(position < arr.length);
7599 	T[] ret;
7600 	ret.length = arr.length + what.length;
7601 	int a = 0;
7602 	foreach(i; arr[0..position+1])
7603 		ret[a++] = i;
7604 
7605 	foreach(i; what)
7606 		ret[a++] = i;
7607 
7608 	foreach(i; arr[position+1..$])
7609 		ret[a++] = i;
7610 
7611 	return ret;
7612 }
7613 
7614 package bool isInArray(T)(T item, T[] arr) {
7615 	foreach(i; arr)
7616 		if(item == i)
7617 			return true;
7618 	return false;
7619 }
7620 
7621 private string[string] aadup(in string[string] arr) {
7622 	string[string] ret;
7623 	foreach(k, v; arr)
7624 		ret[k] = v;
7625 	return ret;
7626 }
7627 
7628 
7629 
7630 
7631 
7632 
7633 
7634 
7635 
7636 
7637 
7638 
7639 
7640 
7641 
7642 // These MUST be sorted. See generatedomcases.d for a program to generate it if you need to add more than a few (otherwise maybe you can work it in yourself but yikes)
7643 
7644 immutable string[] availableEntities =
7645 ["AElig", "AElig", "AMP", "AMP", "Aacute", "Aacute", "Abreve", "Abreve", "Acirc", "Acirc", "Acy", "Acy", "Afr", "Afr", "Agrave", "Agrave", "Alpha", "Alpha", "Amacr", "Amacr", "And", "And", "Aogon", "Aogon", "Aopf", "Aopf", "ApplyFunction", "ApplyFunction", "Aring", "Aring", "Ascr", "Ascr", "Assign", "Assign", "Atilde",
7646 "Atilde", "Auml", "Auml", "Backslash", "Backslash", "Barv", "Barv", "Barwed", "Barwed", "Bcy", "Bcy", "Because", "Because", "Bernoullis", "Bernoullis", "Beta", "Beta", "Bfr", "Bfr", "Bopf", "Bopf", "Breve", "Breve", "Bscr", "Bscr", "Bumpeq", "Bumpeq", "CHcy", "CHcy", "COPY", "COPY", "Cacute", "Cacute", "Cap", "Cap", "CapitalDifferentialD",
7647 "CapitalDifferentialD", "Cayleys", "Cayleys", "Ccaron", "Ccaron", "Ccedil", "Ccedil", "Ccirc", "Ccirc", "Cconint", "Cconint", "Cdot", "Cdot", "Cedilla", "Cedilla", "CenterDot", "CenterDot", "Cfr", "Cfr", "Chi", "Chi", "CircleDot", "CircleDot", "CircleMinus", "CircleMinus", "CirclePlus", "CirclePlus", "CircleTimes", "CircleTimes",
7648 "ClockwiseContourIntegral", "ClockwiseContourIntegral", "CloseCurlyDoubleQuote", "CloseCurlyDoubleQuote", "CloseCurlyQuote", "CloseCurlyQuote", "Colon", "Colon", "Colone", "Colone", "Congruent", "Congruent", "Conint", "Conint", "ContourIntegral", "ContourIntegral", "Copf", "Copf", "Coproduct", "Coproduct", "CounterClockwiseContourIntegral",
7649 "CounterClockwiseContourIntegral", "Cross", "Cross", "Cscr", "Cscr", "Cup", "Cup", "CupCap", "CupCap", "DD", "DD", "DDotrahd", "DDotrahd", "DJcy", "DJcy", "DScy", "DScy", "DZcy", "DZcy", "Dagger", "Dagger", "Darr", "Darr", "Dashv", "Dashv", "Dcaron", "Dcaron", "Dcy", "Dcy", "Del", "Del", "Delta", "Delta", "Dfr", "Dfr",
7650 "DiacriticalAcute", "DiacriticalAcute", "DiacriticalDot", "DiacriticalDot", "DiacriticalDoubleAcute", "DiacriticalDoubleAcute", "DiacriticalGrave", "DiacriticalGrave", "DiacriticalTilde", "DiacriticalTilde", "Diamond", "Diamond", "DifferentialD", "DifferentialD", "Dopf", "Dopf", "Dot", "Dot", "DotDot", "DotDot", "DotEqual",
7651 "DotEqual", "DoubleContourIntegral", "DoubleContourIntegral", "DoubleDot", "DoubleDot", "DoubleDownArrow", "DoubleDownArrow", "DoubleLeftArrow", "DoubleLeftArrow", "DoubleLeftRightArrow", "DoubleLeftRightArrow", "DoubleLeftTee", "DoubleLeftTee", "DoubleLongLeftArrow", "DoubleLongLeftArrow", "DoubleLongLeftRightArrow",
7652 "DoubleLongLeftRightArrow", "DoubleLongRightArrow", "DoubleLongRightArrow", "DoubleRightArrow", "DoubleRightArrow", "DoubleRightTee", "DoubleRightTee", "DoubleUpArrow", "DoubleUpArrow", "DoubleUpDownArrow", "DoubleUpDownArrow", "DoubleVerticalBar", "DoubleVerticalBar", "DownArrow", "DownArrow", "DownArrowBar", "DownArrowBar",
7653 "DownArrowUpArrow", "DownArrowUpArrow", "DownBreve", "DownBreve", "DownLeftRightVector", "DownLeftRightVector", "DownLeftTeeVector", "DownLeftTeeVector", "DownLeftVector", "DownLeftVector", "DownLeftVectorBar", "DownLeftVectorBar", "DownRightTeeVector", "DownRightTeeVector", "DownRightVector", "DownRightVector", "DownRightVectorBar",
7654 "DownRightVectorBar", "DownTee", "DownTee", "DownTeeArrow", "DownTeeArrow", "Downarrow", "Downarrow", "Dscr", "Dscr", "Dstrok", "Dstrok", "ENG", "ENG", "ETH", "ETH", "Eacute", "Eacute", "Ecaron", "Ecaron", "Ecirc", "Ecirc", "Ecy", "Ecy", "Edot", "Edot", "Efr", "Efr", "Egrave", "Egrave", "Element", "Element", "Emacr", "Emacr",
7655 "EmptySmallSquare", "EmptySmallSquare", "EmptyVerySmallSquare", "EmptyVerySmallSquare", "Eogon", "Eogon", "Eopf", "Eopf", "Epsilon", "Epsilon", "Equal", "Equal", "EqualTilde", "EqualTilde", "Equilibrium", "Equilibrium", "Escr", "Escr", "Esim", "Esim", "Eta", "Eta", "Euml", "Euml", "Exists", "Exists", "ExponentialE", "ExponentialE",
7656 "Fcy", "Fcy", "Ffr", "Ffr", "FilledSmallSquare", "FilledSmallSquare", "FilledVerySmallSquare", "FilledVerySmallSquare", "Fopf", "Fopf", "ForAll", "ForAll", "Fouriertrf", "Fouriertrf", "Fscr", "Fscr", "GJcy", "GJcy", "GT", "GT", "Gamma", "Gamma", "Gammad", "Gammad", "Gbreve", "Gbreve", "Gcedil", "Gcedil", "Gcirc", "Gcirc",
7657 "Gcy", "Gcy", "Gdot", "Gdot", "Gfr", "Gfr", "Gg", "Gg", "Gopf", "Gopf", "GreaterEqual", "GreaterEqual", "GreaterEqualLess", "GreaterEqualLess", "GreaterFullEqual", "GreaterFullEqual", "GreaterGreater", "GreaterGreater", "GreaterLess", "GreaterLess", "GreaterSlantEqual", "GreaterSlantEqual", "GreaterTilde", "GreaterTilde",
7658 "Gscr", "Gscr", "Gt", "Gt", "HARDcy", "HARDcy", "Hacek", "Hacek", "Hat", "Hat", "Hcirc", "Hcirc", "Hfr", "Hfr", "HilbertSpace", "HilbertSpace", "Hopf", "Hopf", "HorizontalLine", "HorizontalLine", "Hscr", "Hscr", "Hstrok", "Hstrok", "HumpDownHump", "HumpDownHump", "HumpEqual", "HumpEqual", "IEcy", "IEcy", "IJlig", "IJlig",
7659 "IOcy", "IOcy", "Iacute", "Iacute", "Icirc", "Icirc", "Icy", "Icy", "Idot", "Idot", "Ifr", "Ifr", "Igrave", "Igrave", "Im", "Im", "Imacr", "Imacr", "ImaginaryI", "ImaginaryI", "Implies", "Implies", "Int", "Int", "Integral", "Integral", "Intersection", "Intersection", "InvisibleComma", "InvisibleComma", "InvisibleTimes",
7660 "InvisibleTimes", "Iogon", "Iogon", "Iopf", "Iopf", "Iota", "Iota", "Iscr", "Iscr", "Itilde", "Itilde", "Iukcy", "Iukcy", "Iuml", "Iuml", "Jcirc", "Jcirc", "Jcy", "Jcy", "Jfr", "Jfr", "Jopf", "Jopf", "Jscr", "Jscr", "Jsercy", "Jsercy", "Jukcy", "Jukcy", "KHcy", "KHcy", "KJcy", "KJcy", "Kappa", "Kappa", "Kcedil", "Kcedil",
7661 "Kcy", "Kcy", "Kfr", "Kfr", "Kopf", "Kopf", "Kscr", "Kscr", "LJcy", "LJcy", "LT", "LT", "Lacute", "Lacute", "Lambda", "Lambda", "Lang", "Lang", "Laplacetrf", "Laplacetrf", "Larr", "Larr", "Lcaron", "Lcaron", "Lcedil", "Lcedil", "Lcy", "Lcy", "LeftAngleBracket", "LeftAngleBracket", "LeftArrow", "LeftArrow", "LeftArrowBar",
7662 "LeftArrowBar", "LeftArrowRightArrow", "LeftArrowRightArrow", "LeftCeiling", "LeftCeiling", "LeftDoubleBracket", "LeftDoubleBracket", "LeftDownTeeVector", "LeftDownTeeVector", "LeftDownVector", "LeftDownVector", "LeftDownVectorBar", "LeftDownVectorBar", "LeftFloor", "LeftFloor", "LeftRightArrow", "LeftRightArrow", "LeftRightVector",
7663 "LeftRightVector", "LeftTee", "LeftTee", "LeftTeeArrow", "LeftTeeArrow", "LeftTeeVector", "LeftTeeVector", "LeftTriangle", "LeftTriangle", "LeftTriangleBar", "LeftTriangleBar", "LeftTriangleEqual", "LeftTriangleEqual", "LeftUpDownVector", "LeftUpDownVector", "LeftUpTeeVector", "LeftUpTeeVector", "LeftUpVector", "LeftUpVector",
7664 "LeftUpVectorBar", "LeftUpVectorBar", "LeftVector", "LeftVector", "LeftVectorBar", "LeftVectorBar", "Leftarrow", "Leftarrow", "Leftrightarrow", "Leftrightarrow", "LessEqualGreater", "LessEqualGreater", "LessFullEqual", "LessFullEqual", "LessGreater", "LessGreater", "LessLess", "LessLess", "LessSlantEqual", "LessSlantEqual",
7665 "LessTilde", "LessTilde", "Lfr", "Lfr", "Ll", "Ll", "Lleftarrow", "Lleftarrow", "Lmidot", "Lmidot", "LongLeftArrow", "LongLeftArrow", "LongLeftRightArrow", "LongLeftRightArrow", "LongRightArrow", "LongRightArrow", "Longleftarrow", "Longleftarrow", "Longleftrightarrow", "Longleftrightarrow", "Longrightarrow", "Longrightarrow",
7666 "Lopf", "Lopf", "LowerLeftArrow", "LowerLeftArrow", "LowerRightArrow", "LowerRightArrow", "Lscr", "Lscr", "Lsh", "Lsh", "Lstrok", "Lstrok", "Lt", "Lt", "Map", "Map", "Mcy", "Mcy", "MediumSpace", "MediumSpace", "Mellintrf", "Mellintrf", "Mfr", "Mfr", "MinusPlus", "MinusPlus", "Mopf", "Mopf", "Mscr", "Mscr", "Mu", "Mu",
7667 "NJcy", "NJcy", "Nacute", "Nacute", "Ncaron", "Ncaron", "Ncedil", "Ncedil", "Ncy", "Ncy", "NegativeMediumSpace", "NegativeMediumSpace", "NegativeThickSpace", "NegativeThickSpace", "NegativeThinSpace", "NegativeThinSpace", "NegativeVeryThinSpace", "NegativeVeryThinSpace", "NestedGreaterGreater", "NestedGreaterGreater",
7668 "NestedLessLess", "NestedLessLess", "NewLine", "NewLine", "Nfr", "Nfr", "NoBreak", "NoBreak", "NonBreakingSpace", "NonBreakingSpace", "Nopf", "Nopf", "Not", "Not", "NotCongruent", "NotCongruent", "NotCupCap", "NotCupCap", "NotDoubleVerticalBar", "NotDoubleVerticalBar", "NotElement", "NotElement", "NotEqual", "NotEqual",
7669 "NotExists", "NotExists", "NotGreater", "NotGreater", "NotGreaterEqual", "NotGreaterEqual", "NotGreaterLess", "NotGreaterLess", "NotGreaterTilde", "NotGreaterTilde", "NotLeftTriangle", "NotLeftTriangle", "NotLeftTriangleEqual", "NotLeftTriangleEqual", "NotLess", "NotLess", "NotLessEqual", "NotLessEqual", "NotLessGreater",
7670 "NotLessGreater", "NotLessTilde", "NotLessTilde", "NotPrecedes", "NotPrecedes", "NotPrecedesSlantEqual", "NotPrecedesSlantEqual", "NotReverseElement", "NotReverseElement", "NotRightTriangle", "NotRightTriangle", "NotRightTriangleEqual", "NotRightTriangleEqual", "NotSquareSubsetEqual", "NotSquareSubsetEqual", "NotSquareSupersetEqual",
7671 "NotSquareSupersetEqual", "NotSubsetEqual", "NotSubsetEqual", "NotSucceeds", "NotSucceeds", "NotSucceedsSlantEqual", "NotSucceedsSlantEqual", "NotSupersetEqual", "NotSupersetEqual", "NotTilde", "NotTilde", "NotTildeEqual", "NotTildeEqual", "NotTildeFullEqual", "NotTildeFullEqual", "NotTildeTilde", "NotTildeTilde", "NotVerticalBar",
7672 "NotVerticalBar", "Nscr", "Nscr", "Ntilde", "Ntilde", "Nu", "Nu", "OElig", "OElig", "Oacute", "Oacute", "Ocirc", "Ocirc", "Ocy", "Ocy", "Odblac", "Odblac", "Ofr", "Ofr", "Ograve", "Ograve", "Omacr", "Omacr", "Omega", "Omega", "Omicron", "Omicron", "Oopf", "Oopf", "OpenCurlyDoubleQuote", "OpenCurlyDoubleQuote", "OpenCurlyQuote",
7673 "OpenCurlyQuote", "Or", "Or", "Oscr", "Oscr", "Oslash", "Oslash", "Otilde", "Otilde", "Otimes", "Otimes", "Ouml", "Ouml", "OverBar", "OverBar", "OverBrace", "OverBrace", "OverBracket", "OverBracket", "OverParenthesis", "OverParenthesis", "PartialD", "PartialD", "Pcy", "Pcy", "Pfr", "Pfr", "Phi", "Phi", "Pi", "Pi", "PlusMinus",
7674 "PlusMinus", "Poincareplane", "Poincareplane", "Popf", "Popf", "Pr", "Pr", "Precedes", "Precedes", "PrecedesEqual", "PrecedesEqual", "PrecedesSlantEqual", "PrecedesSlantEqual", "PrecedesTilde", "PrecedesTilde", "Prime", "Prime", "Product", "Product", "Proportion", "Proportion", "Proportional", "Proportional", "Pscr", "Pscr",
7675 "Psi", "Psi", "QUOT", "QUOT", "Qfr", "Qfr", "Qopf", "Qopf", "Qscr", "Qscr", "RBarr", "RBarr", "REG", "REG", "Racute", "Racute", "Rang", "Rang", "Rarr", "Rarr", "Rarrtl", "Rarrtl", "Rcaron", "Rcaron", "Rcedil", "Rcedil", "Rcy", "Rcy", "Re", "Re", "ReverseElement", "ReverseElement", "ReverseEquilibrium", "ReverseEquilibrium",
7676 "ReverseUpEquilibrium", "ReverseUpEquilibrium", "Rfr", "Rfr", "Rho", "Rho", "RightAngleBracket", "RightAngleBracket", "RightArrow", "RightArrow", "RightArrowBar", "RightArrowBar", "RightArrowLeftArrow", "RightArrowLeftArrow", "RightCeiling", "RightCeiling", "RightDoubleBracket", "RightDoubleBracket", "RightDownTeeVector",
7677 "RightDownTeeVector", "RightDownVector", "RightDownVector", "RightDownVectorBar", "RightDownVectorBar", "RightFloor", "RightFloor", "RightTee", "RightTee", "RightTeeArrow", "RightTeeArrow", "RightTeeVector", "RightTeeVector", "RightTriangle", "RightTriangle", "RightTriangleBar", "RightTriangleBar", "RightTriangleEqual",
7678 "RightTriangleEqual", "RightUpDownVector", "RightUpDownVector", "RightUpTeeVector", "RightUpTeeVector", "RightUpVector", "RightUpVector", "RightUpVectorBar", "RightUpVectorBar", "RightVector", "RightVector", "RightVectorBar", "RightVectorBar", "Rightarrow", "Rightarrow", "Ropf", "Ropf", "RoundImplies", "RoundImplies",
7679 "Rrightarrow", "Rrightarrow", "Rscr", "Rscr", "Rsh", "Rsh", "RuleDelayed", "RuleDelayed", "SHCHcy", "SHCHcy", "SHcy", "SHcy", "SOFTcy", "SOFTcy", "Sacute", "Sacute", "Sc", "Sc", "Scaron", "Scaron", "Scedil", "Scedil", "Scirc", "Scirc", "Scy", "Scy", "Sfr", "Sfr", "ShortDownArrow", "ShortDownArrow", "ShortLeftArrow", "ShortLeftArrow",
7680 "ShortRightArrow", "ShortRightArrow", "ShortUpArrow", "ShortUpArrow", "Sigma", "Sigma", "SmallCircle", "SmallCircle", "Sopf", "Sopf", "Sqrt", "Sqrt", "Square", "Square", "SquareIntersection", "SquareIntersection", "SquareSubset", "SquareSubset", "SquareSubsetEqual", "SquareSubsetEqual", "SquareSuperset", "SquareSuperset",
7681 "SquareSupersetEqual", "SquareSupersetEqual", "SquareUnion", "SquareUnion", "Sscr", "Sscr", "Star", "Star", "Sub", "Sub", "Subset", "Subset", "SubsetEqual", "SubsetEqual", "Succeeds", "Succeeds", "SucceedsEqual", "SucceedsEqual", "SucceedsSlantEqual", "SucceedsSlantEqual", "SucceedsTilde", "SucceedsTilde", "SuchThat",
7682 "SuchThat", "Sum", "Sum", "Sup", "Sup", "Superset", "Superset", "SupersetEqual", "SupersetEqual", "Supset", "Supset", "THORN", "THORN", "TRADE", "TRADE", "TSHcy", "TSHcy", "TScy", "TScy", "Tab", "Tab", "Tau", "Tau", "Tcaron", "Tcaron", "Tcedil", "Tcedil", "Tcy", "Tcy", "Tfr", "Tfr", "Therefore", "Therefore", "Theta", "Theta",
7683 "ThinSpace", "ThinSpace", "Tilde", "Tilde", "TildeEqual", "TildeEqual", "TildeFullEqual", "TildeFullEqual", "TildeTilde", "TildeTilde", "Topf", "Topf", "TripleDot", "TripleDot", "Tscr", "Tscr", "Tstrok", "Tstrok", "Uacute", "Uacute", "Uarr", "Uarr", "Uarrocir", "Uarrocir", "Ubrcy", "Ubrcy", "Ubreve", "Ubreve", "Ucirc",
7684 "Ucirc", "Ucy", "Ucy", "Udblac", "Udblac", "Ufr", "Ufr", "Ugrave", "Ugrave", "Umacr", "Umacr", "UnderBar", "UnderBar", "UnderBrace", "UnderBrace", "UnderBracket", "UnderBracket", "UnderParenthesis", "UnderParenthesis", "Union", "Union", "UnionPlus", "UnionPlus", "Uogon", "Uogon", "Uopf", "Uopf", "UpArrow", "UpArrow", "UpArrowBar",
7685 "UpArrowBar", "UpArrowDownArrow", "UpArrowDownArrow", "UpDownArrow", "UpDownArrow", "UpEquilibrium", "UpEquilibrium", "UpTee", "UpTee", "UpTeeArrow", "UpTeeArrow", "Uparrow", "Uparrow", "Updownarrow", "Updownarrow", "UpperLeftArrow", "UpperLeftArrow", "UpperRightArrow", "UpperRightArrow", "Upsi", "Upsi", "Upsilon", "Upsilon",
7686 "Uring", "Uring", "Uscr", "Uscr", "Utilde", "Utilde", "Uuml", "Uuml", "VDash", "VDash", "Vbar", "Vbar", "Vcy", "Vcy", "Vdash", "Vdash", "Vdashl", "Vdashl", "Vee", "Vee", "Verbar", "Verbar", "Vert", "Vert", "VerticalBar", "VerticalBar", "VerticalLine", "VerticalLine", "VerticalSeparator", "VerticalSeparator", "VerticalTilde",
7687 "VerticalTilde", "VeryThinSpace", "VeryThinSpace", "Vfr", "Vfr", "Vopf", "Vopf", "Vscr", "Vscr", "Vvdash", "Vvdash", "Wcirc", "Wcirc", "Wedge", "Wedge", "Wfr", "Wfr", "Wopf", "Wopf", "Wscr", "Wscr", "Xfr", "Xfr", "Xi", "Xi", "Xopf", "Xopf", "Xscr", "Xscr", "YAcy", "YAcy", "YIcy", "YIcy", "YUcy", "YUcy", "Yacute", "Yacute",
7688 "Ycirc", "Ycirc", "Ycy", "Ycy", "Yfr", "Yfr", "Yopf", "Yopf", "Yscr", "Yscr", "Yuml", "Yuml", "ZHcy", "ZHcy", "Zacute", "Zacute", "Zcaron", "Zcaron", "Zcy", "Zcy", "Zdot", "Zdot", "ZeroWidthSpace", "ZeroWidthSpace", "Zeta", "Zeta", "Zfr", "Zfr", "Zopf", "Zopf", "Zscr", "Zscr", "aacute", "aacute", "abreve", "abreve", "ac",
7689 "ac", "acd", "acd", "acirc", "acirc", "acute", "acute", "acy", "acy", "aelig", "aelig", "af", "af", "afr", "afr", "agrave", "agrave", "alefsym", "alefsym", "aleph", "aleph", "alpha", "alpha", "amacr", "amacr", "amalg", "amalg", "and", "and", "andand", "andand", "andd", "andd", "andslope", "andslope", "andv", "andv", "ang",
7690 "ang", "ange", "ange", "angle", "angle", "angmsd", "angmsd", "angmsdaa", "angmsdaa", "angmsdab", "angmsdab", "angmsdac", "angmsdac", "angmsdad", "angmsdad", "angmsdae", "angmsdae", "angmsdaf", "angmsdaf", "angmsdag", "angmsdag", "angmsdah", "angmsdah", "angrt", "angrt", "angrtvb", "angrtvb", "angrtvbd", "angrtvbd", "angsph",
7691 "angsph", "angst", "angst", "angzarr", "angzarr", "aogon", "aogon", "aopf", "aopf", "ap", "ap", "apE", "apE", "apacir", "apacir", "ape", "ape", "apid", "apid", "approx", "approx", "approxeq", "approxeq", "aring", "aring", "ascr", "ascr", "ast", "ast", "asymp", "asymp", "asympeq", "asympeq", "atilde", "atilde", "auml",
7692 "auml", "awconint", "awconint", "awint", "awint", "bNot", "bNot", "backcong", "backcong", "backepsilon", "backepsilon", "backprime", "backprime", "backsim", "backsim", "backsimeq", "backsimeq", "barvee", "barvee", "barwed", "barwed", "barwedge", "barwedge", "bbrk", "bbrk", "bbrktbrk", "bbrktbrk", "bcong", "bcong", "bcy",
7693 "bcy", "bdquo", "bdquo", "becaus", "becaus", "because", "because", "bemptyv", "bemptyv", "bepsi", "bepsi", "bernou", "bernou", "beta", "beta", "beth", "beth", "between", "between", "bfr", "bfr", "bigcap", "bigcap", "bigcirc", "bigcirc", "bigcup", "bigcup", "bigodot", "bigodot", "bigoplus", "bigoplus", "bigotimes", "bigotimes",
7694 "bigsqcup", "bigsqcup", "bigstar", "bigstar", "bigtriangledown", "bigtriangledown", "bigtriangleup", "bigtriangleup", "biguplus", "biguplus", "bigvee", "bigvee", "bigwedge", "bigwedge", "bkarow", "bkarow", "blacklozenge", "blacklozenge", "blacksquare", "blacksquare", "blacktriangle", "blacktriangle", "blacktriangledown",
7695 "blacktriangledown", "blacktriangleleft", "blacktriangleleft", "blacktriangleright", "blacktriangleright", "blank", "blank", "blk12", "blk12", "blk14", "blk14", "blk34", "blk34", "block", "block", "bnot", "bnot", "bopf", "bopf", "bot", "bot", "bottom", "bottom", "bowtie", "bowtie", "boxDL", "boxDL", "boxDR", "boxDR", "boxDl",
7696 "boxDl", "boxDr", "boxDr", "boxH", "boxH", "boxHD", "boxHD", "boxHU", "boxHU", "boxHd", "boxHd", "boxHu", "boxHu", "boxUL", "boxUL", "boxUR", "boxUR", "boxUl", "boxUl", "boxUr", "boxUr", "boxV", "boxV", "boxVH", "boxVH", "boxVL", "boxVL", "boxVR", "boxVR", "boxVh", "boxVh", "boxVl", "boxVl", "boxVr", "boxVr", "boxbox",
7697 "boxbox", "boxdL", "boxdL", "boxdR", "boxdR", "boxdl", "boxdl", "boxdr", "boxdr", "boxh", "boxh", "boxhD", "boxhD", "boxhU", "boxhU", "boxhd", "boxhd", "boxhu", "boxhu", "boxminus", "boxminus", "boxplus", "boxplus", "boxtimes", "boxtimes", "boxuL", "boxuL", "boxuR", "boxuR", "boxul", "boxul", "boxur", "boxur", "boxv",
7698 "boxv", "boxvH", "boxvH", "boxvL", "boxvL", "boxvR", "boxvR", "boxvh", "boxvh", "boxvl", "boxvl", "boxvr", "boxvr", "bprime", "bprime", "breve", "breve", "brvbar", "brvbar", "bscr", "bscr", "bsemi", "bsemi", "bsim", "bsim", "bsime", "bsime", "bsol", "bsol", "bsolb", "bsolb", "bsolhsub", "bsolhsub", "bull", "bull", "bullet",
7699 "bullet", "bump", "bump", "bumpE", "bumpE", "bumpe", "bumpe", "bumpeq", "bumpeq", "cacute", "cacute", "cap", "cap", "capand", "capand", "capbrcup", "capbrcup", "capcap", "capcap", "capcup", "capcup", "capdot", "capdot", "caret", "caret", "caron", "caron", "ccaps", "ccaps", "ccaron", "ccaron", "ccedil", "ccedil", "ccirc",
7700 "ccirc", "ccups", "ccups", "ccupssm", "ccupssm", "cdot", "cdot", "cedil", "cedil", "cemptyv", "cemptyv", "cent", "cent", "centerdot", "centerdot", "cfr", "cfr", "chcy", "chcy", "check", "check", "checkmark", "checkmark", "chi", "chi", "cir", "cir", "cirE", "cirE", "circ", "circ", "circeq", "circeq", "circlearrowleft",
7701 "circlearrowleft", "circlearrowright", "circlearrowright", "circledR", "circledR", "circledS", "circledS", "circledast", "circledast", "circledcirc", "circledcirc", "circleddash", "circleddash", "cire", "cire", "cirfnint", "cirfnint", "cirmid", "cirmid", "cirscir", "cirscir", "clubs", "clubs", "clubsuit", "clubsuit", "colon",
7702 "colon", "colone", "colone", "coloneq", "coloneq", "comma", "comma", "commat", "commat", "comp", "comp", "compfn", "compfn", "complement", "complement", "complexes", "complexes", "cong", "cong", "congdot", "congdot", "conint", "conint", "copf", "copf", "coprod", "coprod", "copy", "copy", "copysr", "copysr", "crarr", "crarr",
7703 "cross", "cross", "cscr", "cscr", "csub", "csub", "csube", "csube", "csup", "csup", "csupe", "csupe", "ctdot", "ctdot", "cudarrl", "cudarrl", "cudarrr", "cudarrr", "cuepr", "cuepr", "cuesc", "cuesc", "cularr", "cularr", "cularrp", "cularrp", "cup", "cup", "cupbrcap", "cupbrcap", "cupcap", "cupcap", "cupcup", "cupcup",
7704 "cupdot", "cupdot", "cupor", "cupor", "curarr", "curarr", "curarrm", "curarrm", "curlyeqprec", "curlyeqprec", "curlyeqsucc", "curlyeqsucc", "curlyvee", "curlyvee", "curlywedge", "curlywedge", "curren", "curren", "curvearrowleft", "curvearrowleft", "curvearrowright", "curvearrowright", "cuvee", "cuvee", "cuwed", "cuwed",
7705 "cwconint", "cwconint", "cwint", "cwint", "cylcty", "cylcty", "dArr", "dArr", "dHar", "dHar", "dagger", "dagger", "daleth", "daleth", "darr", "darr", "dash", "dash", "dashv", "dashv", "dbkarow", "dbkarow", "dblac", "dblac", "dcaron", "dcaron", "dcy", "dcy", "dd", "dd", "ddagger", "ddagger", "ddarr", "ddarr", "ddotseq",
7706 "ddotseq", "deg", "deg", "delta", "delta", "demptyv", "demptyv", "dfisht", "dfisht", "dfr", "dfr", "dharl", "dharl", "dharr", "dharr", "diam", "diam", "diamond", "diamond", "diamondsuit", "diamondsuit", "diams", "diams", "die", "die", "digamma", "digamma", "disin", "disin", "div", "div", "divide", "divide", "divideontimes",
7707 "divideontimes", "divonx", "divonx", "djcy", "djcy", "dlcorn", "dlcorn", "dlcrop", "dlcrop", "dollar", "dollar", "dopf", "dopf", "dot", "dot", "doteq", "doteq", "doteqdot", "doteqdot", "dotminus", "dotminus", "dotplus", "dotplus", "dotsquare", "dotsquare", "doublebarwedge", "doublebarwedge", "downarrow", "downarrow", "downdownarrows",
7708 "downdownarrows", "downharpoonleft", "downharpoonleft", "downharpoonright", "downharpoonright", "drbkarow", "drbkarow", "drcorn", "drcorn", "drcrop", "drcrop", "dscr", "dscr", "dscy", "dscy", "dsol", "dsol", "dstrok", "dstrok", "dtdot", "dtdot", "dtri", "dtri", "dtrif", "dtrif", "duarr", "duarr", "duhar", "duhar", "dwangle",
7709 "dwangle", "dzcy", "dzcy", "dzigrarr", "dzigrarr", "eDDot", "eDDot", "eDot", "eDot", "eacute", "eacute", "easter", "easter", "ecaron", "ecaron", "ecir", "ecir", "ecirc", "ecirc", "ecolon", "ecolon", "ecy", "ecy", "edot", "edot", "ee", "ee", "efDot", "efDot", "efr", "efr", "eg", "eg", "egrave", "egrave", "egs", "egs", "egsdot",
7710 "egsdot", "el", "el", "elinters", "elinters", "ell", "ell", "els", "els", "elsdot", "elsdot", "emacr", "emacr", "empty", "empty", "emptyset", "emptyset", "emptyv", "emptyv", "emsp", "emsp", "emsp13", "emsp13", "emsp14", "emsp14", "eng", "eng", "ensp", "ensp", "eogon", "eogon", "eopf", "eopf", "epar", "epar", "eparsl",
7711 "eparsl", "eplus", "eplus", "epsi", "epsi", "epsilon", "epsilon", "epsiv", "epsiv", "eqcirc", "eqcirc", "eqcolon", "eqcolon", "eqsim", "eqsim", "eqslantgtr", "eqslantgtr", "eqslantless", "eqslantless", "equals", "equals", "equest", "equest", "equiv", "equiv", "equivDD", "equivDD", "eqvparsl", "eqvparsl", "erDot", "erDot",
7712 "erarr", "erarr", "escr", "escr", "esdot", "esdot", "esim", "esim", "eta", "eta", "eth", "eth", "euml", "euml", "euro", "euro", "excl", "excl", "exist", "exist", "expectation", "expectation", "exponentiale", "exponentiale", "fallingdotseq", "fallingdotseq", "fcy", "fcy", "female", "female", "ffilig", "ffilig", "fflig",
7713 "fflig", "ffllig", "ffllig", "ffr", "ffr", "filig", "filig", "flat", "flat", "fllig", "fllig", "fltns", "fltns", "fnof", "fnof", "fopf", "fopf", "forall", "forall", "fork", "fork", "forkv", "forkv", "fpartint", "fpartint", "frac12", "frac12", "frac13", "frac13", "frac14", "frac14", "frac15", "frac15", "frac16", "frac16",
7714 "frac18", "frac18", "frac23", "frac23", "frac25", "frac25", "frac34", "frac34", "frac35", "frac35", "frac38", "frac38", "frac45", "frac45", "frac56", "frac56", "frac58", "frac58", "frac78", "frac78", "frasl", "frasl", "frown", "frown", "fscr", "fscr", "gE", "gE", "gEl", "gEl", "gacute", "gacute", "gamma", "gamma", "gammad",
7715 "gammad", "gap", "gap", "gbreve", "gbreve", "gcirc", "gcirc", "gcy", "gcy", "gdot", "gdot", "ge", "ge", "gel", "gel", "geq", "geq", "geqq", "geqq", "geqslant", "geqslant", "ges", "ges", "gescc", "gescc", "gesdot", "gesdot", "gesdoto", "gesdoto", "gesdotol", "gesdotol", "gesles", "gesles", "gfr", "gfr", "gg", "gg", "ggg",
7716 "ggg", "gimel", "gimel", "gjcy", "gjcy", "gl", "gl", "glE", "glE", "gla", "gla", "glj", "glj", "gnE", "gnE", "gnap", "gnap", "gnapprox", "gnapprox", "gne", "gne", "gneq", "gneq", "gneqq", "gneqq", "gnsim", "gnsim", "gopf", "gopf", "grave", "grave", "gscr", "gscr", "gsim", "gsim", "gsime", "gsime", "gsiml", "gsiml", "gtcc",
7717 "gtcc", "gtcir", "gtcir", "gtdot", "gtdot", "gtlPar", "gtlPar", "gtquest", "gtquest", "gtrapprox", "gtrapprox", "gtrarr", "gtrarr", "gtrdot", "gtrdot", "gtreqless", "gtreqless", "gtreqqless", "gtreqqless", "gtrless", "gtrless", "gtrsim", "gtrsim", "hArr", "hArr", "hairsp", "hairsp", "half", "half", "hamilt", "hamilt",
7718 "hardcy", "hardcy", "harr", "harr", "harrcir", "harrcir", "harrw", "harrw", "hbar", "hbar", "hcirc", "hcirc", "hearts", "hearts", "heartsuit", "heartsuit", "hellip", "hellip", "hercon", "hercon", "hfr", "hfr", "hksearow", "hksearow", "hkswarow", "hkswarow", "hoarr", "hoarr", "homtht", "homtht", "hookleftarrow", "hookleftarrow",
7719 "hookrightarrow", "hookrightarrow", "hopf", "hopf", "horbar", "horbar", "hscr", "hscr", "hslash", "hslash", "hstrok", "hstrok", "hybull", "hybull", "hyphen", "hyphen", "iacute", "iacute", "ic", "ic", "icirc", "icirc", "icy", "icy", "iecy", "iecy", "iexcl", "iexcl", "iff", "iff", "ifr", "ifr", "igrave", "igrave", "ii",
7720 "ii", "iiiint", "iiiint", "iiint", "iiint", "iinfin", "iinfin", "iiota", "iiota", "ijlig", "ijlig", "imacr", "imacr", "image", "image", "imagline", "imagline", "imagpart", "imagpart", "imath", "imath", "imof", "imof", "imped", "imped", "in", "in", "incare", "incare", "infin", "infin", "infintie", "infintie", "inodot",
7721 "inodot", "int", "int", "intcal", "intcal", "integers", "integers", "intercal", "intercal", "intlarhk", "intlarhk", "intprod", "intprod", "iocy", "iocy", "iogon", "iogon", "iopf", "iopf", "iota", "iota", "iprod", "iprod", "iquest", "iquest", "iscr", "iscr", "isin", "isin", "isinE", "isinE", "isindot", "isindot", "isins",
7722 "isins", "isinsv", "isinsv", "isinv", "isinv", "it", "it", "itilde", "itilde", "iukcy", "iukcy", "iuml", "iuml", "jcirc", "jcirc", "jcy", "jcy", "jfr", "jfr", "jmath", "jmath", "jopf", "jopf", "jscr", "jscr", "jsercy", "jsercy", "jukcy", "jukcy", "kappa", "kappa", "kappav", "kappav", "kcedil", "kcedil", "kcy", "kcy", "kfr",
7723 "kfr", "kgreen", "kgreen", "khcy", "khcy", "kjcy", "kjcy", "kopf", "kopf", "kscr", "kscr", "lAarr", "lAarr", "lArr", "lArr", "lAtail", "lAtail", "lBarr", "lBarr", "lE", "lE", "lEg", "lEg", "lHar", "lHar", "lacute", "lacute", "laemptyv", "laemptyv", "lagran", "lagran", "lambda", "lambda", "lang", "lang", "langd", "langd",
7724 "langle", "langle", "lap", "lap", "laquo", "laquo", "larr", "larr", "larrb", "larrb", "larrbfs", "larrbfs", "larrfs", "larrfs", "larrhk", "larrhk", "larrlp", "larrlp", "larrpl", "larrpl", "larrsim", "larrsim", "larrtl", "larrtl", "lat", "lat", "latail", "latail", "late", "late", "lbarr", "lbarr", "lbbrk", "lbbrk", "lbrace",
7725 "lbrace", "lbrack", "lbrack", "lbrke", "lbrke", "lbrksld", "lbrksld", "lbrkslu", "lbrkslu", "lcaron", "lcaron", "lcedil", "lcedil", "lceil", "lceil", "lcub", "lcub", "lcy", "lcy", "ldca", "ldca", "ldquo", "ldquo", "ldquor", "ldquor", "ldrdhar", "ldrdhar", "ldrushar", "ldrushar", "ldsh", "ldsh", "le", "le", "leftarrow",
7726 "leftarrow", "leftarrowtail", "leftarrowtail", "leftharpoondown", "leftharpoondown", "leftharpoonup", "leftharpoonup", "leftleftarrows", "leftleftarrows", "leftrightarrow", "leftrightarrow", "leftrightarrows", "leftrightarrows", "leftrightharpoons", "leftrightharpoons", "leftrightsquigarrow", "leftrightsquigarrow", "leftthreetimes",
7727 "leftthreetimes", "leg", "leg", "leq", "leq", "leqq", "leqq", "leqslant", "leqslant", "les", "les", "lescc", "lescc", "lesdot", "lesdot", "lesdoto", "lesdoto", "lesdotor", "lesdotor", "lesges", "lesges", "lessapprox", "lessapprox", "lessdot", "lessdot", "lesseqgtr", "lesseqgtr", "lesseqqgtr", "lesseqqgtr", "lessgtr", "lessgtr",
7728 "lesssim", "lesssim", "lfisht", "lfisht", "lfloor", "lfloor", "lfr", "lfr", "lg", "lg", "lgE", "lgE", "lhard", "lhard", "lharu", "lharu", "lharul", "lharul", "lhblk", "lhblk", "ljcy", "ljcy", "ll", "ll", "llarr", "llarr", "llcorner", "llcorner", "llhard", "llhard", "lltri", "lltri", "lmidot", "lmidot", "lmoust", "lmoust",
7729 "lmoustache", "lmoustache", "lnE", "lnE", "lnap", "lnap", "lnapprox", "lnapprox", "lne", "lne", "lneq", "lneq", "lneqq", "lneqq", "lnsim", "lnsim", "loang", "loang", "loarr", "loarr", "lobrk", "lobrk", "longleftarrow", "longleftarrow", "longleftrightarrow", "longleftrightarrow", "longmapsto", "longmapsto", "longrightarrow",
7730 "longrightarrow", "looparrowleft", "looparrowleft", "looparrowright", "looparrowright", "lopar", "lopar", "lopf", "lopf", "loplus", "loplus", "lotimes", "lotimes", "lowast", "lowast", "lowbar", "lowbar", "loz", "loz", "lozenge", "lozenge", "lozf", "lozf", "lpar", "lpar", "lparlt", "lparlt", "lrarr", "lrarr", "lrcorner",
7731 "lrcorner", "lrhar", "lrhar", "lrhard", "lrhard", "lrm", "lrm", "lrtri", "lrtri", "lsaquo", "lsaquo", "lscr", "lscr", "lsh", "lsh", "lsim", "lsim", "lsime", "lsime", "lsimg", "lsimg", "lsqb", "lsqb", "lsquo", "lsquo", "lsquor", "lsquor", "lstrok", "lstrok", "ltcc", "ltcc", "ltcir", "ltcir", "ltdot", "ltdot", "lthree",
7732 "lthree", "ltimes", "ltimes", "ltlarr", "ltlarr", "ltquest", "ltquest", "ltrPar", "ltrPar", "ltri", "ltri", "ltrie", "ltrie", "ltrif", "ltrif", "lurdshar", "lurdshar", "luruhar", "luruhar", "mDDot", "mDDot", "macr", "macr", "male", "male", "malt", "malt", "maltese", "maltese", "map", "map", "mapsto", "mapsto", "mapstodown",
7733 "mapstodown", "mapstoleft", "mapstoleft", "mapstoup", "mapstoup", "marker", "marker", "mcomma", "mcomma", "mcy", "mcy", "mdash", "mdash", "measuredangle", "measuredangle", "mfr", "mfr", "mho", "mho", "micro", "micro", "mid", "mid", "midast", "midast", "midcir", "midcir", "middot", "middot", "minus", "minus", "minusb",
7734 "minusb", "minusd", "minusd", "minusdu", "minusdu", "mlcp", "mlcp", "mldr", "mldr", "mnplus", "mnplus", "models", "models", "mopf", "mopf", "mp", "mp", "mscr", "mscr", "mstpos", "mstpos", "mu", "mu", "multimap", "multimap", "mumap", "mumap", "nLeftarrow", "nLeftarrow", "nLeftrightarrow", "nLeftrightarrow", "nRightarrow",
7735 "nRightarrow", "nVDash", "nVDash", "nVdash", "nVdash", "nabla", "nabla", "nacute", "nacute", "nap", "nap", "napos", "napos", "napprox", "napprox", "natur", "natur", "natural", "natural", "naturals", "naturals", "nbsp", "nbsp", "ncap", "ncap", "ncaron", "ncaron", "ncedil", "ncedil", "ncong", "ncong", "ncup", "ncup", "ncy",
7736 "ncy", "ndash", "ndash", "ne", "ne", "neArr", "neArr", "nearhk", "nearhk", "nearr", "nearr", "nearrow", "nearrow", "nequiv", "nequiv", "nesear", "nesear", "nexist", "nexist", "nexists", "nexists", "nfr", "nfr", "nge", "nge", "ngeq", "ngeq", "ngsim", "ngsim", "ngt", "ngt", "ngtr", "ngtr", "nhArr", "nhArr", "nharr", "nharr",
7737 "nhpar", "nhpar", "ni", "ni", "nis", "nis", "nisd", "nisd", "niv", "niv", "njcy", "njcy", "nlArr", "nlArr", "nlarr", "nlarr", "nldr", "nldr", "nle", "nle", "nleftarrow", "nleftarrow", "nleftrightarrow", "nleftrightarrow", "nleq", "nleq", "nless", "nless", "nlsim", "nlsim", "nlt", "nlt", "nltri", "nltri", "nltrie", "nltrie",
7738 "nmid", "nmid", "nopf", "nopf", "not", "not", "notin", "notin", "notinva", "notinva", "notinvb", "notinvb", "notinvc", "notinvc", "notni", "notni", "notniva", "notniva", "notnivb", "notnivb", "notnivc", "notnivc", "npar", "npar", "nparallel", "nparallel", "npolint", "npolint", "npr", "npr", "nprcue", "nprcue", "nprec",
7739 "nprec", "nrArr", "nrArr", "nrarr", "nrarr", "nrightarrow", "nrightarrow", "nrtri", "nrtri", "nrtrie", "nrtrie", "nsc", "nsc", "nsccue", "nsccue", "nscr", "nscr", "nshortmid", "nshortmid", "nshortparallel", "nshortparallel", "nsim", "nsim", "nsime", "nsime", "nsimeq", "nsimeq", "nsmid", "nsmid", "nspar", "nspar", "nsqsube",
7740 "nsqsube", "nsqsupe", "nsqsupe", "nsub", "nsub", "nsube", "nsube", "nsubseteq", "nsubseteq", "nsucc", "nsucc", "nsup", "nsup", "nsupe", "nsupe", "nsupseteq", "nsupseteq", "ntgl", "ntgl", "ntilde", "ntilde", "ntlg", "ntlg", "ntriangleleft", "ntriangleleft", "ntrianglelefteq", "ntrianglelefteq", "ntriangleright", "ntriangleright",
7741 "ntrianglerighteq", "ntrianglerighteq", "nu", "nu", "num", "num", "numero", "numero", "numsp", "numsp", "nvDash", "nvDash", "nvHarr", "nvHarr", "nvdash", "nvdash", "nvinfin", "nvinfin", "nvlArr", "nvlArr", "nvrArr", "nvrArr", "nwArr", "nwArr", "nwarhk", "nwarhk", "nwarr", "nwarr", "nwarrow", "nwarrow", "nwnear", "nwnear",
7742 "oS", "oS", "oacute", "oacute", "oast", "oast", "ocir", "ocir", "ocirc", "ocirc", "ocy", "ocy", "odash", "odash", "odblac", "odblac", "odiv", "odiv", "odot", "odot", "odsold", "odsold", "oelig", "oelig", "ofcir", "ofcir", "ofr", "ofr", "ogon", "ogon", "ograve", "ograve", "ogt", "ogt", "ohbar", "ohbar", "ohm", "ohm", "oint",
7743 "oint", "olarr", "olarr", "olcir", "olcir", "olcross", "olcross", "oline", "oline", "olt", "olt", "omacr", "omacr", "omega", "omega", "omicron", "omicron", "omid", "omid", "ominus", "ominus", "oopf", "oopf", "opar", "opar", "operp", "operp", "oplus", "oplus", "or", "or", "orarr", "orarr", "ord", "ord", "order", "order",
7744 "orderof", "orderof", "ordf", "ordf", "ordm", "ordm", "origof", "origof", "oror", "oror", "orslope", "orslope", "orv", "orv", "oscr", "oscr", "oslash", "oslash", "osol", "osol", "otilde", "otilde", "otimes", "otimes", "otimesas", "otimesas", "ouml", "ouml", "ovbar", "ovbar", "par", "par", "para", "para", "parallel", "parallel",
7745 "parsim", "parsim", "parsl", "parsl", "part", "part", "pcy", "pcy", "percnt", "percnt", "period", "period", "permil", "permil", "perp", "perp", "pertenk", "pertenk", "pfr", "pfr", "phi", "phi", "phiv", "phiv", "phmmat", "phmmat", "phone", "phone", "pi", "pi", "pitchfork", "pitchfork", "piv", "piv", "planck", "planck",
7746 "planckh", "planckh", "plankv", "plankv", "plus", "plus", "plusacir", "plusacir", "plusb", "plusb", "pluscir", "pluscir", "plusdo", "plusdo", "plusdu", "plusdu", "pluse", "pluse", "plusmn", "plusmn", "plussim", "plussim", "plustwo", "plustwo", "pm", "pm", "pointint", "pointint", "popf", "popf", "pound", "pound", "pr",
7747 "pr", "prE", "prE", "prap", "prap", "prcue", "prcue", "pre", "pre", "prec", "prec", "precapprox", "precapprox", "preccurlyeq", "preccurlyeq", "preceq", "preceq", "precnapprox", "precnapprox", "precneqq", "precneqq", "precnsim", "precnsim", "precsim", "precsim", "prime", "prime", "primes", "primes", "prnE", "prnE", "prnap",
7748 "prnap", "prnsim", "prnsim", "prod", "prod", "profalar", "profalar", "profline", "profline", "profsurf", "profsurf", "prop", "prop", "propto", "propto", "prsim", "prsim", "prurel", "prurel", "pscr", "pscr", "psi", "psi", "puncsp", "puncsp", "qfr", "qfr", "qint", "qint", "qopf", "qopf", "qprime", "qprime", "qscr", "qscr",
7749 "quaternions", "quaternions", "quatint", "quatint", "quest", "quest", "questeq", "questeq", "rAarr", "rAarr", "rArr", "rArr", "rAtail", "rAtail", "rBarr", "rBarr", "rHar", "rHar", "racute", "racute", "radic", "radic", "raemptyv", "raemptyv", "rang", "rang", "rangd", "rangd", "range", "range", "rangle", "rangle", "raquo",
7750 "raquo", "rarr", "rarr", "rarrap", "rarrap", "rarrb", "rarrb", "rarrbfs", "rarrbfs", "rarrc", "rarrc", "rarrfs", "rarrfs", "rarrhk", "rarrhk", "rarrlp", "rarrlp", "rarrpl", "rarrpl", "rarrsim", "rarrsim", "rarrtl", "rarrtl", "rarrw", "rarrw", "ratail", "ratail", "ratio", "ratio", "rationals", "rationals", "rbarr", "rbarr",
7751 "rbbrk", "rbbrk", "rbrace", "rbrace", "rbrack", "rbrack", "rbrke", "rbrke", "rbrksld", "rbrksld", "rbrkslu", "rbrkslu", "rcaron", "rcaron", "rcedil", "rcedil", "rceil", "rceil", "rcub", "rcub", "rcy", "rcy", "rdca", "rdca", "rdldhar", "rdldhar", "rdquo", "rdquo", "rdquor", "rdquor", "rdsh", "rdsh", "real", "real", "realine",
7752 "realine", "realpart", "realpart", "reals", "reals", "rect", "rect", "reg", "reg", "rfisht", "rfisht", "rfloor", "rfloor", "rfr", "rfr", "rhard", "rhard", "rharu", "rharu", "rharul", "rharul", "rho", "rho", "rhov", "rhov", "rightarrow", "rightarrow", "rightarrowtail", "rightarrowtail", "rightharpoondown", "rightharpoondown",
7753 "rightharpoonup", "rightharpoonup", "rightleftarrows", "rightleftarrows", "rightleftharpoons", "rightleftharpoons", "rightrightarrows", "rightrightarrows", "rightsquigarrow", "rightsquigarrow", "rightthreetimes", "rightthreetimes", "ring", "ring", "risingdotseq", "risingdotseq", "rlarr", "rlarr", "rlhar", "rlhar", "rlm",
7754 "rlm", "rmoust", "rmoust", "rmoustache", "rmoustache", "rnmid", "rnmid", "roang", "roang", "roarr", "roarr", "robrk", "robrk", "ropar", "ropar", "ropf", "ropf", "roplus", "roplus", "rotimes", "rotimes", "rpar", "rpar", "rpargt", "rpargt", "rppolint", "rppolint", "rrarr", "rrarr", "rsaquo", "rsaquo", "rscr", "rscr", "rsh",
7755 "rsh", "rsqb", "rsqb", "rsquo", "rsquo", "rsquor", "rsquor", "rthree", "rthree", "rtimes", "rtimes", "rtri", "rtri", "rtrie", "rtrie", "rtrif", "rtrif", "rtriltri", "rtriltri", "ruluhar", "ruluhar", "rx", "rx", "sacute", "sacute", "sbquo", "sbquo", "sc", "sc", "scE", "scE", "scap", "scap", "scaron", "scaron", "sccue",
7756 "sccue", "sce", "sce", "scedil", "scedil", "scirc", "scirc", "scnE", "scnE", "scnap", "scnap", "scnsim", "scnsim", "scpolint", "scpolint", "scsim", "scsim", "scy", "scy", "sdot", "sdot", "sdotb", "sdotb", "sdote", "sdote", "seArr", "seArr", "searhk", "searhk", "searr", "searr", "searrow", "searrow", "sect", "sect", "semi",
7757 "semi", "seswar", "seswar", "setminus", "setminus", "setmn", "setmn", "sext", "sext", "sfr", "sfr", "sfrown", "sfrown", "sharp", "sharp", "shchcy", "shchcy", "shcy", "shcy", "shortmid", "shortmid", "shortparallel", "shortparallel", "shy", "shy", "sigma", "sigma", "sigmaf", "sigmaf", "sigmav", "sigmav", "sim", "sim", "simdot",
7758 "simdot", "sime", "sime", "simeq", "simeq", "simg", "simg", "simgE", "simgE", "siml", "siml", "simlE", "simlE", "simne", "simne", "simplus", "simplus", "simrarr", "simrarr", "slarr", "slarr", "smallsetminus", "smallsetminus", "smashp", "smashp", "smeparsl", "smeparsl", "smid", "smid", "smile", "smile", "smt", "smt", "smte",
7759 "smte", "softcy", "softcy", "sol", "sol", "solb", "solb", "solbar", "solbar", "sopf", "sopf", "spades", "spades", "spadesuit", "spadesuit", "spar", "spar", "sqcap", "sqcap", "sqcup", "sqcup", "sqsub", "sqsub", "sqsube", "sqsube", "sqsubset", "sqsubset", "sqsubseteq", "sqsubseteq", "sqsup", "sqsup", "sqsupe", "sqsupe",
7760 "sqsupset", "sqsupset", "sqsupseteq", "sqsupseteq", "squ", "squ", "square", "square", "squarf", "squarf", "squf", "squf", "srarr", "srarr", "sscr", "sscr", "ssetmn", "ssetmn", "ssmile", "ssmile", "sstarf", "sstarf", "star", "star", "starf", "starf", "straightepsilon", "straightepsilon", "straightphi", "straightphi", "strns",
7761 "strns", "sub", "sub", "subE", "subE", "subdot", "subdot", "sube", "sube", "subedot", "subedot", "submult", "submult", "subnE", "subnE", "subne", "subne", "subplus", "subplus", "subrarr", "subrarr", "subset", "subset", "subseteq", "subseteq", "subseteqq", "subseteqq", "subsetneq", "subsetneq", "subsetneqq", "subsetneqq",
7762 "subsim", "subsim", "subsub", "subsub", "subsup", "subsup", "succ", "succ", "succapprox", "succapprox", "succcurlyeq", "succcurlyeq", "succeq", "succeq", "succnapprox", "succnapprox", "succneqq", "succneqq", "succnsim", "succnsim", "succsim", "succsim", "sum", "sum", "sung", "sung", "sup", "sup", "sup1", "sup1", "sup2",
7763 "sup2", "sup3", "sup3", "supE", "supE", "supdot", "supdot", "supdsub", "supdsub", "supe", "supe", "supedot", "supedot", "suphsol", "suphsol", "suphsub", "suphsub", "suplarr", "suplarr", "supmult", "supmult", "supnE", "supnE", "supne", "supne", "supplus", "supplus", "supset", "supset", "supseteq", "supseteq", "supseteqq",
7764 "supseteqq", "supsetneq", "supsetneq", "supsetneqq", "supsetneqq", "supsim", "supsim", "supsub", "supsub", "supsup", "supsup", "swArr", "swArr", "swarhk", "swarhk", "swarr", "swarr", "swarrow", "swarrow", "swnwar", "swnwar", "szlig", "szlig", "target", "target", "tau", "tau", "tbrk", "tbrk", "tcaron", "tcaron", "tcedil",
7765 "tcedil", "tcy", "tcy", "tdot", "tdot", "telrec", "telrec", "tfr", "tfr", "there4", "there4", "therefore", "therefore", "theta", "theta", "thetasym", "thetasym", "thetav", "thetav", "thickapprox", "thickapprox", "thicksim", "thicksim", "thinsp", "thinsp", "thkap", "thkap", "thksim", "thksim", "thorn", "thorn", "tilde",
7766 "tilde", "times", "times", "timesb", "timesb", "timesbar", "timesbar", "timesd", "timesd", "tint", "tint", "toea", "toea", "top", "top", "topbot", "topbot", "topcir", "topcir", "topf", "topf", "topfork", "topfork", "tosa", "tosa", "tprime", "tprime", "trade", "trade", "triangle", "triangle", "triangledown", "triangledown",
7767 "triangleleft", "triangleleft", "trianglelefteq", "trianglelefteq", "triangleq", "triangleq", "triangleright", "triangleright", "trianglerighteq", "trianglerighteq", "tridot", "tridot", "trie", "trie", "triminus", "triminus", "triplus", "triplus", "trisb", "trisb", "tritime", "tritime", "trpezium", "trpezium", "tscr",
7768 "tscr", "tscy", "tscy", "tshcy", "tshcy", "tstrok", "tstrok", "twixt", "twixt", "twoheadleftarrow", "twoheadleftarrow", "twoheadrightarrow", "twoheadrightarrow", "uArr", "uArr", "uHar", "uHar", "uacute", "uacute", "uarr", "uarr", "ubrcy", "ubrcy", "ubreve", "ubreve", "ucirc", "ucirc", "ucy", "ucy", "udarr", "udarr", "udblac",
7769 "udblac", "udhar", "udhar", "ufisht", "ufisht", "ufr", "ufr", "ugrave", "ugrave", "uharl", "uharl", "uharr", "uharr", "uhblk", "uhblk", "ulcorn", "ulcorn", "ulcorner", "ulcorner", "ulcrop", "ulcrop", "ultri", "ultri", "umacr", "umacr", "uml", "uml", "uogon", "uogon", "uopf", "uopf", "uparrow", "uparrow", "updownarrow",
7770 "updownarrow", "upharpoonleft", "upharpoonleft", "upharpoonright", "upharpoonright", "uplus", "uplus", "upsi", "upsi", "upsih", "upsih", "upsilon", "upsilon", "upuparrows", "upuparrows", "urcorn", "urcorn", "urcorner", "urcorner", "urcrop", "urcrop", "uring", "uring", "urtri", "urtri", "uscr", "uscr", "utdot", "utdot",
7771 "utilde", "utilde", "utri", "utri", "utrif", "utrif", "uuarr", "uuarr", "uuml", "uuml", "uwangle", "uwangle", "vArr", "vArr", "vBar", "vBar", "vBarv", "vBarv", "vDash", "vDash", "vangrt", "vangrt", "varepsilon", "varepsilon", "varkappa", "varkappa", "varnothing", "varnothing", "varphi", "varphi", "varpi", "varpi", "varpropto",
7772 "varpropto", "varr", "varr", "varrho", "varrho", "varsigma", "varsigma", "vartheta", "vartheta", "vartriangleleft", "vartriangleleft", "vartriangleright", "vartriangleright", "vcy", "vcy", "vdash", "vdash", "vee", "vee", "veebar", "veebar", "veeeq", "veeeq", "vellip", "vellip", "verbar", "verbar", "vert", "vert", "vfr",
7773 "vfr", "vltri", "vltri", "vopf", "vopf", "vprop", "vprop", "vrtri", "vrtri", "vscr", "vscr", "vzigzag", "vzigzag", "wcirc", "wcirc", "wedbar", "wedbar", "wedge", "wedge", "wedgeq", "wedgeq", "weierp", "weierp", "wfr", "wfr", "wopf", "wopf", "wp", "wp", "wr", "wr", "wreath", "wreath", "wscr", "wscr", "xcap", "xcap", "xcirc",
7774 "xcirc", "xcup", "xcup", "xdtri", "xdtri", "xfr", "xfr", "xhArr", "xhArr", "xharr", "xharr", "xi", "xi", "xlArr", "xlArr", "xlarr", "xlarr", "xmap", "xmap", "xnis", "xnis", "xodot", "xodot", "xopf", "xopf", "xoplus", "xoplus", "xotime", "xotime", "xrArr", "xrArr", "xrarr", "xrarr", "xscr", "xscr", "xsqcup", "xsqcup", "xuplus",
7775 "xuplus", "xutri", "xutri", "xvee", "xvee", "xwedge", "xwedge", "yacute", "yacute", "yacy", "yacy", "ycirc", "ycirc", "ycy", "ycy", "yen", "yen", "yfr", "yfr", "yicy", "yicy", "yopf", "yopf", "yscr", "yscr", "yucy", "yucy", "yuml", "yuml", "zacute", "zacute", "zcaron", "zcaron", "zcy", "zcy", "zdot", "zdot", "zeetrf",
7776 "zeetrf", "zeta", "zeta", "zfr", "zfr", "zhcy", "zhcy", "zigrarr", "zigrarr", "zopf", "zopf", "zscr", "zscr", "zwj", "zwj", "zwnj", "zwnj", ];
7777 
7778 immutable dchar[] availableEntitiesValues =
7779 ['\u00c6', '\u00c6', '\u0026', '\u0026', '\u00c1', '\u00c1', '\u0102', '\u0102', '\u00c2', '\u00c2', '\u0410', '\u0410', '\U0001d504', '\U0001d504', '\u00c0', '\u00c0', '\u0391', '\u0391', '\u0100', '\u0100', '\u2a53', '\u2a53', '\u0104', '\u0104', '\U0001d538', '\U0001d538', '\u2061', '\u2061', '\u00c5', '\u00c5', '\U0001d49c', '\U0001d49c', '\u2254', '\u2254', '\u00c3',
7780 '\u00c3', '\u00c4', '\u00c4', '\u2216', '\u2216', '\u2ae7', '\u2ae7', '\u2306', '\u2306', '\u0411', '\u0411', '\u2235', '\u2235', '\u212c', '\u212c', '\u0392', '\u0392', '\U0001d505', '\U0001d505', '\U0001d539', '\U0001d539', '\u02d8', '\u02d8', '\u212c', '\u212c', '\u224e', '\u224e', '\u0427', '\u0427', '\u00a9', '\u00a9', '\u0106', '\u0106', '\u22d2', '\u22d2', '\u2145',
7781 '\u2145', '\u212d', '\u212d', '\u010c', '\u010c', '\u00c7', '\u00c7', '\u0108', '\u0108', '\u2230', '\u2230', '\u010a', '\u010a', '\u00b8', '\u00b8', '\u00b7', '\u00b7', '\u212d', '\u212d', '\u03a7', '\u03a7', '\u2299', '\u2299', '\u2296', '\u2296', '\u2295', '\u2295', '\u2297', '\u2297',
7782 '\u2232', '\u2232', '\u201d', '\u201d', '\u2019', '\u2019', '\u2237', '\u2237', '\u2a74', '\u2a74', '\u2261', '\u2261', '\u222f', '\u222f', '\u222e', '\u222e', '\u2102', '\u2102', '\u2210', '\u2210', '\u2233',
7783 '\u2233', '\u2a2f', '\u2a2f', '\U0001d49e', '\U0001d49e', '\u22d3', '\u22d3', '\u224d', '\u224d', '\u2145', '\u2145', '\u2911', '\u2911', '\u0402', '\u0402', '\u0405', '\u0405', '\u040f', '\u040f', '\u2021', '\u2021', '\u21a1', '\u21a1', '\u2ae4', '\u2ae4', '\u010e', '\u010e', '\u0414', '\u0414', '\u2207', '\u2207', '\u0394', '\u0394', '\U0001d507', '\U0001d507',
7784 '\u00b4', '\u00b4', '\u02d9', '\u02d9', '\u02dd', '\u02dd', '\u0060', '\u0060', '\u02dc', '\u02dc', '\u22c4', '\u22c4', '\u2146', '\u2146', '\U0001d53b', '\U0001d53b', '\u00a8', '\u00a8', '\u20dc', '\u20dc', '\u2250',
7785 '\u2250', '\u222f', '\u222f', '\u00a8', '\u00a8', '\u21d3', '\u21d3', '\u21d0', '\u21d0', '\u21d4', '\u21d4', '\u2ae4', '\u2ae4', '\u27f8', '\u27f8', '\u27fa',
7786 '\u27fa', '\u27f9', '\u27f9', '\u21d2', '\u21d2', '\u22a8', '\u22a8', '\u21d1', '\u21d1', '\u21d5', '\u21d5', '\u2225', '\u2225', '\u2193', '\u2193', '\u2913', '\u2913',
7787 '\u21f5', '\u21f5', '\u0311', '\u0311', '\u2950', '\u2950', '\u295e', '\u295e', '\u21bd', '\u21bd', '\u2956', '\u2956', '\u295f', '\u295f', '\u21c1', '\u21c1', '\u2957',
7788 '\u2957', '\u22a4', '\u22a4', '\u21a7', '\u21a7', '\u21d3', '\u21d3', '\U0001d49f', '\U0001d49f', '\u0110', '\u0110', '\u014a', '\u014a', '\u00d0', '\u00d0', '\u00c9', '\u00c9', '\u011a', '\u011a', '\u00ca', '\u00ca', '\u042d', '\u042d', '\u0116', '\u0116', '\U0001d508', '\U0001d508', '\u00c8', '\u00c8', '\u2208', '\u2208', '\u0112', '\u0112',
7789 '\u25fb', '\u25fb', '\u25ab', '\u25ab', '\u0118', '\u0118', '\U0001d53c', '\U0001d53c', '\u0395', '\u0395', '\u2a75', '\u2a75', '\u2242', '\u2242', '\u21cc', '\u21cc', '\u2130', '\u2130', '\u2a73', '\u2a73', '\u0397', '\u0397', '\u00cb', '\u00cb', '\u2203', '\u2203', '\u2147', '\u2147',
7790 '\u0424', '\u0424', '\U0001d509', '\U0001d509', '\u25fc', '\u25fc', '\u25aa', '\u25aa', '\U0001d53d', '\U0001d53d', '\u2200', '\u2200', '\u2131', '\u2131', '\u2131', '\u2131', '\u0403', '\u0403', '\u003e', '\u003e', '\u0393', '\u0393', '\u03dc', '\u03dc', '\u011e', '\u011e', '\u0122', '\u0122', '\u011c', '\u011c',
7791 '\u0413', '\u0413', '\u0120', '\u0120', '\U0001d50a', '\U0001d50a', '\u22d9', '\u22d9', '\U0001d53e', '\U0001d53e', '\u2265', '\u2265', '\u22db', '\u22db', '\u2267', '\u2267', '\u2aa2', '\u2aa2', '\u2277', '\u2277', '\u2a7e', '\u2a7e', '\u2273', '\u2273',
7792 '\U0001d4a2', '\U0001d4a2', '\u226b', '\u226b', '\u042a', '\u042a', '\u02c7', '\u02c7', '\u005e', '\u005e', '\u0124', '\u0124', '\u210c', '\u210c', '\u210b', '\u210b', '\u210d', '\u210d', '\u2500', '\u2500', '\u210b', '\u210b', '\u0126', '\u0126', '\u224e', '\u224e', '\u224f', '\u224f', '\u0415', '\u0415', '\u0132', '\u0132',
7793 '\u0401', '\u0401', '\u00cd', '\u00cd', '\u00ce', '\u00ce', '\u0418', '\u0418', '\u0130', '\u0130', '\u2111', '\u2111', '\u00cc', '\u00cc', '\u2111', '\u2111', '\u012a', '\u012a', '\u2148', '\u2148', '\u21d2', '\u21d2', '\u222c', '\u222c', '\u222b', '\u222b', '\u22c2', '\u22c2', '\u2063', '\u2063', '\u2062',
7794 '\u2062', '\u012e', '\u012e', '\U0001d540', '\U0001d540', '\u0399', '\u0399', '\u2110', '\u2110', '\u0128', '\u0128', '\u0406', '\u0406', '\u00cf', '\u00cf', '\u0134', '\u0134', '\u0419', '\u0419', '\U0001d50d', '\U0001d50d', '\U0001d541', '\U0001d541', '\U0001d4a5', '\U0001d4a5', '\u0408', '\u0408', '\u0404', '\u0404', '\u0425', '\u0425', '\u040c', '\u040c', '\u039a', '\u039a', '\u0136', '\u0136',
7795 '\u041a', '\u041a', '\U0001d50e', '\U0001d50e', '\U0001d542', '\U0001d542', '\U0001d4a6', '\U0001d4a6', '\u0409', '\u0409', '\u003c', '\u003c', '\u0139', '\u0139', '\u039b', '\u039b', '\u27ea', '\u27ea', '\u2112', '\u2112', '\u219e', '\u219e', '\u013d', '\u013d', '\u013b', '\u013b', '\u041b', '\u041b', '\u27e8', '\u27e8', '\u2190', '\u2190', '\u21e4',
7796 '\u21e4', '\u21c6', '\u21c6', '\u2308', '\u2308', '\u27e6', '\u27e6', '\u2961', '\u2961', '\u21c3', '\u21c3', '\u2959', '\u2959', '\u230a', '\u230a', '\u2194', '\u2194', '\u294e',
7797 '\u294e', '\u22a3', '\u22a3', '\u21a4', '\u21a4', '\u295a', '\u295a', '\u22b2', '\u22b2', '\u29cf', '\u29cf', '\u22b4', '\u22b4', '\u2951', '\u2951', '\u2960', '\u2960', '\u21bf', '\u21bf',
7798 '\u2958', '\u2958', '\u21bc', '\u21bc', '\u2952', '\u2952', '\u21d0', '\u21d0', '\u21d4', '\u21d4', '\u22da', '\u22da', '\u2266', '\u2266', '\u2276', '\u2276', '\u2aa1', '\u2aa1', '\u2a7d', '\u2a7d',
7799 '\u2272', '\u2272', '\U0001d50f', '\U0001d50f', '\u22d8', '\u22d8', '\u21da', '\u21da', '\u013f', '\u013f', '\u27f5', '\u27f5', '\u27f7', '\u27f7', '\u27f6', '\u27f6', '\u27f8', '\u27f8', '\u27fa', '\u27fa', '\u27f9', '\u27f9',
7800 '\U0001d543', '\U0001d543', '\u2199', '\u2199', '\u2198', '\u2198', '\u2112', '\u2112', '\u21b0', '\u21b0', '\u0141', '\u0141', '\u226a', '\u226a', '\u2905', '\u2905', '\u041c', '\u041c', '\u205f', '\u205f', '\u2133', '\u2133', '\U0001d510', '\U0001d510', '\u2213', '\u2213', '\U0001d544', '\U0001d544', '\u2133', '\u2133', '\u039c', '\u039c',
7801 '\u040a', '\u040a', '\u0143', '\u0143', '\u0147', '\u0147', '\u0145', '\u0145', '\u041d', '\u041d', '\u200b', '\u200b', '\u200b', '\u200b', '\u200b', '\u200b', '\u200b', '\u200b', '\u226b', '\u226b',
7802 '\u226a', '\u226a', '\u000a', '\u000a', '\U0001d511', '\U0001d511', '\u2060', '\u2060', '\u00a0', '\u00a0', '\u2115', '\u2115', '\u2aec', '\u2aec', '\u2262', '\u2262', '\u226d', '\u226d', '\u2226', '\u2226', '\u2209', '\u2209', '\u2260', '\u2260',
7803 '\u2204', '\u2204', '\u226f', '\u226f', '\u2271', '\u2271', '\u2279', '\u2279', '\u2275', '\u2275', '\u22ea', '\u22ea', '\u22ec', '\u22ec', '\u226e', '\u226e', '\u2270', '\u2270', '\u2278',
7804 '\u2278', '\u2274', '\u2274', '\u2280', '\u2280', '\u22e0', '\u22e0', '\u220c', '\u220c', '\u22eb', '\u22eb', '\u22ed', '\u22ed', '\u22e2', '\u22e2', '\u22e3',
7805 '\u22e3', '\u2288', '\u2288', '\u2281', '\u2281', '\u22e1', '\u22e1', '\u2289', '\u2289', '\u2241', '\u2241', '\u2244', '\u2244', '\u2247', '\u2247', '\u2249', '\u2249', '\u2224',
7806 '\u2224', '\U0001d4a9', '\U0001d4a9', '\u00d1', '\u00d1', '\u039d', '\u039d', '\u0152', '\u0152', '\u00d3', '\u00d3', '\u00d4', '\u00d4', '\u041e', '\u041e', '\u0150', '\u0150', '\U0001d512', '\U0001d512', '\u00d2', '\u00d2', '\u014c', '\u014c', '\u03a9', '\u03a9', '\u039f', '\u039f', '\U0001d546', '\U0001d546', '\u201c', '\u201c', '\u2018',
7807 '\u2018', '\u2a54', '\u2a54', '\U0001d4aa', '\U0001d4aa', '\u00d8', '\u00d8', '\u00d5', '\u00d5', '\u2a37', '\u2a37', '\u00d6', '\u00d6', '\u203e', '\u203e', '\u23de', '\u23de', '\u23b4', '\u23b4', '\u23dc', '\u23dc', '\u2202', '\u2202', '\u041f', '\u041f', '\U0001d513', '\U0001d513', '\u03a6', '\u03a6', '\u03a0', '\u03a0', '\u00b1',
7808 '\u00b1', '\u210c', '\u210c', '\u2119', '\u2119', '\u2abb', '\u2abb', '\u227a', '\u227a', '\u2aaf', '\u2aaf', '\u227c', '\u227c', '\u227e', '\u227e', '\u2033', '\u2033', '\u220f', '\u220f', '\u2237', '\u2237', '\u221d', '\u221d', '\U0001d4ab', '\U0001d4ab',
7809 '\u03a8', '\u03a8', '\u0022', '\u0022', '\U0001d514', '\U0001d514', '\u211a', '\u211a', '\U0001d4ac', '\U0001d4ac', '\u2910', '\u2910', '\u00ae', '\u00ae', '\u0154', '\u0154', '\u27eb', '\u27eb', '\u21a0', '\u21a0', '\u2916', '\u2916', '\u0158', '\u0158', '\u0156', '\u0156', '\u0420', '\u0420', '\u211c', '\u211c', '\u220b', '\u220b', '\u21cb', '\u21cb',
7810 '\u296f', '\u296f', '\u211c', '\u211c', '\u03a1', '\u03a1', '\u27e9', '\u27e9', '\u2192', '\u2192', '\u21e5', '\u21e5', '\u21c4', '\u21c4', '\u2309', '\u2309', '\u27e7', '\u27e7', '\u295d',
7811 '\u295d', '\u21c2', '\u21c2', '\u2955', '\u2955', '\u230b', '\u230b', '\u22a2', '\u22a2', '\u21a6', '\u21a6', '\u295b', '\u295b', '\u22b3', '\u22b3', '\u29d0', '\u29d0', '\u22b5',
7812 '\u22b5', '\u294f', '\u294f', '\u295c', '\u295c', '\u21be', '\u21be', '\u2954', '\u2954', '\u21c0', '\u21c0', '\u2953', '\u2953', '\u21d2', '\u21d2', '\u211d', '\u211d', '\u2970', '\u2970',
7813 '\u21db', '\u21db', '\u211b', '\u211b', '\u21b1', '\u21b1', '\u29f4', '\u29f4', '\u0429', '\u0429', '\u0428', '\u0428', '\u042c', '\u042c', '\u015a', '\u015a', '\u2abc', '\u2abc', '\u0160', '\u0160', '\u015e', '\u015e', '\u015c', '\u015c', '\u0421', '\u0421', '\U0001d516', '\U0001d516', '\u2193', '\u2193', '\u2190', '\u2190',
7814 '\u2192', '\u2192', '\u2191', '\u2191', '\u03a3', '\u03a3', '\u2218', '\u2218', '\U0001d54a', '\U0001d54a', '\u221a', '\u221a', '\u25a1', '\u25a1', '\u2293', '\u2293', '\u228f', '\u228f', '\u2291', '\u2291', '\u2290', '\u2290',
7815 '\u2292', '\u2292', '\u2294', '\u2294', '\U0001d4ae', '\U0001d4ae', '\u22c6', '\u22c6', '\u22d0', '\u22d0', '\u22d0', '\u22d0', '\u2286', '\u2286', '\u227b', '\u227b', '\u2ab0', '\u2ab0', '\u227d', '\u227d', '\u227f', '\u227f', '\u220b',
7816 '\u220b', '\u2211', '\u2211', '\u22d1', '\u22d1', '\u2283', '\u2283', '\u2287', '\u2287', '\u22d1', '\u22d1', '\u00de', '\u00de', '\u2122', '\u2122', '\u040b', '\u040b', '\u0426', '\u0426', '\u0009', '\u0009', '\u03a4', '\u03a4', '\u0164', '\u0164', '\u0162', '\u0162', '\u0422', '\u0422', '\U0001d517', '\U0001d517', '\u2234', '\u2234', '\u0398', '\u0398',
7817 '\u2009', '\u2009', '\u223c', '\u223c', '\u2243', '\u2243', '\u2245', '\u2245', '\u2248', '\u2248', '\U0001d54b', '\U0001d54b', '\u20db', '\u20db', '\U0001d4af', '\U0001d4af', '\u0166', '\u0166', '\u00da', '\u00da', '\u219f', '\u219f', '\u2949', '\u2949', '\u040e', '\u040e', '\u016c', '\u016c', '\u00db',
7818 '\u00db', '\u0423', '\u0423', '\u0170', '\u0170', '\U0001d518', '\U0001d518', '\u00d9', '\u00d9', '\u016a', '\u016a', '\u005f', '\u005f', '\u23df', '\u23df', '\u23b5', '\u23b5', '\u23dd', '\u23dd', '\u22c3', '\u22c3', '\u228e', '\u228e', '\u0172', '\u0172', '\U0001d54c', '\U0001d54c', '\u2191', '\u2191', '\u2912',
7819 '\u2912', '\u21c5', '\u21c5', '\u2195', '\u2195', '\u296e', '\u296e', '\u22a5', '\u22a5', '\u21a5', '\u21a5', '\u21d1', '\u21d1', '\u21d5', '\u21d5', '\u2196', '\u2196', '\u2197', '\u2197', '\u03d2', '\u03d2', '\u03a5', '\u03a5',
7820 '\u016e', '\u016e', '\U0001d4b0', '\U0001d4b0', '\u0168', '\u0168', '\u00dc', '\u00dc', '\u22ab', '\u22ab', '\u2aeb', '\u2aeb', '\u0412', '\u0412', '\u22a9', '\u22a9', '\u2ae6', '\u2ae6', '\u22c1', '\u22c1', '\u2016', '\u2016', '\u2016', '\u2016', '\u2223', '\u2223', '\u007c', '\u007c', '\u2758', '\u2758', '\u2240',
7821 '\u2240', '\u200a', '\u200a', '\U0001d519', '\U0001d519', '\U0001d54d', '\U0001d54d', '\U0001d4b1', '\U0001d4b1', '\u22aa', '\u22aa', '\u0174', '\u0174', '\u22c0', '\u22c0', '\U0001d51a', '\U0001d51a', '\U0001d54e', '\U0001d54e', '\U0001d4b2', '\U0001d4b2', '\U0001d51b', '\U0001d51b', '\u039e', '\u039e', '\U0001d54f', '\U0001d54f', '\U0001d4b3', '\U0001d4b3', '\u042f', '\u042f', '\u0407', '\u0407', '\u042e', '\u042e', '\u00dd', '\u00dd',
7822 '\u0176', '\u0176', '\u042b', '\u042b', '\U0001d51c', '\U0001d51c', '\U0001d550', '\U0001d550', '\U0001d4b4', '\U0001d4b4', '\u0178', '\u0178', '\u0416', '\u0416', '\u0179', '\u0179', '\u017d', '\u017d', '\u0417', '\u0417', '\u017b', '\u017b', '\u200b', '\u200b', '\u0396', '\u0396', '\u2128', '\u2128', '\u2124', '\u2124', '\U0001d4b5', '\U0001d4b5', '\u00e1', '\u00e1', '\u0103', '\u0103', '\u223e',
7823 '\u223e', '\u223f', '\u223f', '\u00e2', '\u00e2', '\u00b4', '\u00b4', '\u0430', '\u0430', '\u00e6', '\u00e6', '\u2061', '\u2061', '\U0001d51e', '\U0001d51e', '\u00e0', '\u00e0', '\u2135', '\u2135', '\u2135', '\u2135', '\u03b1', '\u03b1', '\u0101', '\u0101', '\u2a3f', '\u2a3f', '\u2227', '\u2227', '\u2a55', '\u2a55', '\u2a5c', '\u2a5c', '\u2a58', '\u2a58', '\u2a5a', '\u2a5a', '\u2220',
7824 '\u2220', '\u29a4', '\u29a4', '\u2220', '\u2220', '\u2221', '\u2221', '\u29a8', '\u29a8', '\u29a9', '\u29a9', '\u29aa', '\u29aa', '\u29ab', '\u29ab', '\u29ac', '\u29ac', '\u29ad', '\u29ad', '\u29ae', '\u29ae', '\u29af', '\u29af', '\u221f', '\u221f', '\u22be', '\u22be', '\u299d', '\u299d', '\u2222',
7825 '\u2222', '\u00c5', '\u00c5', '\u237c', '\u237c', '\u0105', '\u0105', '\U0001d552', '\U0001d552', '\u2248', '\u2248', '\u2a70', '\u2a70', '\u2a6f', '\u2a6f', '\u224a', '\u224a', '\u224b', '\u224b', '\u2248', '\u2248', '\u224a', '\u224a', '\u00e5', '\u00e5', '\U0001d4b6', '\U0001d4b6', '\u002a', '\u002a', '\u2248', '\u2248', '\u224d', '\u224d', '\u00e3', '\u00e3', '\u00e4',
7826 '\u00e4', '\u2233', '\u2233', '\u2a11', '\u2a11', '\u2aed', '\u2aed', '\u224c', '\u224c', '\u03f6', '\u03f6', '\u2035', '\u2035', '\u223d', '\u223d', '\u22cd', '\u22cd', '\u22bd', '\u22bd', '\u2305', '\u2305', '\u2305', '\u2305', '\u23b5', '\u23b5', '\u23b6', '\u23b6', '\u224c', '\u224c', '\u0431',
7827 '\u0431', '\u201e', '\u201e', '\u2235', '\u2235', '\u2235', '\u2235', '\u29b0', '\u29b0', '\u03f6', '\u03f6', '\u212c', '\u212c', '\u03b2', '\u03b2', '\u2136', '\u2136', '\u226c', '\u226c', '\U0001d51f', '\U0001d51f', '\u22c2', '\u22c2', '\u25ef', '\u25ef', '\u22c3', '\u22c3', '\u2a00', '\u2a00', '\u2a01', '\u2a01', '\u2a02', '\u2a02',
7828 '\u2a06', '\u2a06', '\u2605', '\u2605', '\u25bd', '\u25bd', '\u25b3', '\u25b3', '\u2a04', '\u2a04', '\u22c1', '\u22c1', '\u22c0', '\u22c0', '\u290d', '\u290d', '\u29eb', '\u29eb', '\u25aa', '\u25aa', '\u25b4', '\u25b4', '\u25be',
7829 '\u25be', '\u25c2', '\u25c2', '\u25b8', '\u25b8', '\u2423', '\u2423', '\u2592', '\u2592', '\u2591', '\u2591', '\u2593', '\u2593', '\u2588', '\u2588', '\u2310', '\u2310', '\U0001d553', '\U0001d553', '\u22a5', '\u22a5', '\u22a5', '\u22a5', '\u22c8', '\u22c8', '\u2557', '\u2557', '\u2554', '\u2554', '\u2556',
7830 '\u2556', '\u2553', '\u2553', '\u2550', '\u2550', '\u2566', '\u2566', '\u2569', '\u2569', '\u2564', '\u2564', '\u2567', '\u2567', '\u255d', '\u255d', '\u255a', '\u255a', '\u255c', '\u255c', '\u2559', '\u2559', '\u2551', '\u2551', '\u256c', '\u256c', '\u2563', '\u2563', '\u2560', '\u2560', '\u256b', '\u256b', '\u2562', '\u2562', '\u255f', '\u255f', '\u29c9',
7831 '\u29c9', '\u2555', '\u2555', '\u2552', '\u2552', '\u2510', '\u2510', '\u250c', '\u250c', '\u2500', '\u2500', '\u2565', '\u2565', '\u2568', '\u2568', '\u252c', '\u252c', '\u2534', '\u2534', '\u229f', '\u229f', '\u229e', '\u229e', '\u22a0', '\u22a0', '\u255b', '\u255b', '\u2558', '\u2558', '\u2518', '\u2518', '\u2514', '\u2514', '\u2502',
7832 '\u2502', '\u256a', '\u256a', '\u2561', '\u2561', '\u255e', '\u255e', '\u253c', '\u253c', '\u2524', '\u2524', '\u251c', '\u251c', '\u2035', '\u2035', '\u02d8', '\u02d8', '\u00a6', '\u00a6', '\U0001d4b7', '\U0001d4b7', '\u204f', '\u204f', '\u223d', '\u223d', '\u22cd', '\u22cd', '\u005c', '\u005c', '\u29c5', '\u29c5', '\u27c8', '\u27c8', '\u2022', '\u2022', '\u2022',
7833 '\u2022', '\u224e', '\u224e', '\u2aae', '\u2aae', '\u224f', '\u224f', '\u224f', '\u224f', '\u0107', '\u0107', '\u2229', '\u2229', '\u2a44', '\u2a44', '\u2a49', '\u2a49', '\u2a4b', '\u2a4b', '\u2a47', '\u2a47', '\u2a40', '\u2a40', '\u2041', '\u2041', '\u02c7', '\u02c7', '\u2a4d', '\u2a4d', '\u010d', '\u010d', '\u00e7', '\u00e7', '\u0109',
7834 '\u0109', '\u2a4c', '\u2a4c', '\u2a50', '\u2a50', '\u010b', '\u010b', '\u00b8', '\u00b8', '\u29b2', '\u29b2', '\u00a2', '\u00a2', '\u00b7', '\u00b7', '\U0001d520', '\U0001d520', '\u0447', '\u0447', '\u2713', '\u2713', '\u2713', '\u2713', '\u03c7', '\u03c7', '\u25cb', '\u25cb', '\u29c3', '\u29c3', '\u02c6', '\u02c6', '\u2257', '\u2257', '\u21ba',
7835 '\u21ba', '\u21bb', '\u21bb', '\u00ae', '\u00ae', '\u24c8', '\u24c8', '\u229b', '\u229b', '\u229a', '\u229a', '\u229d', '\u229d', '\u2257', '\u2257', '\u2a10', '\u2a10', '\u2aef', '\u2aef', '\u29c2', '\u29c2', '\u2663', '\u2663', '\u2663', '\u2663', '\u003a',
7836 '\u003a', '\u2254', '\u2254', '\u2254', '\u2254', '\u002c', '\u002c', '\u0040', '\u0040', '\u2201', '\u2201', '\u2218', '\u2218', '\u2201', '\u2201', '\u2102', '\u2102', '\u2245', '\u2245', '\u2a6d', '\u2a6d', '\u222e', '\u222e', '\U0001d554', '\U0001d554', '\u2210', '\u2210', '\u00a9', '\u00a9', '\u2117', '\u2117', '\u21b5', '\u21b5',
7837 '\u2717', '\u2717', '\U0001d4b8', '\U0001d4b8', '\u2acf', '\u2acf', '\u2ad1', '\u2ad1', '\u2ad0', '\u2ad0', '\u2ad2', '\u2ad2', '\u22ef', '\u22ef', '\u2938', '\u2938', '\u2935', '\u2935', '\u22de', '\u22de', '\u22df', '\u22df', '\u21b6', '\u21b6', '\u293d', '\u293d', '\u222a', '\u222a', '\u2a48', '\u2a48', '\u2a46', '\u2a46', '\u2a4a', '\u2a4a',
7838 '\u228d', '\u228d', '\u2a45', '\u2a45', '\u21b7', '\u21b7', '\u293c', '\u293c', '\u22de', '\u22de', '\u22df', '\u22df', '\u22ce', '\u22ce', '\u22cf', '\u22cf', '\u00a4', '\u00a4', '\u21b6', '\u21b6', '\u21b7', '\u21b7', '\u22ce', '\u22ce', '\u22cf', '\u22cf',
7839 '\u2232', '\u2232', '\u2231', '\u2231', '\u232d', '\u232d', '\u21d3', '\u21d3', '\u2965', '\u2965', '\u2020', '\u2020', '\u2138', '\u2138', '\u2193', '\u2193', '\u2010', '\u2010', '\u22a3', '\u22a3', '\u290f', '\u290f', '\u02dd', '\u02dd', '\u010f', '\u010f', '\u0434', '\u0434', '\u2146', '\u2146', '\u2021', '\u2021', '\u21ca', '\u21ca', '\u2a77',
7840 '\u2a77', '\u00b0', '\u00b0', '\u03b4', '\u03b4', '\u29b1', '\u29b1', '\u297f', '\u297f', '\U0001d521', '\U0001d521', '\u21c3', '\u21c3', '\u21c2', '\u21c2', '\u22c4', '\u22c4', '\u22c4', '\u22c4', '\u2666', '\u2666', '\u2666', '\u2666', '\u00a8', '\u00a8', '\u03dd', '\u03dd', '\u22f2', '\u22f2', '\u00f7', '\u00f7', '\u00f7', '\u00f7', '\u22c7',
7841 '\u22c7', '\u22c7', '\u22c7', '\u0452', '\u0452', '\u231e', '\u231e', '\u230d', '\u230d', '\u0024', '\u0024', '\U0001d555', '\U0001d555', '\u02d9', '\u02d9', '\u2250', '\u2250', '\u2251', '\u2251', '\u2238', '\u2238', '\u2214', '\u2214', '\u22a1', '\u22a1', '\u2306', '\u2306', '\u2193', '\u2193', '\u21ca',
7842 '\u21ca', '\u21c3', '\u21c3', '\u21c2', '\u21c2', '\u2910', '\u2910', '\u231f', '\u231f', '\u230c', '\u230c', '\U0001d4b9', '\U0001d4b9', '\u0455', '\u0455', '\u29f6', '\u29f6', '\u0111', '\u0111', '\u22f1', '\u22f1', '\u25bf', '\u25bf', '\u25be', '\u25be', '\u21f5', '\u21f5', '\u296f', '\u296f', '\u29a6',
7843 '\u29a6', '\u045f', '\u045f', '\u27ff', '\u27ff', '\u2a77', '\u2a77', '\u2251', '\u2251', '\u00e9', '\u00e9', '\u2a6e', '\u2a6e', '\u011b', '\u011b', '\u2256', '\u2256', '\u00ea', '\u00ea', '\u2255', '\u2255', '\u044d', '\u044d', '\u0117', '\u0117', '\u2147', '\u2147', '\u2252', '\u2252', '\U0001d522', '\U0001d522', '\u2a9a', '\u2a9a', '\u00e8', '\u00e8', '\u2a96', '\u2a96', '\u2a98',
7844 '\u2a98', '\u2a99', '\u2a99', '\u23e7', '\u23e7', '\u2113', '\u2113', '\u2a95', '\u2a95', '\u2a97', '\u2a97', '\u0113', '\u0113', '\u2205', '\u2205', '\u2205', '\u2205', '\u2205', '\u2205', '\u2003', '\u2003', '\u2004', '\u2004', '\u2005', '\u2005', '\u014b', '\u014b', '\u2002', '\u2002', '\u0119', '\u0119', '\U0001d556', '\U0001d556', '\u22d5', '\u22d5', '\u29e3',
7845 '\u29e3', '\u2a71', '\u2a71', '\u03b5', '\u03b5', '\u03b5', '\u03b5', '\u03f5', '\u03f5', '\u2256', '\u2256', '\u2255', '\u2255', '\u2242', '\u2242', '\u2a96', '\u2a96', '\u2a95', '\u2a95', '\u003d', '\u003d', '\u225f', '\u225f', '\u2261', '\u2261', '\u2a78', '\u2a78', '\u29e5', '\u29e5', '\u2253', '\u2253',
7846 '\u2971', '\u2971', '\u212f', '\u212f', '\u2250', '\u2250', '\u2242', '\u2242', '\u03b7', '\u03b7', '\u00f0', '\u00f0', '\u00eb', '\u00eb', '\u20ac', '\u20ac', '\u0021', '\u0021', '\u2203', '\u2203', '\u2130', '\u2130', '\u2147', '\u2147', '\u2252', '\u2252', '\u0444', '\u0444', '\u2640', '\u2640', '\ufb03', '\ufb03', '\ufb00',
7847 '\ufb00', '\ufb04', '\ufb04', '\U0001d523', '\U0001d523', '\ufb01', '\ufb01', '\u266d', '\u266d', '\ufb02', '\ufb02', '\u25b1', '\u25b1', '\u0192', '\u0192', '\U0001d557', '\U0001d557', '\u2200', '\u2200', '\u22d4', '\u22d4', '\u2ad9', '\u2ad9', '\u2a0d', '\u2a0d', '\u00bd', '\u00bd', '\u2153', '\u2153', '\u00bc', '\u00bc', '\u2155', '\u2155', '\u2159', '\u2159',
7848 '\u215b', '\u215b', '\u2154', '\u2154', '\u2156', '\u2156', '\u00be', '\u00be', '\u2157', '\u2157', '\u215c', '\u215c', '\u2158', '\u2158', '\u215a', '\u215a', '\u215d', '\u215d', '\u215e', '\u215e', '\u2044', '\u2044', '\u2322', '\u2322', '\U0001d4bb', '\U0001d4bb', '\u2267', '\u2267', '\u2a8c', '\u2a8c', '\u01f5', '\u01f5', '\u03b3', '\u03b3', '\u03dd',
7849 '\u03dd', '\u2a86', '\u2a86', '\u011f', '\u011f', '\u011d', '\u011d', '\u0433', '\u0433', '\u0121', '\u0121', '\u2265', '\u2265', '\u22db', '\u22db', '\u2265', '\u2265', '\u2267', '\u2267', '\u2a7e', '\u2a7e', '\u2a7e', '\u2a7e', '\u2aa9', '\u2aa9', '\u2a80', '\u2a80', '\u2a82', '\u2a82', '\u2a84', '\u2a84', '\u2a94', '\u2a94', '\U0001d524', '\U0001d524', '\u226b', '\u226b', '\u22d9',
7850 '\u22d9', '\u2137', '\u2137', '\u0453', '\u0453', '\u2277', '\u2277', '\u2a92', '\u2a92', '\u2aa5', '\u2aa5', '\u2aa4', '\u2aa4', '\u2269', '\u2269', '\u2a8a', '\u2a8a', '\u2a8a', '\u2a8a', '\u2a88', '\u2a88', '\u2a88', '\u2a88', '\u2269', '\u2269', '\u22e7', '\u22e7', '\U0001d558', '\U0001d558', '\u0060', '\u0060', '\u210a', '\u210a', '\u2273', '\u2273', '\u2a8e', '\u2a8e', '\u2a90', '\u2a90', '\u2aa7',
7851 '\u2aa7', '\u2a7a', '\u2a7a', '\u22d7', '\u22d7', '\u2995', '\u2995', '\u2a7c', '\u2a7c', '\u2a86', '\u2a86', '\u2978', '\u2978', '\u22d7', '\u22d7', '\u22db', '\u22db', '\u2a8c', '\u2a8c', '\u2277', '\u2277', '\u2273', '\u2273', '\u21d4', '\u21d4', '\u200a', '\u200a', '\u00bd', '\u00bd', '\u210b', '\u210b',
7852 '\u044a', '\u044a', '\u2194', '\u2194', '\u2948', '\u2948', '\u21ad', '\u21ad', '\u210f', '\u210f', '\u0125', '\u0125', '\u2665', '\u2665', '\u2665', '\u2665', '\u2026', '\u2026', '\u22b9', '\u22b9', '\U0001d525', '\U0001d525', '\u2925', '\u2925', '\u2926', '\u2926', '\u21ff', '\u21ff', '\u223b', '\u223b', '\u21a9', '\u21a9',
7853 '\u21aa', '\u21aa', '\U0001d559', '\U0001d559', '\u2015', '\u2015', '\U0001d4bd', '\U0001d4bd', '\u210f', '\u210f', '\u0127', '\u0127', '\u2043', '\u2043', '\u2010', '\u2010', '\u00ed', '\u00ed', '\u2063', '\u2063', '\u00ee', '\u00ee', '\u0438', '\u0438', '\u0435', '\u0435', '\u00a1', '\u00a1', '\u21d4', '\u21d4', '\U0001d526', '\U0001d526', '\u00ec', '\u00ec', '\u2148',
7854 '\u2148', '\u2a0c', '\u2a0c', '\u222d', '\u222d', '\u29dc', '\u29dc', '\u2129', '\u2129', '\u0133', '\u0133', '\u012b', '\u012b', '\u2111', '\u2111', '\u2110', '\u2110', '\u2111', '\u2111', '\u0131', '\u0131', '\u22b7', '\u22b7', '\u01b5', '\u01b5', '\u2208', '\u2208', '\u2105', '\u2105', '\u221e', '\u221e', '\u29dd', '\u29dd', '\u0131',
7855 '\u0131', '\u222b', '\u222b', '\u22ba', '\u22ba', '\u2124', '\u2124', '\u22ba', '\u22ba', '\u2a17', '\u2a17', '\u2a3c', '\u2a3c', '\u0451', '\u0451', '\u012f', '\u012f', '\U0001d55a', '\U0001d55a', '\u03b9', '\u03b9', '\u2a3c', '\u2a3c', '\u00bf', '\u00bf', '\U0001d4be', '\U0001d4be', '\u2208', '\u2208', '\u22f9', '\u22f9', '\u22f5', '\u22f5', '\u22f4',
7856 '\u22f4', '\u22f3', '\u22f3', '\u2208', '\u2208', '\u2062', '\u2062', '\u0129', '\u0129', '\u0456', '\u0456', '\u00ef', '\u00ef', '\u0135', '\u0135', '\u0439', '\u0439', '\U0001d527', '\U0001d527', '\u0237', '\u0237', '\U0001d55b', '\U0001d55b', '\U0001d4bf', '\U0001d4bf', '\u0458', '\u0458', '\u0454', '\u0454', '\u03ba', '\u03ba', '\u03f0', '\u03f0', '\u0137', '\u0137', '\u043a', '\u043a', '\U0001d528',
7857 '\U0001d528', '\u0138', '\u0138', '\u0445', '\u0445', '\u045c', '\u045c', '\U0001d55c', '\U0001d55c', '\U0001d4c0', '\U0001d4c0', '\u21da', '\u21da', '\u21d0', '\u21d0', '\u291b', '\u291b', '\u290e', '\u290e', '\u2266', '\u2266', '\u2a8b', '\u2a8b', '\u2962', '\u2962', '\u013a', '\u013a', '\u29b4', '\u29b4', '\u2112', '\u2112', '\u03bb', '\u03bb', '\u27e8', '\u27e8', '\u2991', '\u2991',
7858 '\u27e8', '\u27e8', '\u2a85', '\u2a85', '\u00ab', '\u00ab', '\u2190', '\u2190', '\u21e4', '\u21e4', '\u291f', '\u291f', '\u291d', '\u291d', '\u21a9', '\u21a9', '\u21ab', '\u21ab', '\u2939', '\u2939', '\u2973', '\u2973', '\u21a2', '\u21a2', '\u2aab', '\u2aab', '\u2919', '\u2919', '\u2aad', '\u2aad', '\u290c', '\u290c', '\u2772', '\u2772', '\u007b',
7859 '\u007b', '\u005b', '\u005b', '\u298b', '\u298b', '\u298f', '\u298f', '\u298d', '\u298d', '\u013e', '\u013e', '\u013c', '\u013c', '\u2308', '\u2308', '\u007b', '\u007b', '\u043b', '\u043b', '\u2936', '\u2936', '\u201c', '\u201c', '\u201e', '\u201e', '\u2967', '\u2967', '\u294b', '\u294b', '\u21b2', '\u21b2', '\u2264', '\u2264', '\u2190',
7860 '\u2190', '\u21a2', '\u21a2', '\u21bd', '\u21bd', '\u21bc', '\u21bc', '\u21c7', '\u21c7', '\u2194', '\u2194', '\u21c6', '\u21c6', '\u21cb', '\u21cb', '\u21ad', '\u21ad', '\u22cb',
7861 '\u22cb', '\u22da', '\u22da', '\u2264', '\u2264', '\u2266', '\u2266', '\u2a7d', '\u2a7d', '\u2a7d', '\u2a7d', '\u2aa8', '\u2aa8', '\u2a7f', '\u2a7f', '\u2a81', '\u2a81', '\u2a83', '\u2a83', '\u2a93', '\u2a93', '\u2a85', '\u2a85', '\u22d6', '\u22d6', '\u22da', '\u22da', '\u2a8b', '\u2a8b', '\u2276', '\u2276',
7862 '\u2272', '\u2272', '\u297c', '\u297c', '\u230a', '\u230a', '\U0001d529', '\U0001d529', '\u2276', '\u2276', '\u2a91', '\u2a91', '\u21bd', '\u21bd', '\u21bc', '\u21bc', '\u296a', '\u296a', '\u2584', '\u2584', '\u0459', '\u0459', '\u226a', '\u226a', '\u21c7', '\u21c7', '\u231e', '\u231e', '\u296b', '\u296b', '\u25fa', '\u25fa', '\u0140', '\u0140', '\u23b0', '\u23b0',
7863 '\u23b0', '\u23b0', '\u2268', '\u2268', '\u2a89', '\u2a89', '\u2a89', '\u2a89', '\u2a87', '\u2a87', '\u2a87', '\u2a87', '\u2268', '\u2268', '\u22e6', '\u22e6', '\u27ec', '\u27ec', '\u21fd', '\u21fd', '\u27e6', '\u27e6', '\u27f5', '\u27f5', '\u27f7', '\u27f7', '\u27fc', '\u27fc', '\u27f6',
7864 '\u27f6', '\u21ab', '\u21ab', '\u21ac', '\u21ac', '\u2985', '\u2985', '\U0001d55d', '\U0001d55d', '\u2a2d', '\u2a2d', '\u2a34', '\u2a34', '\u2217', '\u2217', '\u005f', '\u005f', '\u25ca', '\u25ca', '\u25ca', '\u25ca', '\u29eb', '\u29eb', '\u0028', '\u0028', '\u2993', '\u2993', '\u21c6', '\u21c6', '\u231f',
7865 '\u231f', '\u21cb', '\u21cb', '\u296d', '\u296d', '\u200e', '\u200e', '\u22bf', '\u22bf', '\u2039', '\u2039', '\U0001d4c1', '\U0001d4c1', '\u21b0', '\u21b0', '\u2272', '\u2272', '\u2a8d', '\u2a8d', '\u2a8f', '\u2a8f', '\u005b', '\u005b', '\u2018', '\u2018', '\u201a', '\u201a', '\u0142', '\u0142', '\u2aa6', '\u2aa6', '\u2a79', '\u2a79', '\u22d6', '\u22d6', '\u22cb',
7866 '\u22cb', '\u22c9', '\u22c9', '\u2976', '\u2976', '\u2a7b', '\u2a7b', '\u2996', '\u2996', '\u25c3', '\u25c3', '\u22b4', '\u22b4', '\u25c2', '\u25c2', '\u294a', '\u294a', '\u2966', '\u2966', '\u223a', '\u223a', '\u00af', '\u00af', '\u2642', '\u2642', '\u2720', '\u2720', '\u2720', '\u2720', '\u21a6', '\u21a6', '\u21a6', '\u21a6', '\u21a7',
7867 '\u21a7', '\u21a4', '\u21a4', '\u21a5', '\u21a5', '\u25ae', '\u25ae', '\u2a29', '\u2a29', '\u043c', '\u043c', '\u2014', '\u2014', '\u2221', '\u2221', '\U0001d52a', '\U0001d52a', '\u2127', '\u2127', '\u00b5', '\u00b5', '\u2223', '\u2223', '\u002a', '\u002a', '\u2af0', '\u2af0', '\u00b7', '\u00b7', '\u2212', '\u2212', '\u229f',
7868 '\u229f', '\u2238', '\u2238', '\u2a2a', '\u2a2a', '\u2adb', '\u2adb', '\u2026', '\u2026', '\u2213', '\u2213', '\u22a7', '\u22a7', '\U0001d55e', '\U0001d55e', '\u2213', '\u2213', '\U0001d4c2', '\U0001d4c2', '\u223e', '\u223e', '\u03bc', '\u03bc', '\u22b8', '\u22b8', '\u22b8', '\u22b8', '\u21cd', '\u21cd', '\u21ce', '\u21ce', '\u21cf',
7869 '\u21cf', '\u22af', '\u22af', '\u22ae', '\u22ae', '\u2207', '\u2207', '\u0144', '\u0144', '\u2249', '\u2249', '\u0149', '\u0149', '\u2249', '\u2249', '\u266e', '\u266e', '\u266e', '\u266e', '\u2115', '\u2115', '\u00a0', '\u00a0', '\u2a43', '\u2a43', '\u0148', '\u0148', '\u0146', '\u0146', '\u2247', '\u2247', '\u2a42', '\u2a42', '\u043d',
7870 '\u043d', '\u2013', '\u2013', '\u2260', '\u2260', '\u21d7', '\u21d7', '\u2924', '\u2924', '\u2197', '\u2197', '\u2197', '\u2197', '\u2262', '\u2262', '\u2928', '\u2928', '\u2204', '\u2204', '\u2204', '\u2204', '\U0001d52b', '\U0001d52b', '\u2271', '\u2271', '\u2271', '\u2271', '\u2275', '\u2275', '\u226f', '\u226f', '\u226f', '\u226f', '\u21ce', '\u21ce', '\u21ae', '\u21ae',
7871 '\u2af2', '\u2af2', '\u220b', '\u220b', '\u22fc', '\u22fc', '\u22fa', '\u22fa', '\u220b', '\u220b', '\u045a', '\u045a', '\u21cd', '\u21cd', '\u219a', '\u219a', '\u2025', '\u2025', '\u2270', '\u2270', '\u219a', '\u219a', '\u21ae', '\u21ae', '\u2270', '\u2270', '\u226e', '\u226e', '\u2274', '\u2274', '\u226e', '\u226e', '\u22ea', '\u22ea', '\u22ec', '\u22ec',
7872 '\u2224', '\u2224', '\U0001d55f', '\U0001d55f', '\u00ac', '\u00ac', '\u2209', '\u2209', '\u2209', '\u2209', '\u22f7', '\u22f7', '\u22f6', '\u22f6', '\u220c', '\u220c', '\u220c', '\u220c', '\u22fe', '\u22fe', '\u22fd', '\u22fd', '\u2226', '\u2226', '\u2226', '\u2226', '\u2a14', '\u2a14', '\u2280', '\u2280', '\u22e0', '\u22e0', '\u2280',
7873 '\u2280', '\u21cf', '\u21cf', '\u219b', '\u219b', '\u219b', '\u219b', '\u22eb', '\u22eb', '\u22ed', '\u22ed', '\u2281', '\u2281', '\u22e1', '\u22e1', '\U0001d4c3', '\U0001d4c3', '\u2224', '\u2224', '\u2226', '\u2226', '\u2241', '\u2241', '\u2244', '\u2244', '\u2244', '\u2244', '\u2224', '\u2224', '\u2226', '\u2226', '\u22e2',
7874 '\u22e2', '\u22e3', '\u22e3', '\u2284', '\u2284', '\u2288', '\u2288', '\u2288', '\u2288', '\u2281', '\u2281', '\u2285', '\u2285', '\u2289', '\u2289', '\u2289', '\u2289', '\u2279', '\u2279', '\u00f1', '\u00f1', '\u2278', '\u2278', '\u22ea', '\u22ea', '\u22ec', '\u22ec', '\u22eb', '\u22eb',
7875 '\u22ed', '\u22ed', '\u03bd', '\u03bd', '\u0023', '\u0023', '\u2116', '\u2116', '\u2007', '\u2007', '\u22ad', '\u22ad', '\u2904', '\u2904', '\u22ac', '\u22ac', '\u29de', '\u29de', '\u2902', '\u2902', '\u2903', '\u2903', '\u21d6', '\u21d6', '\u2923', '\u2923', '\u2196', '\u2196', '\u2196', '\u2196', '\u2927', '\u2927',
7876 '\u24c8', '\u24c8', '\u00f3', '\u00f3', '\u229b', '\u229b', '\u229a', '\u229a', '\u00f4', '\u00f4', '\u043e', '\u043e', '\u229d', '\u229d', '\u0151', '\u0151', '\u2a38', '\u2a38', '\u2299', '\u2299', '\u29bc', '\u29bc', '\u0153', '\u0153', '\u29bf', '\u29bf', '\U0001d52c', '\U0001d52c', '\u02db', '\u02db', '\u00f2', '\u00f2', '\u29c1', '\u29c1', '\u29b5', '\u29b5', '\u03a9', '\u03a9', '\u222e',
7877 '\u222e', '\u21ba', '\u21ba', '\u29be', '\u29be', '\u29bb', '\u29bb', '\u203e', '\u203e', '\u29c0', '\u29c0', '\u014d', '\u014d', '\u03c9', '\u03c9', '\u03bf', '\u03bf', '\u29b6', '\u29b6', '\u2296', '\u2296', '\U0001d560', '\U0001d560', '\u29b7', '\u29b7', '\u29b9', '\u29b9', '\u2295', '\u2295', '\u2228', '\u2228', '\u21bb', '\u21bb', '\u2a5d', '\u2a5d', '\u2134', '\u2134',
7878 '\u2134', '\u2134', '\u00aa', '\u00aa', '\u00ba', '\u00ba', '\u22b6', '\u22b6', '\u2a56', '\u2a56', '\u2a57', '\u2a57', '\u2a5b', '\u2a5b', '\u2134', '\u2134', '\u00f8', '\u00f8', '\u2298', '\u2298', '\u00f5', '\u00f5', '\u2297', '\u2297', '\u2a36', '\u2a36', '\u00f6', '\u00f6', '\u233d', '\u233d', '\u2225', '\u2225', '\u00b6', '\u00b6', '\u2225', '\u2225',
7879 '\u2af3', '\u2af3', '\u2afd', '\u2afd', '\u2202', '\u2202', '\u043f', '\u043f', '\u0025', '\u0025', '\u002e', '\u002e', '\u2030', '\u2030', '\u22a5', '\u22a5', '\u2031', '\u2031', '\U0001d52d', '\U0001d52d', '\u03c6', '\u03c6', '\u03d5', '\u03d5', '\u2133', '\u2133', '\u260e', '\u260e', '\u03c0', '\u03c0', '\u22d4', '\u22d4', '\u03d6', '\u03d6', '\u210f', '\u210f',
7880 '\u210e', '\u210e', '\u210f', '\u210f', '\u002b', '\u002b', '\u2a23', '\u2a23', '\u229e', '\u229e', '\u2a22', '\u2a22', '\u2214', '\u2214', '\u2a25', '\u2a25', '\u2a72', '\u2a72', '\u00b1', '\u00b1', '\u2a26', '\u2a26', '\u2a27', '\u2a27', '\u00b1', '\u00b1', '\u2a15', '\u2a15', '\U0001d561', '\U0001d561', '\u00a3', '\u00a3', '\u227a',
7881 '\u227a', '\u2ab3', '\u2ab3', '\u2ab7', '\u2ab7', '\u227c', '\u227c', '\u2aaf', '\u2aaf', '\u227a', '\u227a', '\u2ab7', '\u2ab7', '\u227c', '\u227c', '\u2aaf', '\u2aaf', '\u2ab9', '\u2ab9', '\u2ab5', '\u2ab5', '\u22e8', '\u22e8', '\u227e', '\u227e', '\u2032', '\u2032', '\u2119', '\u2119', '\u2ab5', '\u2ab5', '\u2ab9',
7882 '\u2ab9', '\u22e8', '\u22e8', '\u220f', '\u220f', '\u232e', '\u232e', '\u2312', '\u2312', '\u2313', '\u2313', '\u221d', '\u221d', '\u221d', '\u221d', '\u227e', '\u227e', '\u22b0', '\u22b0', '\U0001d4c5', '\U0001d4c5', '\u03c8', '\u03c8', '\u2008', '\u2008', '\U0001d52e', '\U0001d52e', '\u2a0c', '\u2a0c', '\U0001d562', '\U0001d562', '\u2057', '\u2057', '\U0001d4c6', '\U0001d4c6',
7883 '\u210d', '\u210d', '\u2a16', '\u2a16', '\u003f', '\u003f', '\u225f', '\u225f', '\u21db', '\u21db', '\u21d2', '\u21d2', '\u291c', '\u291c', '\u290f', '\u290f', '\u2964', '\u2964', '\u0155', '\u0155', '\u221a', '\u221a', '\u29b3', '\u29b3', '\u27e9', '\u27e9', '\u2992', '\u2992', '\u29a5', '\u29a5', '\u27e9', '\u27e9', '\u00bb',
7884 '\u00bb', '\u2192', '\u2192', '\u2975', '\u2975', '\u21e5', '\u21e5', '\u2920', '\u2920', '\u2933', '\u2933', '\u291e', '\u291e', '\u21aa', '\u21aa', '\u21ac', '\u21ac', '\u2945', '\u2945', '\u2974', '\u2974', '\u21a3', '\u21a3', '\u219d', '\u219d', '\u291a', '\u291a', '\u2236', '\u2236', '\u211a', '\u211a', '\u290d', '\u290d',
7885 '\u2773', '\u2773', '\u007d', '\u007d', '\u005d', '\u005d', '\u298c', '\u298c', '\u298e', '\u298e', '\u2990', '\u2990', '\u0159', '\u0159', '\u0157', '\u0157', '\u2309', '\u2309', '\u007d', '\u007d', '\u0440', '\u0440', '\u2937', '\u2937', '\u2969', '\u2969', '\u201d', '\u201d', '\u201d', '\u201d', '\u21b3', '\u21b3', '\u211c', '\u211c', '\u211b',
7886 '\u211b', '\u211c', '\u211c', '\u211d', '\u211d', '\u25ad', '\u25ad', '\u00ae', '\u00ae', '\u297d', '\u297d', '\u230b', '\u230b', '\U0001d52f', '\U0001d52f', '\u21c1', '\u21c1', '\u21c0', '\u21c0', '\u296c', '\u296c', '\u03c1', '\u03c1', '\u03f1', '\u03f1', '\u2192', '\u2192', '\u21a3', '\u21a3', '\u21c1', '\u21c1',
7887 '\u21c0', '\u21c0', '\u21c4', '\u21c4', '\u21cc', '\u21cc', '\u21c9', '\u21c9', '\u219d', '\u219d', '\u22cc', '\u22cc', '\u02da', '\u02da', '\u2253', '\u2253', '\u21c4', '\u21c4', '\u21cc', '\u21cc', '\u200f',
7888 '\u200f', '\u23b1', '\u23b1', '\u23b1', '\u23b1', '\u2aee', '\u2aee', '\u27ed', '\u27ed', '\u21fe', '\u21fe', '\u27e7', '\u27e7', '\u2986', '\u2986', '\U0001d563', '\U0001d563', '\u2a2e', '\u2a2e', '\u2a35', '\u2a35', '\u0029', '\u0029', '\u2994', '\u2994', '\u2a12', '\u2a12', '\u21c9', '\u21c9', '\u203a', '\u203a', '\U0001d4c7', '\U0001d4c7', '\u21b1',
7889 '\u21b1', '\u005d', '\u005d', '\u2019', '\u2019', '\u2019', '\u2019', '\u22cc', '\u22cc', '\u22ca', '\u22ca', '\u25b9', '\u25b9', '\u22b5', '\u22b5', '\u25b8', '\u25b8', '\u29ce', '\u29ce', '\u2968', '\u2968', '\u211e', '\u211e', '\u015b', '\u015b', '\u201a', '\u201a', '\u227b', '\u227b', '\u2ab4', '\u2ab4', '\u2ab8', '\u2ab8', '\u0161', '\u0161', '\u227d',
7890 '\u227d', '\u2ab0', '\u2ab0', '\u015f', '\u015f', '\u015d', '\u015d', '\u2ab6', '\u2ab6', '\u2aba', '\u2aba', '\u22e9', '\u22e9', '\u2a13', '\u2a13', '\u227f', '\u227f', '\u0441', '\u0441', '\u22c5', '\u22c5', '\u22a1', '\u22a1', '\u2a66', '\u2a66', '\u21d8', '\u21d8', '\u2925', '\u2925', '\u2198', '\u2198', '\u2198', '\u2198', '\u00a7', '\u00a7', '\u003b',
7891 '\u003b', '\u2929', '\u2929', '\u2216', '\u2216', '\u2216', '\u2216', '\u2736', '\u2736', '\U0001d530', '\U0001d530', '\u2322', '\u2322', '\u266f', '\u266f', '\u0449', '\u0449', '\u0448', '\u0448', '\u2223', '\u2223', '\u2225', '\u2225', '\u00ad', '\u00ad', '\u03c3', '\u03c3', '\u03c2', '\u03c2', '\u03c2', '\u03c2', '\u223c', '\u223c', '\u2a6a',
7892 '\u2a6a', '\u2243', '\u2243', '\u2243', '\u2243', '\u2a9e', '\u2a9e', '\u2aa0', '\u2aa0', '\u2a9d', '\u2a9d', '\u2a9f', '\u2a9f', '\u2246', '\u2246', '\u2a24', '\u2a24', '\u2972', '\u2972', '\u2190', '\u2190', '\u2216', '\u2216', '\u2a33', '\u2a33', '\u29e4', '\u29e4', '\u2223', '\u2223', '\u2323', '\u2323', '\u2aaa', '\u2aaa', '\u2aac',
7893 '\u2aac', '\u044c', '\u044c', '\u002f', '\u002f', '\u29c4', '\u29c4', '\u233f', '\u233f', '\U0001d564', '\U0001d564', '\u2660', '\u2660', '\u2660', '\u2660', '\u2225', '\u2225', '\u2293', '\u2293', '\u2294', '\u2294', '\u228f', '\u228f', '\u2291', '\u2291', '\u228f', '\u228f', '\u2291', '\u2291', '\u2290', '\u2290', '\u2292', '\u2292',
7894 '\u2290', '\u2290', '\u2292', '\u2292', '\u25a1', '\u25a1', '\u25a1', '\u25a1', '\u25aa', '\u25aa', '\u25aa', '\u25aa', '\u2192', '\u2192', '\U0001d4c8', '\U0001d4c8', '\u2216', '\u2216', '\u2323', '\u2323', '\u22c6', '\u22c6', '\u2606', '\u2606', '\u2605', '\u2605', '\u03f5', '\u03f5', '\u03d5', '\u03d5', '\u00af',
7895 '\u00af', '\u2282', '\u2282', '\u2ac5', '\u2ac5', '\u2abd', '\u2abd', '\u2286', '\u2286', '\u2ac3', '\u2ac3', '\u2ac1', '\u2ac1', '\u2acb', '\u2acb', '\u228a', '\u228a', '\u2abf', '\u2abf', '\u2979', '\u2979', '\u2282', '\u2282', '\u2286', '\u2286', '\u2ac5', '\u2ac5', '\u228a', '\u228a', '\u2acb', '\u2acb',
7896 '\u2ac7', '\u2ac7', '\u2ad5', '\u2ad5', '\u2ad3', '\u2ad3', '\u227b', '\u227b', '\u2ab8', '\u2ab8', '\u227d', '\u227d', '\u2ab0', '\u2ab0', '\u2aba', '\u2aba', '\u2ab6', '\u2ab6', '\u22e9', '\u22e9', '\u227f', '\u227f', '\u2211', '\u2211', '\u266a', '\u266a', '\u2283', '\u2283', '\u00b9', '\u00b9', '\u00b2',
7897 '\u00b2', '\u00b3', '\u00b3', '\u2ac6', '\u2ac6', '\u2abe', '\u2abe', '\u2ad8', '\u2ad8', '\u2287', '\u2287', '\u2ac4', '\u2ac4', '\u27c9', '\u27c9', '\u2ad7', '\u2ad7', '\u297b', '\u297b', '\u2ac2', '\u2ac2', '\u2acc', '\u2acc', '\u228b', '\u228b', '\u2ac0', '\u2ac0', '\u2283', '\u2283', '\u2287', '\u2287', '\u2ac6',
7898 '\u2ac6', '\u228b', '\u228b', '\u2acc', '\u2acc', '\u2ac8', '\u2ac8', '\u2ad4', '\u2ad4', '\u2ad6', '\u2ad6', '\u21d9', '\u21d9', '\u2926', '\u2926', '\u2199', '\u2199', '\u2199', '\u2199', '\u292a', '\u292a', '\u00df', '\u00df', '\u2316', '\u2316', '\u03c4', '\u03c4', '\u23b4', '\u23b4', '\u0165', '\u0165', '\u0163',
7899 '\u0163', '\u0442', '\u0442', '\u20db', '\u20db', '\u2315', '\u2315', '\U0001d531', '\U0001d531', '\u2234', '\u2234', '\u2234', '\u2234', '\u03b8', '\u03b8', '\u03d1', '\u03d1', '\u03d1', '\u03d1', '\u2248', '\u2248', '\u223c', '\u223c', '\u2009', '\u2009', '\u2248', '\u2248', '\u223c', '\u223c', '\u00fe', '\u00fe', '\u02dc',
7900 '\u02dc', '\u00d7', '\u00d7', '\u22a0', '\u22a0', '\u2a31', '\u2a31', '\u2a30', '\u2a30', '\u222d', '\u222d', '\u2928', '\u2928', '\u22a4', '\u22a4', '\u2336', '\u2336', '\u2af1', '\u2af1', '\U0001d565', '\U0001d565', '\u2ada', '\u2ada', '\u2929', '\u2929', '\u2034', '\u2034', '\u2122', '\u2122', '\u25b5', '\u25b5', '\u25bf', '\u25bf',
7901 '\u25c3', '\u25c3', '\u22b4', '\u22b4', '\u225c', '\u225c', '\u25b9', '\u25b9', '\u22b5', '\u22b5', '\u25ec', '\u25ec', '\u225c', '\u225c', '\u2a3a', '\u2a3a', '\u2a39', '\u2a39', '\u29cd', '\u29cd', '\u2a3b', '\u2a3b', '\u23e2', '\u23e2', '\U0001d4c9',
7902 '\U0001d4c9', '\u0446', '\u0446', '\u045b', '\u045b', '\u0167', '\u0167', '\u226c', '\u226c', '\u219e', '\u219e', '\u21a0', '\u21a0', '\u21d1', '\u21d1', '\u2963', '\u2963', '\u00fa', '\u00fa', '\u2191', '\u2191', '\u045e', '\u045e', '\u016d', '\u016d', '\u00fb', '\u00fb', '\u0443', '\u0443', '\u21c5', '\u21c5', '\u0171',
7903 '\u0171', '\u296e', '\u296e', '\u297e', '\u297e', '\U0001d532', '\U0001d532', '\u00f9', '\u00f9', '\u21bf', '\u21bf', '\u21be', '\u21be', '\u2580', '\u2580', '\u231c', '\u231c', '\u231c', '\u231c', '\u230f', '\u230f', '\u25f8', '\u25f8', '\u016b', '\u016b', '\u00a8', '\u00a8', '\u0173', '\u0173', '\U0001d566', '\U0001d566', '\u2191', '\u2191', '\u2195',
7904 '\u2195', '\u21bf', '\u21bf', '\u21be', '\u21be', '\u228e', '\u228e', '\u03c5', '\u03c5', '\u03d2', '\u03d2', '\u03c5', '\u03c5', '\u21c8', '\u21c8', '\u231d', '\u231d', '\u231d', '\u231d', '\u230e', '\u230e', '\u016f', '\u016f', '\u25f9', '\u25f9', '\U0001d4ca', '\U0001d4ca', '\u22f0', '\u22f0',
7905 '\u0169', '\u0169', '\u25b5', '\u25b5', '\u25b4', '\u25b4', '\u21c8', '\u21c8', '\u00fc', '\u00fc', '\u29a7', '\u29a7', '\u21d5', '\u21d5', '\u2ae8', '\u2ae8', '\u2ae9', '\u2ae9', '\u22a8', '\u22a8', '\u299c', '\u299c', '\u03f5', '\u03f5', '\u03f0', '\u03f0', '\u2205', '\u2205', '\u03d5', '\u03d5', '\u03d6', '\u03d6', '\u221d',
7906 '\u221d', '\u2195', '\u2195', '\u03f1', '\u03f1', '\u03c2', '\u03c2', '\u03d1', '\u03d1', '\u22b2', '\u22b2', '\u22b3', '\u22b3', '\u0432', '\u0432', '\u22a2', '\u22a2', '\u2228', '\u2228', '\u22bb', '\u22bb', '\u225a', '\u225a', '\u22ee', '\u22ee', '\u007c', '\u007c', '\u007c', '\u007c', '\U0001d533',
7907 '\U0001d533', '\u22b2', '\u22b2', '\U0001d567', '\U0001d567', '\u221d', '\u221d', '\u22b3', '\u22b3', '\U0001d4cb', '\U0001d4cb', '\u299a', '\u299a', '\u0175', '\u0175', '\u2a5f', '\u2a5f', '\u2227', '\u2227', '\u2259', '\u2259', '\u2118', '\u2118', '\U0001d534', '\U0001d534', '\U0001d568', '\U0001d568', '\u2118', '\u2118', '\u2240', '\u2240', '\u2240', '\u2240', '\U0001d4cc', '\U0001d4cc', '\u22c2', '\u22c2', '\u25ef',
7908 '\u25ef', '\u22c3', '\u22c3', '\u25bd', '\u25bd', '\U0001d535', '\U0001d535', '\u27fa', '\u27fa', '\u27f7', '\u27f7', '\u03be', '\u03be', '\u27f8', '\u27f8', '\u27f5', '\u27f5', '\u27fc', '\u27fc', '\u22fb', '\u22fb', '\u2a00', '\u2a00', '\U0001d569', '\U0001d569', '\u2a01', '\u2a01', '\u2a02', '\u2a02', '\u27f9', '\u27f9', '\u27f6', '\u27f6', '\U0001d4cd', '\U0001d4cd', '\u2a06', '\u2a06', '\u2a04',
7909 '\u2a04', '\u25b3', '\u25b3', '\u22c1', '\u22c1', '\u22c0', '\u22c0', '\u00fd', '\u00fd', '\u044f', '\u044f', '\u0177', '\u0177', '\u044b', '\u044b', '\u00a5', '\u00a5', '\U0001d536', '\U0001d536', '\u0457', '\u0457', '\U0001d56a', '\U0001d56a', '\U0001d4ce', '\U0001d4ce', '\u044e', '\u044e', '\u00ff', '\u00ff', '\u017a', '\u017a', '\u017e', '\u017e', '\u0437', '\u0437', '\u017c', '\u017c', '\u2128',
7910 '\u2128', '\u03b6', '\u03b6', '\U0001d537', '\U0001d537', '\u0436', '\u0436', '\u21dd', '\u21dd', '\U0001d56b', '\U0001d56b', '\U0001d4cf', '\U0001d4cf', '\u200d', '\u200d', '\u200c', '\u200c', ];
7911 
7912 
7913 
7914 
7915 
7916 
7917 
7918 
7919 
7920 
7921 
7922 
7923 
7924 
7925 
7926 
7927 
7928 
7929 
7930 
7931 
7932 
7933 
7934 // dom event support, if you want to use it
7935 
7936 /// used for DOM events
7937 version(dom_with_events)
7938 alias EventHandler = void delegate(Element handlerAttachedTo, Event event);
7939 
7940 /// This is a DOM event, like in javascript. Note that this library never fires events - it is only here for you to use if you want it.
7941 version(dom_with_events)
7942 class Event {
7943 	this(string eventName, Element target) {
7944 		this.eventName = eventName;
7945 		this.srcElement = target;
7946 	}
7947 
7948 	/// Prevents the default event handler (if there is one) from being called
7949 	void preventDefault() {
7950 		defaultPrevented = true;
7951 	}
7952 
7953 	/// Stops the event propagation immediately.
7954 	void stopPropagation() {
7955 		propagationStopped = true;
7956 	}
7957 
7958 	bool defaultPrevented;
7959 	bool propagationStopped;
7960 	string eventName;
7961 
7962 	Element srcElement;
7963 	alias srcElement target;
7964 
7965 	Element relatedTarget;
7966 
7967 	int clientX;
7968 	int clientY;
7969 
7970 	int button;
7971 
7972 	bool isBubbling;
7973 
7974 	/// this sends it only to the target. If you want propagation, use dispatch() instead.
7975 	void send() {
7976 		if(srcElement is null)
7977 			return;
7978 
7979 		auto e = srcElement;
7980 
7981 		if(eventName in e.bubblingEventHandlers)
7982 		foreach(handler; e.bubblingEventHandlers[eventName])
7983 			handler(e, this);
7984 
7985 		if(!defaultPrevented)
7986 			if(eventName in e.defaultEventHandlers)
7987 				e.defaultEventHandlers[eventName](e, this);
7988 	}
7989 
7990 	/// this dispatches the element using the capture -> target -> bubble process
7991 	void dispatch() {
7992 		if(srcElement is null)
7993 			return;
7994 
7995 		// first capture, then bubble
7996 
7997 		Element[] chain;
7998 		Element curr = srcElement;
7999 		while(curr) {
8000 			auto l = curr;
8001 			chain ~= l;
8002 			curr = curr.parentNode;
8003 
8004 		}
8005 
8006 		isBubbling = false;
8007 
8008 		foreach(e; chain.retro()) {
8009 			if(eventName in e.capturingEventHandlers)
8010 			foreach(handler; e.capturingEventHandlers[eventName])
8011 				handler(e, this);
8012 
8013 			// the default on capture should really be to always do nothing
8014 
8015 			//if(!defaultPrevented)
8016 			//	if(eventName in e.defaultEventHandlers)
8017 			//		e.defaultEventHandlers[eventName](e.element, this);
8018 
8019 			if(propagationStopped)
8020 				break;
8021 		}
8022 
8023 		isBubbling = true;
8024 		if(!propagationStopped)
8025 		foreach(e; chain) {
8026 			if(eventName in e.bubblingEventHandlers)
8027 			foreach(handler; e.bubblingEventHandlers[eventName])
8028 				handler(e, this);
8029 
8030 			if(propagationStopped)
8031 				break;
8032 		}
8033 
8034 		if(!defaultPrevented)
8035 		foreach(e; chain) {
8036 				if(eventName in e.defaultEventHandlers)
8037 					e.defaultEventHandlers[eventName](e, this);
8038 		}
8039 	}
8040 }
8041 
8042 struct FormFieldOptions {
8043 	// usable for any
8044 
8045 	/// this is a regex pattern used to validate the field
8046 	string pattern;
8047 	/// must the field be filled in? Even with a regex, it can be submitted blank if this is false.
8048 	bool isRequired;
8049 	/// this is displayed as an example to the user
8050 	string placeholder;
8051 
8052 	// usable for numeric ones
8053 
8054 
8055 	// convenience methods to quickly get some options
8056 	@property static FormFieldOptions none() {
8057 		FormFieldOptions f;
8058 		return f;
8059 	}
8060 
8061 	static FormFieldOptions required() {
8062 		FormFieldOptions f;
8063 		f.isRequired = true;
8064 		return f;
8065 	}
8066 
8067 	static FormFieldOptions regex(string pattern, bool required = false) {
8068 		FormFieldOptions f;
8069 		f.pattern = pattern;
8070 		f.isRequired = required;
8071 		return f;
8072 	}
8073 
8074 	static FormFieldOptions fromElement(Element e) {
8075 		FormFieldOptions f;
8076 		if(e.hasAttribute("required"))
8077 			f.isRequired = true;
8078 		if(e.hasAttribute("pattern"))
8079 			f.pattern = e.pattern;
8080 		if(e.hasAttribute("placeholder"))
8081 			f.placeholder = e.placeholder;
8082 		return f;
8083 	}
8084 
8085 	Element applyToElement(Element e) {
8086 		if(this.isRequired)
8087 			e.required = "required";
8088 		if(this.pattern.length)
8089 			e.pattern = this.pattern;
8090 		if(this.placeholder.length)
8091 			e.placeholder = this.placeholder;
8092 		return e;
8093 	}
8094 }
8095 
8096 // this needs to look just like a string, but can expand as needed
8097 version(no_dom_stream)
8098 alias string Utf8Stream;
8099 else
8100 class Utf8Stream {
8101 	protected:
8102 		// these two should be overridden in subclasses to actually do the stream magic
8103 		string getMore() {
8104 			if(getMoreHelper !is null)
8105 				return getMoreHelper();
8106 			return null;
8107 		}
8108 
8109 		bool hasMore() {
8110 			if(hasMoreHelper !is null)
8111 				return hasMoreHelper();
8112 			return false;
8113 		}
8114 		// the rest should be ok
8115 
8116 	public:
8117 		this(string d) {
8118 			this.data = d;
8119 		}
8120 
8121 		this(string delegate() getMoreHelper, bool delegate() hasMoreHelper) {
8122 			this.getMoreHelper = getMoreHelper;
8123 			this.hasMoreHelper = hasMoreHelper;
8124 
8125 			if(hasMore())
8126 				this.data ~= getMore();
8127 
8128 			// stdout.flush();
8129 		}
8130 
8131 		@property final size_t length() {
8132 			// the parser checks length primarily directly before accessing the next character
8133 			// so this is the place we'll hook to append more if possible and needed.
8134 			if(lastIdx + 1 >= data.length && hasMore()) {
8135 				data ~= getMore();
8136 			}
8137 			return data.length;
8138 		}
8139 
8140 		final char opIndex(size_t idx) {
8141 			if(idx > lastIdx)
8142 				lastIdx = idx;
8143 			return data[idx];
8144 		}
8145 
8146 		final string opSlice(size_t start, size_t end) {
8147 			if(end > lastIdx)
8148 				lastIdx = end;
8149 			return data[start .. end];
8150 		}
8151 
8152 		final size_t opDollar() {
8153 			return length();
8154 		}
8155 
8156 		final Utf8Stream opBinary(string op : "~")(string s) {
8157 			this.data ~= s;
8158 			return this;
8159 		}
8160 
8161 		final Utf8Stream opOpAssign(string op : "~")(string s) {
8162 			this.data ~= s;
8163 			return this;
8164 		}
8165 
8166 		final Utf8Stream opAssign(string rhs) {
8167 			this.data = rhs;
8168 			return this;
8169 		}
8170 	private:
8171 		string data;
8172 
8173 		size_t lastIdx;
8174 
8175 		bool delegate() hasMoreHelper;
8176 		string delegate() getMoreHelper;
8177 
8178 
8179 		/+
8180 		// used to maybe clear some old stuff
8181 		// you might have to remove elements parsed with it too since they can hold slices into the
8182 		// old stuff, preventing gc
8183 		void dropFront(int bytes) {
8184 			posAdjustment += bytes;
8185 			data = data[bytes .. $];
8186 		}
8187 
8188 		int posAdjustment;
8189 		+/
8190 }
8191 
8192 void fillForm(T)(Form form, T obj, string name) {
8193 	import arsd.database;
8194 	fillData((k, v) => form.setValue(k, v), obj, name);
8195 }
8196 
8197 /++
8198 	Normalizes the whitespace in the given text according to HTML rules.
8199 
8200 	History:
8201 		Added March 25, 2022 (dub v10.8)
8202 +/
8203 string normalizeWhitespace(string text) {
8204 	string ret;
8205 	ret.reserve(text.length);
8206 	bool lastWasWhite = true;
8207 	foreach(char ch; text) {
8208 		if(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
8209 			if(lastWasWhite)
8210 				continue;
8211 			lastWasWhite = true;
8212 			ch = ' ';
8213 		} else {
8214 			lastWasWhite = false;
8215 		}
8216 
8217 		ret ~= ch;
8218 	}
8219 
8220 	return ret.stripRight;
8221 }
8222 
8223 unittest {
8224 	assert(normalizeWhitespace("    foo   ") == "foo");
8225 	assert(normalizeWhitespace("    f\n \t oo   ") == "f oo");
8226 }
8227 
8228 unittest {
8229 	Document document;
8230 
8231 	document = new Document("<test> foo \r </test>");
8232 	assert(document.root.visibleText == "foo");
8233 
8234 	document = new Document("<test> foo \r <br>hi</test>");
8235 	assert(document.root.visibleText == "foo\nhi");
8236 
8237 	document = new Document("<test> foo \r <br>hi<pre>hi\nthere\n    indent<br />line</pre></test>");
8238 	assert(document.root.visibleText == "foo\nhihi\nthere\n    indent\nline", document.root.visibleText);
8239 }
8240 
8241 /+
8242 /+
8243 Syntax:
8244 
8245 Tag: tagname#id.class
8246 Tree: Tag(Children, comma, separated...)
8247 Children: Tee or Variable
8248 Variable: $varname with optional |funcname following.
8249 
8250 If a variable has a tree after it, it breaks the variable down:
8251 	* if array, foreach it does the tree
8252 	* if struct, it breaks down the member variables
8253 
8254 stolen from georgy on irc, see: https://github.com/georgy7/stringplate
8255 +/
8256 struct Stringplate {
8257 	/++
8258 
8259 	+/
8260 	this(string s) {
8261 
8262 	}
8263 
8264 	/++
8265 
8266 	+/
8267 	Element expand(T...)(T vars) {
8268 		return null;
8269 	}
8270 }
8271 ///
8272 unittest {
8273 	auto stringplate = Stringplate("#bar(.foo($foo), .baz($baz))");
8274 	assert(stringplate.expand.innerHTML == `<div id="bar"><div class="foo">$foo</div><div class="baz">$baz</div></div>`);
8275 }
8276 +/
8277 
8278 bool allAreInlineHtml(const(Element)[] children, const string[] inlineElements) {
8279 	foreach(child; children) {
8280 		if(child.nodeType == NodeType.Text && child.nodeValue.strip.length) {
8281 			// cool
8282 		} else if(child.tagName.isInArray(inlineElements) && allAreInlineHtml(child.children, inlineElements)) {
8283 			// cool, this is an inline element and none of its children contradict that
8284 		} else {
8285 			// prolly block
8286 			return false;
8287 		}
8288 	}
8289 	return true;
8290 }
8291 
8292 private bool isSimpleWhite(dchar c) {
8293 	return c == ' ' || c == '\r' || c == '\n' || c == '\t';
8294 }
8295 
8296 unittest {
8297 	// Test for issue #120
8298 	string s = `<html>
8299 	<body>
8300 		<P>AN
8301 		<P>bubbles</P>
8302 		<P>giggles</P>
8303 	</body>
8304 </html>`;
8305 	auto doc = new Document();
8306 	doc.parseUtf8(s, false, false);
8307 	auto s2 = doc.toString();
8308 	assert(
8309 			s2.indexOf("bubbles") < s2.indexOf("giggles"),
8310 			"paragraph order incorrect:\n" ~ s2);
8311 }
8312 
8313 unittest {
8314 	// test for suncarpet email dec 24 2019
8315 	// arbitrary id asduiwh
8316 	auto document = new Document("<html>
8317         <head>
8318                 <meta charset=\"utf-8\"></meta>
8319                 <title>Element.querySelector Test</title>
8320         </head>
8321         <body>
8322                 <div id=\"foo\">
8323                         <div>Foo</div>
8324                         <div>Bar</div>
8325                 </div>
8326 		<div id=\"empty\"></div>
8327 		<div id=\"empty-but-text\">test</div>
8328         </body>
8329 </html>");
8330 
8331 	auto doc = document;
8332 
8333 	{
8334 	auto empty = doc.requireElementById("empty");
8335 	assert(empty.querySelector(" > *") is null, empty.querySelector(" > *").toString);
8336 	}
8337 	{
8338 	auto empty = doc.requireElementById("empty-but-text");
8339 	assert(empty.querySelector(" > *") is null, empty.querySelector(" > *").toString);
8340 	}
8341 
8342 	assert(doc.querySelectorAll("div div").length == 2);
8343 	assert(doc.querySelector("div").querySelectorAll("div").length == 2);
8344 	assert(doc.querySelectorAll("> html").length == 0);
8345 	assert(doc.querySelector("head").querySelectorAll("> title").length == 1);
8346 	assert(doc.querySelector("head").querySelectorAll("> meta[charset]").length == 1);
8347 
8348 
8349 	assert(doc.root.matches("html"));
8350 	assert(!doc.root.matches("nothtml"));
8351 	assert(doc.querySelector("#foo > div").matches("div"));
8352 	assert(doc.querySelector("body > #foo").matches("#foo"));
8353 
8354 	assert(doc.root.querySelectorAll(":root > body").length == 0); // the root has no CHILD root!
8355 	assert(doc.querySelectorAll(":root > body").length == 1); // but the DOCUMENT does
8356 	assert(doc.querySelectorAll(" > body").length == 1); //  should mean the same thing
8357 	assert(doc.root.querySelectorAll(" > body").length == 1); // the root of HTML has this
8358 	assert(doc.root.querySelectorAll(" > html").length == 0); // but not this
8359 
8360 	// also confirming the querySelector works via the mdn definition
8361 	auto foo = doc.requireSelector("#foo");
8362 	assert(foo.querySelector("#foo > div") !is null);
8363 	assert(foo.querySelector("body #foo > div") !is null);
8364 
8365 	// this is SUPPOSED to work according to the spec but never has in dom.d since it limits the scope.
8366 	// the new css :scope thing is designed to bring this in. and meh idk if i even care.
8367 	//assert(foo.querySelectorAll("#foo > div").length == 2);
8368 }
8369 
8370 unittest {
8371 	// based on https://developer.mozilla.org/en-US/docs/Web/API/Element/closest example
8372 	auto document = new Document(`<article>
8373   <div id="div-01">Here is div-01
8374     <div id="div-02">Here is div-02
8375       <div id="div-03">Here is div-03</div>
8376     </div>
8377   </div>
8378 </article>`, true, true);
8379 
8380 	auto el = document.getElementById("div-03");
8381 	assert(el.closest("#div-02").id == "div-02");
8382 	assert(el.closest("div div").id == "div-03");
8383 	assert(el.closest("article > div").id == "div-01");
8384 	assert(el.closest(":not(div)").tagName == "article");
8385 
8386 	assert(el.closest("p") is null);
8387 	assert(el.closest("p, div") is el);
8388 }
8389 
8390 unittest {
8391 	// https://developer.mozilla.org/en-US/docs/Web/CSS/:is
8392 	auto document = new Document(`<test>
8393 		<div class="foo"><p>cool</p><span>bar</span></div>
8394 		<main><p>two</p></main>
8395 	</test>`);
8396 
8397 	assert(document.querySelectorAll(":is(.foo, main) p").length == 2);
8398 	assert(document.querySelector("div:where(.foo)") !is null);
8399 }
8400 
8401 unittest {
8402 immutable string html = q{
8403 <root>
8404 <div class="roundedbox">
8405  <table>
8406   <caption class="boxheader">Recent Reviews</caption>
8407   <tr>
8408    <th>Game</th>
8409    <th>User</th>
8410    <th>Rating</th>
8411    <th>Created</th>
8412   </tr>
8413 
8414   <tr>
8415    <td>June 13, 2020 15:10</td>
8416    <td><a href="/reviews/8833">[Show]</a></td>
8417   </tr>
8418 
8419   <tr>
8420    <td>June 13, 2020 15:02</td>
8421    <td><a href="/reviews/8832">[Show]</a></td>
8422   </tr>
8423 
8424   <tr>
8425    <td>June 13, 2020 14:41</td>
8426    <td><a href="/reviews/8831">[Show]</a></td>
8427   </tr>
8428  </table>
8429 </div>
8430 </root>
8431 };
8432 
8433   auto doc = new Document(cast(string)html);
8434   // this should select the second table row, but...
8435   auto rd = doc.root.querySelector(`div.roundedbox > table > caption.boxheader + tr + tr + tr > td > a[href^=/reviews/]`);
8436   assert(rd !is null);
8437   assert(rd.href == "/reviews/8832");
8438 
8439   rd = doc.querySelector(`div.roundedbox > table > caption.boxheader + tr + tr + tr > td > a[href^=/reviews/]`);
8440   assert(rd !is null);
8441   assert(rd.href == "/reviews/8832");
8442 }
8443 
8444 unittest {
8445 	try {
8446 		auto doc = new XmlDocument("<testxmlns:foo=\"/\"></test>");
8447 		assert(0);
8448 	} catch(Exception e) {
8449 		// good; it should throw an exception, not an error.
8450 	}
8451 }
8452 
8453 unittest {
8454 	// toPrettyString is not stable, but these are some best-effort attempts
8455 	// despite these being in a test, I might change these anyway!
8456 	assert(Element.make("a").toPrettyString == "<a></a>");
8457 	assert(Element.make("a", "").toPrettyString(false, 0, " ") == "<a></a>");
8458 	assert(Element.make("a", " ").toPrettyString(false, 0, " ") == "<a> </a>");//, Element.make("a", " ").toPrettyString(false, 0, " "));
8459 	assert(Element.make("a", "b").toPrettyString == "<a>b</a>");
8460 	assert(Element.make("a", "b").toPrettyString(false, 0, "") == "<a>b</a>");
8461 
8462 	{
8463 	auto document = new Document("<html><body><p>hello <a href=\"world\">world</a></p></body></html>");
8464 	auto pretty = document.toPrettyString(false, 0, "  ");
8465 	assert(pretty ==
8466 `<!DOCTYPE html>
8467 <html>
8468   <body>
8469     <p>hello <a href="world">world</a></p>
8470   </body>
8471 </html>`, pretty);
8472 	}
8473 
8474 	{
8475 	auto document = new XmlDocument("<html><body><p>hello <a href=\"world\">world</a></p></body></html>");
8476 	assert(document.toPrettyString(false, 0, "  ") ==
8477 `<?xml version="1.0" encoding="UTF-8"?>
8478 <html>
8479   <body>
8480     <p>
8481       hello
8482       <a href="world">world</a>
8483     </p>
8484   </body>
8485 </html>`);
8486 	}
8487 
8488 	foreach(test; [
8489 		"<a att=\"http://ele\"><b><ele1>Hello</ele1>\n  <c>\n   <d>\n    <ele2>How are you?</ele2>\n   </d>\n   <e>\n    <ele3>Good &amp; you?</ele3>\n   </e>\n  </c>\n </b>\n</a>",
8490 		"<a att=\"http://ele\"><b><ele1>Hello</ele1><c><d><ele2>How are you?</ele2></d><e><ele3>Good &amp; you?</ele3></e></c></b></a>",
8491 	] )
8492 	{
8493 	auto document = new XmlDocument(test);
8494 	assert(document.root.toPrettyString(false, 0, " ") == "<a att=\"http://ele\">\n <b>\n  <ele1>Hello</ele1>\n  <c>\n   <d>\n    <ele2>How are you?</ele2>\n   </d>\n   <e>\n    <ele3>Good &amp; you?</ele3>\n   </e>\n  </c>\n </b>\n</a>");
8495 	assert(document.toPrettyString(false, 0, " ") == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<a att=\"http://ele\">\n <b>\n  <ele1>Hello</ele1>\n  <c>\n   <d>\n    <ele2>How are you?</ele2>\n   </d>\n   <e>\n    <ele3>Good &amp; you?</ele3>\n   </e>\n  </c>\n </b>\n</a>");
8496 	auto omg = document.root;
8497 	omg.parent_ = null;
8498 	assert(omg.toPrettyString(false, 0, " ") == "<a att=\"http://ele\">\n <b>\n  <ele1>Hello</ele1>\n  <c>\n   <d>\n    <ele2>How are you?</ele2>\n   </d>\n   <e>\n    <ele3>Good &amp; you?</ele3>\n   </e>\n  </c>\n </b>\n</a>");
8499 	}
8500 
8501 	{
8502 	auto document = new XmlDocument(`<a><b>toto</b><c></c></a>`);
8503 	assert(document.root.toPrettyString(false, 0, null) == `<a><b>toto</b><c></c></a>`);
8504 	assert(document.root.toPrettyString(false, 0, " ") == `<a>
8505  <b>toto</b>
8506  <c></c>
8507 </a>`);
8508 	}
8509 
8510 	{
8511 auto str = `<!DOCTYPE html>
8512 <html>
8513 	<head>
8514 		<title>Test</title>
8515 	</head>
8516 	<body>
8517 		<p>Hello there</p>
8518 		<p>I like <a href="">Links</a></p>
8519 		<div>
8520 			this is indented since there's a block inside
8521 			<p>this is the block</p>
8522 			and this gets its own line
8523 		</div>
8524 	</body>
8525 </html>`;
8526 		auto doc = new Document(str, true, true);
8527 		assert(doc.toPrettyString == str);
8528 	}
8529 }
8530 
8531 unittest {
8532 	auto document = new Document("<foo><items><item><title>test</title><desc>desc</desc></item></items></foo>");
8533 	auto items = document.root.requireSelector("> items");
8534 	auto item = items.requireSelector("> item");
8535 	auto title = item.requireSelector("> title");
8536 
8537 	// this not actually implemented at this point but i might want to later. it prolly should work as an extension of the standard behavior
8538 	// assert(title.requireSelector("~ desc").innerText == "desc");
8539 
8540 	assert(item.requireSelector("title ~ desc").innerText == "desc");
8541 
8542 	assert(items.querySelector("item:has(title)") !is null);
8543 	assert(items.querySelector("item:has(nothing)") is null);
8544 
8545 	assert(title.innerText == "test");
8546 }
8547 
8548 unittest {
8549 	auto document = new Document("broken"); // just ensuring it doesn't crash
8550 }
8551 
8552 unittest {
8553 	bool encountered;
8554 	class StreamDocument : Document {
8555 		override void processNodeWhileParsing(Element parent, Element child) {
8556 			// import std.stdio; writeln("Processing: ", child);
8557 			if(child.tagName == "bar")
8558 				encountered = true;
8559 		}
8560 
8561 		this() {
8562 			super("<foo><bar></bar></foo>");
8563 		}
8564 	}
8565 
8566 	auto test = new StreamDocument();
8567 	assert(encountered); // it should have been seen
8568 	assert(test.querySelector("bar") is null); // but not appended to the dom node
8569 }
8570 
8571 /*
8572 Copyright: Adam D. Ruppe, 2010 - 2023
8573 License:   <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
8574 Authors: Adam D. Ruppe, with contributions by Nick Sabalausky, Trass3r, and ketmar among others
8575 */