1 module dietc.parser;
2 
3 import dietc.lexer;
4 
5 import std.algorithm;
6 import std.conv : to, text;
7 import std.meta : AliasSeq;
8 
9 alias ASTClasses = AliasSeq!(Document, HiddenComment, Comment, DStatement, DietFilter, TagNode, TagNode.AttributeAST,
10 		RawAssignment, Assignment, StringTagContents, TextLine, XMLNode, PipeText, Expression, TextLine.PartAST);
11 
12 interface AST
13 {
14 	Token token() @property;
15 	void accept(ASTVisitor visitor);
16 }
17 
18 abstract class ASTVisitor
19 {
20 	static foreach (T; ASTClasses)
21 		void visit(T ast) in (ast !is null)
22 		{
23 			ast.accept(this);
24 		}
25 
26 	void visit(AST ast) in (ast !is null)
27 	{
28 		static foreach (T; ASTClasses)
29 			if (cast(T) ast)
30 				return visit(cast(T) ast);
31 		throw new Exception("Unknown ast passed?!");
32 	}
33 }
34 
35 enum VisitResult
36 {
37 	continue_,
38 	recurse,
39 	return_
40 }
41 
42 alias VisitorDelegate = VisitResult delegate(AST node, AST parent);
43 
44 void traverse(AST node, VisitorDelegate callback)
45 {
46 	static class VisitorImpl : ASTVisitor
47 	{
48 		static foreach (T; ASTClasses)
49 			override void visit(T ast)
50 			{
51 				if (result == VisitResult.return_)
52 					return;
53 
54 				result = callback(ast, parents[$ - 1]);
55 				if (result == VisitResult.return_)
56 					return;
57 
58 				if (result == VisitResult.recurse)
59 					{
60 					parents ~= ast;
61 					scope (exit)
62 						parents.length--;
63 					ast.accept(this);
64 				}
65 			}
66 
67 		alias visit = ASTVisitor.visit;
68 
69 		VisitResult result;
70 		AST[] parents;
71 		VisitorDelegate callback;
72 
73 		this(AST node, VisitorDelegate callback)
74 		{
75 			parents = [node];
76 			this.callback = callback;
77 		}
78 	}
79 
80 	node.accept(new VisitorImpl(node, callback));
81 }
82 
83 class Document : AST, Node
84 {
85 	Node[] _children;
86 	Token _token;
87 
88 	Node[] children() @property
89 	{
90 		return _children;
91 	}
92 
93 	void addChild(Node child)
94 	{
95 		_token.range[1] = child.token.range[1];
96 		_children ~= child;
97 	}
98 
99 	Token token() @property
100 	{
101 		return _token;
102 	}
103 
104 	void accept(ASTVisitor visitor)
105 	{
106 		foreach (child; _children)
107 			if (child)
108 				visitor.visit(child);
109 	}
110 
111 	this(Token token)
112 	{
113 		_token = token;
114 	}
115 
116 	override string toString() const
117 	{
118 		string ret = "Document(";
119 		foreach (child; _children)
120 			ret ~= "\n" ~ child.to!string.indent;
121 		return ret ~= ")";
122 	}
123 }
124 
125 interface Node : AST
126 {
127 	Node[] children() @property;
128 	void addChild(Node);
129 }
130 
131 interface INamed
132 {
133 	string name() @property;
134 }
135 
136 interface IStringContainer : AST
137 {
138 	string content() @property;
139 }
140 
141 abstract class StringNode : Node, IStringContainer
142 {
143 	Token _token;
144 	string _content;
145 	Node[] _children;
146 
147 	Node[] children() @property
148 	{
149 		return _children;
150 	}
151 
152 	void addChild(Node child)
153 	{
154 		_token.range[1] = child.token.range[1];
155 		_children ~= child;
156 	}
157 
158 	Token token() @property
159 	{
160 		return _token;
161 	}
162 
163 	void accept(ASTVisitor visitor)
164 	{
165 		foreach (child; _children)
166 			if (child)
167 				visitor.visit(child);
168 	}
169 
170 	string content() @property
171 	{
172 		return _content;
173 	}
174 
175 	this(Token token, string content)
176 	{
177 		_token = token;
178 		_content = content;
179 	}
180 
181 	override string toString()
182 	{
183 		import std.array : join;
184 
185 		string ret = text('(', (cast(Object)this).classinfo.name, `) "`, content, '"');
186 		foreach (child; children)
187 			ret ~= "\n" ~ child.to!string.indent;
188 		return ret;
189 	}
190 }
191 
192 class Comment : StringNode
193 {
194 	this(Token token, string content)
195 	{
196 		super(token, content);
197 	}
198 }
199 
200 class HiddenComment : Comment
201 {
202 	this(Token token, string content)
203 	{
204 		super(token, content);
205 	}
206 }
207 
208 class DStatement : StringNode
209 {
210 	this(Token token, string content)
211 	{
212 		super(token, content);
213 	}
214 }
215 
216 class DietFilter : StringNode, INamed
217 {
218 	string _name;
219 
220 	string name() @property
221 	{
222 		return _name;
223 	}
224 
225 	this(Token token, string name, string content)
226 	{
227 		super(token, content);
228 		_name = name;
229 	}
230 }
231 
232 interface NestedTags : Node, TagContents
233 {
234 }
235 
236 class TagNode : NestedTags, INamed
237 {
238 	// NormalTagStart (TextBlock | NestedStart NestedTags | TagContents)
239 	// NormalTagStart: TAG_IDENTIFIER? ( NodeID | NodeClass )* Attributes? ( FitInside? FitOutside? | FitOutside FitInside ) Translated?
240 	// TagContents: RawAssignment | Assignment | ' '? TextLine | EOL
241 
242 	struct Attribute
243 	{
244 		Token name;
245 		Expression expr;
246 
247 		string toString()
248 		{
249 			return name.content ~ name.range.to!string ~ (expr ? "='" ~ expr.content ~ "'" : "×");
250 		}
251 	}
252 
253 	/// Wrapper class around Attribute used for visiting
254 	class AttributeAST : AST, INamed
255 	{
256 		Attribute attribute;
257 
258 		this(Attribute attribute)
259 		{
260 			this.attribute = attribute;
261 		}
262 
263 		Token token() @property
264 		{
265 			auto tok = attribute.name;
266 			if (attribute.expr)
267 				tok.range[1] = attribute.expr.token.range[1];
268 			return tok;
269 		}
270 
271 		string name() @property
272 		{
273 			return attribute.name.content;
274 		}
275 
276 		void accept(ASTVisitor visitor)
277 		{
278 			if (attribute.expr)
279 				visitor.visit(attribute.expr);
280 		}
281 
282 		override string toString()
283 		{
284 			return attribute.toString();
285 		}
286 	}
287 
288 	Token[] directIDs, directClasses;
289 	Attribute[] attributes;
290 	Token _fitInside, _fitOutside, _translated;
291 	Token _token, _tag;
292 	TagContents _contents;
293 	Node[] _children;
294 	size_t[2] attributesRange;
295 
296 	Node[] children() @property
297 	{
298 		return _children;
299 	}
300 
301 	void accept(ASTVisitor visitor)
302 	{
303 		foreach (attr; attributes)
304 			visitor.visit(new AttributeAST(attr));
305 		if (_contents !is null)
306 			visitor.visit(_contents);
307 		foreach (child; _children)
308 			if (child)
309 				visitor.visit(child);
310 	}
311 
312 	void addChild(Node child)
313 	{
314 		_token.range[1] = child.token.range[1];
315 		_children ~= child;
316 	}
317 
318 	Token tag() @property
319 	{
320 		return _tag;
321 	}
322 
323 	Token token() @property
324 	{
325 		return _token;
326 	}
327 
328 	/// Returns: the tag name.
329 	string name() @property
330 	{
331 		return _tag.content;
332 	}
333 
334 	/// Returns: true if the `<` whitespace modifier token is present
335 	bool fitInside() @property
336 	{
337 		return _fitInside != Token.init;
338 	}
339 
340 	/// Returns: true if the `>` whitespace modifier token is present
341 	bool fitOutside() @property
342 	{
343 		return _fitOutside != Token.init;
344 	}
345 
346 	/// Returns: true if the `&` token is present
347 	bool translated() @property
348 	{
349 		return _translated != Token.init;
350 	}
351 
352 	/// Content of the tag (nullable)
353 	TagContents contents() @property
354 	{
355 		return _contents;
356 	}
357 
358 	this(Token tag)
359 	{
360 		this(tag, null);
361 	}
362 
363 	this(Token tag, TagContents contents)
364 	{
365 		_tag = _token = tag;
366 		_contents = contents;
367 	}
368 
369 	override string toString()
370 	{
371 		import std.array : join;
372 
373 		string ret = "tag" ~ token.range.to!string ~ "<" ~ _tag.range.to!string ~ name ~ ">";
374 		foreach (c; directClasses)
375 			ret ~= c.content;
376 		foreach (id; directIDs)
377 			ret ~= id.content;
378 		ret ~= "(";
379 		ret ~= attributes.to!(string[]).join(", ");
380 		ret ~= ")" ~ attributesRange.to!string;
381 		if (fitOutside)
382 			ret ~= ">";
383 		if (fitInside)
384 			ret ~= "<";
385 		if (translated)
386 			ret ~= "&";
387 		if (_contents !is null)
388 			ret ~= " = " ~ contents.to!string;
389 		foreach (child; children)
390 			ret ~= "\n" ~ child.to!string.indent;
391 		return ret;
392 	}
393 }
394 
395 interface TagContents : AST
396 {
397 }
398 
399 class StringTagContents : TagContents, IStringContainer
400 {
401 	Token _token;
402 	string _content;
403 
404 	Token token() @property
405 	{
406 		return _token;
407 	}
408 
409 	void accept(ASTVisitor)
410 	{
411 	}
412 
413 	string content() @property
414 	{
415 		return _content;
416 	}
417 
418 	this(Token token, string content)
419 	{
420 		_token = token;
421 		_content = content;
422 	}
423 
424 	override string toString()
425 	{
426 		import std.array : join;
427 
428 		string ret = text('(', (cast(Object)this).classinfo.name, `) "`, content, '"');
429 		return ret;
430 	}
431 }
432 
433 class Assignment : StringTagContents
434 {
435 	this(Token token, string content)
436 	{
437 		super(token, content);
438 	}
439 }
440 
441 class RawAssignment : Assignment
442 {
443 	this(Token token, string content)
444 	{
445 		super(token, content);
446 	}
447 }
448 
449 class TextLine : TagContents
450 {
451 	struct Part
452 	{
453 		Token token;
454 		string raw;
455 		Expression inlineExpr;
456 		NestedTags inlineTag;
457 		bool escapeInlineExpr;
458 
459 		string toString() const
460 		{
461 			string pre = token.range.to!string;
462 			if (raw.length)
463 				return pre ~ raw;
464 			else if (inlineExpr !is null)
465 				return pre ~ (escapeInlineExpr ? "#" : "!") ~ "{" ~ inlineExpr.to!string ~ "}";
466 			else if (inlineTag !is null)
467 				return pre ~ "#[" ~ inlineTag.to!string ~ "]";
468 			else
469 				return pre;
470 		}
471 	}
472 
473 	/// Wrapper class around Part used for visiting
474 	class PartAST : AST
475 	{
476 		Part part;
477 
478 		this(Part part)
479 		{
480 			this.part = part;
481 		}
482 
483 		Token token() @property
484 		{
485 			return part.token;
486 		}
487 
488 		void accept(ASTVisitor visitor)
489 		{
490 			if (part.inlineExpr)
491 				visitor.visit(part.inlineExpr);
492 			if (part.inlineTag)
493 				visitor.visit(part.inlineTag);
494 		}
495 
496 		override string toString() const
497 		{
498 			return part.toString();
499 		}
500 	}
501 
502 	Token _token;
503 	Part[] _parts;
504 
505 	void accept(ASTVisitor visitor)
506 	{
507 		foreach (part; _parts)
508 			visitor.visit(new PartAST(part));
509 	}
510 
511 	Token token() @property
512 	{
513 		return _token;
514 	}
515 
516 	this(Token token, Part[] parts)
517 	{
518 		_token = token;
519 		_parts = parts;
520 	}
521 
522 	override string toString()
523 	{
524 		string ret = _token.range.to!string ~ "'";
525 		foreach (part; _parts)
526 			ret ~= '{' ~ part.toString ~ '}';
527 		return ret ~ "'";
528 	}
529 }
530 
531 class XMLNode : NestedTags
532 {
533 	TextLine _line;
534 	Node[] _children;
535 
536 	Node[] children() @property
537 	{
538 		return _children;
539 	}
540 
541 	void accept(ASTVisitor visitor)
542 	{
543 		foreach (child; _children)
544 			if (child)
545 				visitor.visit(child);
546 	}
547 
548 	void addChild(Node child)
549 	{
550 		_line.token.range[1] = child.token.range[1];
551 		_children ~= child;
552 	}
553 
554 	Token token() @property
555 	{
556 		return _line.token;
557 	}
558 
559 	this(TextLine line)
560 	{
561 		_line = line;
562 	}
563 }
564 
565 class PipeText : NestedTags
566 {
567 	Token _token;
568 	Token _translated;
569 	TagContents _content;
570 	Node[] _children;
571 
572 	Node[] children() @property
573 	{
574 		return _children;
575 	}
576 
577 	void accept(ASTVisitor visitor)
578 	{
579 		if (_content)
580 			visitor.visit(_content);
581 		foreach (child; _children)
582 			if (child)
583 				visitor.visit(child);
584 	}
585 
586 	void addChild(Node child)
587 	{
588 		_token.range[1] = child.token.range[1];
589 		_children ~= child;
590 	}
591 
592 	Token token() @property
593 	{
594 		return _token;
595 	}
596 
597 	bool translated() @property
598 	{
599 		return _translated != Token.init;
600 	}
601 
602 	TagContents content() @property
603 	{
604 		return _content;
605 	}
606 
607 	this(Token token, Token translated, TagContents content)
608 	{
609 		_token = token;
610 		_translated = translated;
611 		_content = content;
612 	}
613 }
614 
615 class Expression : AST
616 {
617 	Token _token;
618 	string _content;
619 
620 	Token token() @property
621 	{
622 		return _token;
623 	}
624 
625 	void accept(ASTVisitor)
626 	{
627 	}
628 
629 	string content() @property
630 	{
631 		return _content;
632 	}
633 
634 	this(Token token, string content)
635 	{
636 		_token = token;
637 		_content = content;
638 	}
639 
640 	override string toString()
641 	{
642 		return _content;
643 	}
644 }
645 
646 struct ASTParser
647 {
648 	DietInput input;
649 	Document root;
650 
651 	void parseDocument()
652 	{
653 		root = new Document(input.front);
654 		while (true)
655 		{
656 			auto n = parseNode();
657 			if (!n)
658 				break;
659 			root.addChild(n);
660 		}
661 		input.expect(TokenType.eof);
662 	}
663 
664 	Node parseNode()
665 	{
666 		auto past = input.save();
667 		auto val = parseNodeValue();
668 		if (val is null)
669 		{
670 			input = past;
671 			return null;
672 		}
673 		if (input.skipAll(TokenType.newline) == 0 && input.front.type != TokenType.eof)
674 		{
675 			input = past;
676 			return null;
677 		}
678 		if (input.peek(TokenType.indent))
679 		{
680 			input.popFront();
681 			while (true)
682 			{
683 				auto n = parseNode();
684 				if (!n)
685 					break;
686 				val.addChild(n);
687 			}
688 			input.expect(TokenType.detent);
689 		}
690 		return val;
691 	}
692 
693 	Node parseNodeValue()
694 	{
695 		if (auto comment = parseComment())
696 			return comment;
697 		if (auto statement = parseDStatement())
698 			return statement;
699 		if (auto filter = parseFilter())
700 			return filter;
701 		if (auto nested = parseNestedTags())
702 			return nested;
703 		return null;
704 	}
705 
706 	string parseText(bool multiline, bool allowIndent)
707 	{
708 		string ret;
709 		size_t indentation = 1;
710 		bool lastNewline;
711 		while (true)
712 		{
713 			auto v = input.front;
714 			if (lastNewline && v.type != TokenType.indent)
715 				break;
716 			if (v.type == TokenType.eof)
717 				break;
718 			if (v.type == TokenType.newline && (indentation == 1 || !multiline))
719 			{
720 				if (multiline)
721 				{
722 					lastNewline = true;
723 					input.popFront;
724 				}
725 				else
726 					break;
727 			}
728 			else
729 			{
730 				input.popFront;
731 				if (v.type == TokenType.indent)
732 				{
733 					if (indentation > 1 && !allowIndent)
734 						input.errors.error(input, v.range[0],
735 								"Can't indent here because it is already indented.");
736 					indentation++;
737 				}
738 				else if (v.type == TokenType.detent)
739 				{
740 					indentation--;
741 					if (indentation == 0)
742 						break;
743 				}
744 				else
745 				{
746 					ret ~= v.content;
747 				}
748 			}
749 		}
750 		return ret;
751 	}
752 
753 	Comment parseComment()
754 	{
755 		auto tok = input.front;
756 		if (input.matchText("//-"))
757 		{
758 			string content = parseText(true, true);
759 			tok.range[1] = input.front.range[0];
760 			return new HiddenComment(tok, content);
761 		}
762 		else if (input.matchText("//"))
763 		{
764 			string content = parseText(true, true);
765 			tok.range[1] = input.front.range[0];
766 			return new Comment(tok, content);
767 		}
768 		return null;
769 	}
770 
771 	DStatement parseDStatement()
772 	{
773 		auto tok = input.front;
774 		if (input.matchText("-"))
775 		{
776 			string content = parseText(false, false);
777 			tok.range[1] = input.front.range[0];
778 			return new DStatement(tok, content);
779 		}
780 		return null;
781 	}
782 
783 	DietFilter parseFilter()
784 	{
785 		auto startTok = input.front;
786 		if (input.matchText(":"))
787 		{
788 			auto tok = input.front;
789 			string name;
790 			if (input.expect(TokenType.identifier))
791 			{
792 				name = tok.content;
793 				if (!name.validateIdentifierAlpha)
794 					input.errors.expect(input, tok.range[0], "identifier of type [-_a-zA-Z][-_0-9a-zA-Z]*");
795 			}
796 			input.match(TokenType.whitespace);
797 			string text = parseText(true, true);
798 			return new DietFilter(startTok, name, text);
799 		}
800 		return null;
801 	}
802 
803 	NestedTags parseNestedTags()
804 	{
805 		if (auto comment = parseDoctype())
806 			return comment;
807 		if (auto statement = parseXML())
808 			return statement;
809 		if (auto filter = parsePipeText())
810 			return filter;
811 		if (auto nested = parseTag(true))
812 			return nested;
813 		return null;
814 	}
815 
816 	NestedTags parseSingleTag()
817 	{
818 		if (auto comment = parseDoctype())
819 			return comment;
820 		if (auto statement = parseXML())
821 			return statement;
822 		if (auto filter = parsePipeText())
823 			return filter;
824 		if (auto nested = parseTag(false))
825 			return nested;
826 		return null;
827 	}
828 
829 	TextLine parseTextLine()
830 	{
831 		DietInput fastForward(string inc, string dec)
832 		{
833 			auto c = input.save;
834 			int depth = 0;
835 			do
836 			{
837 				if (c.front.content == inc)
838 					depth++;
839 				else if (c.front.content == dec)
840 					depth--;
841 				c.popFront;
842 			}
843 			while (depth > 0 && c.front.type != TokenType.eof);
844 			return c;
845 		}
846 
847 		Token tok = input.front;
848 		TextLine.Part[] parts;
849 		string raw;
850 		size_t[2] rawRange = input.index;
851 		immutable size_t startIndex = input.front.range[0];
852 
853 		void flushRaw()
854 		{
855 			if (raw.length)
856 				parts ~= TextLine.Part(Token(TokenType.code, raw, rawRange), raw);
857 			raw = "";
858 		}
859 
860 		while (!input.front.type.among!(TokenType.eof, TokenType.newline))
861 		{
862 			auto v = input.front;
863 			input.popFront;
864 			if (v.content == "\\")
865 			{
866 				v = input.front;
867 				input.popFront;
868 				if (!v.content.among!("!", "\\", "#"))
869 					input.errors.expect(input, v.range[0], "escaped !, \\ or #");
870 				if (!raw.length)
871 					rawRange[0] = v.range[0];
872 				raw ~= v.content;
873 				rawRange[1] = input.index;
874 			}
875 			else if (v.content == "!")
876 			{
877 				if (input.front.content == "{")
878 				{
879 					flushRaw();
880 					auto load = fastForward("{", "}");
881 					input.code.length = load.index;
882 					auto start = v.range[0];
883 					input.popFront();
884 					auto expr = parseExpression;
885 					auto end = input.index;
886 					v.range = [start, end];
887 					parts ~= TextLine.Part(v, null, expr, null, true);
888 					input = load;
889 				}
890 				else
891 				{
892 					if (!raw.length)
893 						rawRange[0] = v.range[0];
894 					raw ~= v.content;
895 					rawRange[1] = input.index;
896 				}
897 			}
898 			else if (v.content == "#")
899 			{
900 				if (input.front.content == "{")
901 				{
902 					flushRaw();
903 					auto load = fastForward("{", "}");
904 					input.code.length = load.index;
905 					auto start = v.range[0];
906 					input.popFront();
907 					auto expr = parseExpression;
908 					auto end = input.index;
909 					v.range = [start, end];
910 					parts ~= TextLine.Part(v, null, expr, null, true);
911 					input = load;
912 				}
913 				else if (input.front.content == "[")
914 				{
915 					flushRaw();
916 					auto load = fastForward("[", "]");
917 					input.code.length = load.index;
918 					auto start = v.range[0];
919 					input.popFront();
920 					auto tag = parseSingleTag;
921 					auto end = input.index;
922 					v.range = [start, end];
923 					parts ~= TextLine.Part(v, null, null, tag);
924 					input = load;
925 				}
926 				else
927 				{
928 					if (!raw.length)
929 						rawRange[0] = v.range[0];
930 					raw ~= v.content;
931 					rawRange[1] = input.index;
932 				}
933 			}
934 			else if (v.type != TokenType.newline)
935 			{
936 				if (!raw.length)
937 					rawRange[0] = v.range[0];
938 				raw ~= v.content;
939 				rawRange[1] = input.index;
940 			}
941 			else
942 				break;
943 			tok.range[1] = input.index;
944 		}
945 		flushRaw();
946 		if (!parts.length && startIndex == input.front.range[0])
947 			return null;
948 		return new TextLine(tok, parts);
949 	}
950 
951 	TagNode parseDoctype()
952 	{
953 		auto tok = input.front;
954 		if (input.matchText("!!!"))
955 		{
956 			tok.content = "doctype";
957 			auto text = parseTextLine();
958 			return new TagNode(tok, text);
959 		}
960 		return null;
961 	}
962 
963 	XMLNode parseXML()
964 	{
965 		if (input.front.content == "<")
966 		{
967 			auto v = parseTextLine();
968 			return new XMLNode(v);
969 		}
970 		return null;
971 	}
972 
973 	PipeText parsePipeText()
974 	{
975 		auto tok = input.front;
976 		if (tok.content == "|")
977 		{
978 			input.popFront;
979 			Token translated;
980 			if (input.front.content == "&")
981 			{
982 				translated = input.front;
983 				input.popFront;
984 			}
985 			return new PipeText(tok, translated, parseTagContents());
986 		}
987 		return null;
988 	}
989 
990 	Token parseAttributeIdentifier()
991 	{
992 		import std.array : array;
993 		import std.range : retro, chain;
994 
995 		Token tok;
996 		tok.type = TokenType.code;
997 		tok.range = input.front.range;
998 		string ret;
999 		char[] stack;
1000 		Loop: while (!input.empty)
1001 		{
1002 			auto front = input.front;
1003 			if (front.type != TokenType.identifier)
1004 			{
1005 				switch (front.content)
1006 				{
1007 				case ",":
1008 				case "=":
1009 					if (stack.length == 0)
1010 						break Loop;
1011 					break;
1012 				case ")":
1013 				case "]":
1014 				case "}":
1015 					if (stack.length && stack[$ - 1] != front.content[0])
1016 					{
1017 						input.errors.expect(input, front.range[0],
1018 								"'" ~ (cast(char[])(cast(ubyte[]) stack).retro.chain.array).idup ~ "' before ')'");
1019 						stack.length = 0;
1020 					}
1021 					if (stack.length == 0)
1022 						break Loop;
1023 					stack.length--;
1024 					break;
1025 				case "(":
1026 					stack ~= ')';
1027 					break;
1028 				case "[":
1029 					stack ~= ']';
1030 					break;
1031 				case "{":
1032 					stack ~= '}';
1033 					break;
1034 				case "\"":
1035 					if (stack.length && stack[$ - 1] == '"')
1036 					{
1037 						stack.length--;
1038 					}
1039 					else
1040 					{
1041 						stack ~= '"';
1042 					}
1043 					break;
1044 				default:
1045 					break;
1046 				}
1047 			}
1048 			ret ~= front.content;
1049 			input.popFront;
1050 		}
1051 		tok.range[1] = input.front.range[0];
1052 		tok.content = ret;
1053 		return tok;
1054 	}
1055 
1056 	Expression parseExpression()
1057 	{
1058 		import std.array : array;
1059 		import std.range : retro, chain;
1060 
1061 		auto save = input.save;
1062 		auto tok = input.front;
1063 		string ret;
1064 		char[] stack;
1065 		bool escape = false;
1066 		int level = 0;
1067 		Loop: while (!input.empty)
1068 		{
1069 			auto front = input.front;
1070 			if (front.type == TokenType.indent)
1071 				level++;
1072 			else if (front.type == TokenType.detent)
1073 				level--;
1074 			if (level < 0)
1075 			{
1076 				input = save;
1077 				return null;
1078 			}
1079 			if (front.type == TokenType.raw)
1080 			{
1081 				if (stack.length && stack[$ - 1].among!('"', '\''))
1082 				{
1083 					if (escape)
1084 					{
1085 						escape = false;
1086 					}
1087 					else
1088 					{
1089 						if (front.content == "\\")
1090 							escape = true;
1091 						else if (front.content.length == 1 && front.content[0] == stack[$ - 1])
1092 							stack.length--;
1093 					}
1094 				}
1095 				else
1096 				{
1097 					switch (front.content)
1098 					{
1099 					case ",":
1100 						if (stack.length == 0)
1101 							break Loop;
1102 						break;
1103 					case ")":
1104 					case "]":
1105 					case "}":
1106 						if (stack.length && stack[$ - 1] != front.content[0])
1107 						{
1108 							input.errors.expect(input, front.range[0],
1109 									"'" ~ (cast(char[])(cast(ubyte[]) stack).retro.chain.array).idup ~ "' before ')'");
1110 							stack.length = 0;
1111 						}
1112 						if (stack.length == 0)
1113 							break Loop;
1114 						stack.length--;
1115 						break;
1116 					case "(":
1117 						stack ~= ')';
1118 						break;
1119 					case "[":
1120 						stack ~= ']';
1121 						break;
1122 					case "{":
1123 						stack ~= '}';
1124 						break;
1125 					case "\"":
1126 						stack ~= '"';
1127 						break;
1128 					case "'":
1129 						stack ~= '\'';
1130 						break;
1131 					default:
1132 						break;
1133 					}
1134 				}
1135 			}
1136 			else
1137 				escape = false;
1138 			ret ~= front.content;
1139 			input.popFront;
1140 		}
1141 		if (stack.length)
1142 			input.errors.expect(input, input.front.range[0],
1143 					"'" ~ (cast(char[])(cast(ubyte[]) stack).retro.chain.array).idup ~ "' before ')'");
1144 		tok.range[1] = input.front.range[0];
1145 		return new Expression(tok, ret);
1146 	}
1147 
1148 	TagNode parseTag(bool allowNested = true)
1149 	{
1150 		auto save = input.save;
1151 
1152 		auto tok = input.front;
1153 		input.popFront;
1154 
1155 		Token[] classes, ids;
1156 
1157 		bool parseClassOrID()
1158 		{
1159 			Token combineIdentifier()
1160 			{
1161 				Token start = input.front;
1162 				input.popFront;
1163 				auto next = input.front;
1164 				if (input.expect(TokenType.identifier))
1165 				{
1166 					if (!validateIdentifier(next.content))
1167 						input.errors.expect(input, next.range[0], "identifier of type [-_0-9a-zA-Z]+");
1168 					start.content ~= next.content;
1169 					start.range[1] = next.range[1];
1170 				}
1171 				return start;
1172 			}
1173 
1174 			if (input.front.content == ".")
1175 			{
1176 				classes ~= combineIdentifier();
1177 				return true;
1178 			}
1179 			else if (input.front.content == "#")
1180 			{
1181 				ids ~= combineIdentifier();
1182 				return true;
1183 			}
1184 			else
1185 				return false;
1186 		}
1187 
1188 		Token tag;
1189 		bool match;
1190 		if (input.front.content == "." || input.front.content == "#")
1191 		{
1192 			tag = tok;
1193 			tag.range[1] = tag.range[0];
1194 			tag.content = "div";
1195 			while (parseClassOrID())
1196 			{
1197 			}
1198 			match = classes.length > 0 || ids.length > 0;
1199 		}
1200 		else if (tok.type == TokenType.identifier)
1201 		{
1202 			tag = tok;
1203 			if (!tok.content.validateTagIdentifier)
1204 				input.errors.expect(input, tok.range[0], "identifier of type [-:_0-9a-zA-Z]+");
1205 			while (parseClassOrID())
1206 			{
1207 			}
1208 			match = true;
1209 		}
1210 
1211 		if (match)
1212 		{
1213 			auto ret = new TagNode(tag);
1214 			ret.directIDs = ids;
1215 			ret.directClasses = classes;
1216 			ret.attributesRange[] = input.front.range[0];
1217 			if (input.matchText("("))
1218 			{
1219 				ret.attributesRange[0]++;
1220 				ret.attributesRange[1]++;
1221 				auto lastValid = input.save;
1222 				// don't store in lastValid after detent happened
1223 				bool detented = false;
1224 				TagNode.Attribute[] lastNonDetented;
1225 				bool lastNonDetentedHadValue;
1226 				if (input.peek(TokenType.identifier))
1227 				{
1228 					lastValid = input.save;
1229 					bool errored;
1230 					while (input.front.content != ")")
1231 					{
1232 						input.skipAllWhiteGetDetent(detented);
1233 						const wasDetented = detented;
1234 						auto identifier = parseAttributeIdentifier();
1235 						Expression value;
1236 						bool validKey;
1237 						if (!identifier.content.length)
1238 							errored = true;
1239 						else if (!detented)
1240 						{
1241 							lastValid = input.save;
1242 							validKey = true;
1243 						}
1244 						input.skipAllWhiteGetDetent(detented);
1245 						bool empty;
1246 						if (input.matchText("="))
1247 						{
1248 							if (!detented)
1249 								lastValid = input.save;
1250 							input.skipAllWhiteGetDetent(detented);
1251 							value = parseExpression();
1252 							if (value !is null)
1253 							{
1254 								if (!detented)
1255 									lastValid = input.save;
1256 							}
1257 							else
1258 								errored = true;
1259 						}
1260 						else if (input.matchText(","))
1261 						{
1262 							if (!detented)
1263 								lastValid = input.save;
1264 							input.skipAllWhiteGetDetent(detented);
1265 						}
1266 						else
1267 						{
1268 							if (!identifier.content.length)
1269 								break;
1270 							empty = true;
1271 						}
1272 
1273 						if (!errored)
1274 						{
1275 							if (!detented)
1276 								lastValid = input.save;
1277 							input.skipAllWhiteGetDetent(detented);
1278 						}
1279 
1280 						ret.attributes ~= TagNode.Attribute(identifier, value);
1281 						if (!detented)
1282 							lastNonDetented = ret.attributes;
1283 
1284 						if (!wasDetented && detented && validKey)
1285 						{
1286 							lastNonDetented = ret.attributes;
1287 							lastNonDetentedHadValue = false;
1288 						}
1289 
1290 						if (empty && !input.front.content.among!(")", ","))
1291 						{
1292 							errored = true;
1293 							break;
1294 						}
1295 
1296 						if (input.front.content == ",")
1297 							input.popFront();
1298 					}
1299 					ret.attributesRange[1] = input.front.range[0];
1300 					if (errored)
1301 					{
1302 						lastValid.errors = input.errors;
1303 						ret.attributes = lastNonDetented;
1304 						input = lastValid;
1305 					}
1306 				}
1307 				if (!input.expect(TokenType.raw, ")") && detented)
1308 				{
1309 					lastValid.errors = input.errors;
1310 					ret.attributes = lastNonDetented;
1311 					input = lastValid;
1312 				}
1313 				if (detented && !lastNonDetentedHadValue && ret.attributes.length)
1314 					ret.attributes[$ - 1].expr = null;
1315 			}
1316 			if (input.front.content == "<")
1317 			{
1318 				ret._fitInside = input.front;
1319 				input.popFront;
1320 			}
1321 			else if (input.front.content == ">")
1322 			{
1323 				auto tmp = input.front;
1324 				ret._fitOutside = tmp;
1325 				input.popFront;
1326 				if (input.front.content == "<")
1327 				{
1328 					tmp = input.front;
1329 					input.popFront;
1330 					ret._fitInside = tmp;
1331 				}
1332 			}
1333 			if (input.front.content == "&")
1334 			{
1335 				ret._translated = input.front;
1336 				input.popFront;
1337 			}
1338 
1339 			if (input.front.content == ".")
1340 			{
1341 				ret._contents = parseTextBlock();
1342 			}
1343 			else if (allowNested && input.front.content == ":")
1344 			{
1345 				input.popFront;
1346 				input.skipAll(TokenType.whitespace);
1347 				ret._contents = parseNestedTags();
1348 			}
1349 			else
1350 			{
1351 				ret._contents = parseTagContents();
1352 			}
1353 
1354 			ret._token.range[1] = input.index;
1355 			return ret;
1356 		}
1357 		else
1358 		{
1359 			input = save;
1360 			return null;
1361 		}
1362 	}
1363 
1364 	StringTagContents parseTextBlock()
1365 	{
1366 		Token front = input.front;
1367 		if (front.content == ".")
1368 		{
1369 			input.popFront;
1370 			string content;
1371 			input.skipAll(TokenType.identifier, TokenType.raw, TokenType.whitespace);
1372 			if (input.expect(TokenType.newline))
1373 			{
1374 				input.skipAll(TokenType.newline);
1375 				if (input.expect(TokenType.indent))
1376 				{
1377 					content = parseText(true, true);
1378 					input.expect(TokenType.detent);
1379 				}
1380 			}
1381 			front.range[1] = input.index;
1382 			return new StringTagContents(front, content);
1383 		}
1384 		return null;
1385 	}
1386 
1387 	TagContents parseTagContents()
1388 	{
1389 		if (auto assignment = parseAssignment())
1390 			return assignment;
1391 		if (input.peek(TokenType.newline))
1392 			return null;
1393 		input.match(TokenType.whitespace);
1394 		return parseTextLine();
1395 	}
1396 
1397 	Assignment parseAssignment()
1398 	{
1399 		auto tok = input.front;
1400 		if (input.matchText("!="))
1401 		{
1402 			string content = parseText(false, false);
1403 			tok.content = "!=";
1404 			tok.range[1] = input.front.range[0];
1405 			return new RawAssignment(tok, content);
1406 		}
1407 		else if (input.matchText("="))
1408 		{
1409 			string content = parseText(false, false);
1410 			tok.range[1] = input.front.range[0];
1411 			return new Assignment(tok, content);
1412 		}
1413 		return null;
1414 	}
1415 
1416 	/// Searches for a path of AST nodes lying within the specified offset.
1417 	/// Params:
1418 	///   offset = The cursor position to search AST nodes in.
1419 	///   inclusiveStart = true if an AST [1 .. 3] should be matched for index 1.
1420 	///   inclusiveEnd = true if an AST [1 .. 3] should be matched for index 3.
1421 	/// Returns: A path of AST nodes starting at the broadest object (Document) down to the finest object.
1422 	AST[] searchAST(size_t offset, bool inclusiveStart = true, bool inclusiveEnd = true)
1423 	out (r; r.length > 0)
1424 	{
1425 		AST[] ret = [root];
1426 
1427 		root.traverse((AST node, AST parent) {
1428 			if (ret[$ - 1] != parent)
1429 				return VisitResult.return_;
1430 			auto range = node.token.range;
1431 			if (!offset.withinRange(range, inclusiveStart, inclusiveEnd))
1432 				return VisitResult.continue_;
1433 			ret ~= node;
1434 			return VisitResult.recurse;
1435 		});
1436 
1437 		return ret;
1438 	}
1439 }
1440 
1441 bool withinRange(size_t offset, size_t[2] range, bool inclusiveStart = true, bool inclusiveEnd = true)
1442 {
1443 	if (inclusiveStart && inclusiveEnd)
1444 		return offset >= range[0] && offset <= range[1];
1445 	else if (inclusiveStart)
1446 		return offset >= range[0] && offset < range[1];
1447 	else if (inclusiveEnd)
1448 		return offset > range[0] && offset <= range[1];
1449 	else
1450 		return offset > range[0] && offset < range[1];
1451 }
1452 
1453 void skipAllWhiteGetDetent(ref DietInput input, ref bool detented)
1454 {
1455 	auto c = input.skipAllCount(TokenType.whitespace, TokenType.detent,
1456 			TokenType.indent, TokenType.newline);
1457 	if (c[1])
1458 		detented = true;
1459 }