1 /** Types to represent the DOM tree.
2 
3 	The DOM tree is used as an intermediate representation between the parser
4 	and the generator. Filters and other kinds of transformations can be
5 	executed on the DOM tree. The generator itself will apply filters and
6 	other traits using `diet.traits.applyTraits`.
7 */
8 module diet.dom;
9 
10 import diet.internal..string;
11 
12 
13 string expectText(const(Attribute) att)
14 {
15 	import diet.defs : enforcep;
16 	if (att.contents.length == 0) return null;
17 	enforcep(att.isText, "'"~att.name~"' expected to be a pure text attribute.", att.loc);
18 	return att.contents[0].value;
19 }
20 
21 string expectText(const(Node) n)
22 {
23 	import diet.defs : enforcep;
24 	if (n.contents.length == 0) return null;
25 	enforcep(n.contents.length > 0 && n.contents[0].kind == NodeContent.Kind.text &&
26 		(n.contents.length == 1 || n.contents[1].kind != NodeContent.Kind.node),
27 		"Expected pure text node.", n.loc);
28 	return n.contents[0].value;
29 }
30 
31 string expectExpression(const(Attribute) att)
32 {
33 	import diet.defs : enforcep;
34 	enforcep(att.isExpression, "'"~att.name~"' expected to be an expression attribute.", att.loc);
35 	return att.contents[0].value;
36 }
37 
38 bool isExpression(const(Attribute) att) { return att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.interpolation; }
39 bool isText(const(Attribute) att) { return att.contents.length == 0 || att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.text; }
40 
41 /** Converts an array of attribute contents to node contents.
42 */
43 NodeContent[] toNodeContent(in AttributeContent[] contents, Location loc)
44 {
45 	auto ret = new NodeContent[](contents.length);
46 	foreach (i, ref c; contents) {
47 		final switch (c.kind) {
48 			case AttributeContent.Kind.text: ret[i] = NodeContent.text(c.value, loc); break;
49 			case AttributeContent.Kind.interpolation: ret[i] = NodeContent.interpolation(c.value, loc); break;
50 			case AttributeContent.Kind.rawInterpolation: ret[i] = NodeContent.rawInterpolation(c.value, loc); break;
51 		}
52 	}
53 	return ret;
54 }
55 
56 
57 /** Encapsulates a full Diet template document.
58 */
59 /*final*/ class Document { // non-final because of https://issues.dlang.org/show_bug.cgi?id=17146
60 	Node[] nodes;
61 
62 	this(Node[] nodes) { this.nodes = nodes; }
63 }
64 
65 
66 /** Represents a single node in the DOM tree.
67 */
68 /*final*/ class Node { // non-final because of https://issues.dlang.org/show_bug.cgi?id=17146
69 	@safe nothrow:
70 
71 	/// A set of names that identify special-purpose nodes
72 	enum SpecialName {
73 		/** Normal comment. The content will appear in the output if the output
74 			format supports comments.
75 		*/
76 		comment = "//",
77 
78 		/** Hidden comment. The content will never appear in the output.
79 		*/
80 		hidden = "//-",
81 
82 		/** D statement. A node that has pure text as its first content,
83 			optionally followed by any number of child nodes. The text content
84 			is either a complete D statement, or an open block statement
85 			(without a block statement appended). In the latter case, all nested
86 			nodes are considered to be part of the block statement's body by
87 			the generator.
88 		*/
89 		code = "-",
90 
91 		/** A dummy node that contains only text and string interpolations.
92 			These nodes behave the same as if their node content would be
93 			inserted in their place, except that they will cause whitespace
94 			(usually a space or a newline) to be prepended in the output, if
95 			they are not the first child of their parent.
96 		*/
97 		text = "|",
98 
99 		/** Filter node. These nodes contain only text and string interpolations
100 			and have a "filterChain" attribute that contains a space separated
101 			list of filter names that are applied in reverse order when the
102 			traits (see `diet.traits.applyTraits`) are applied by the generator.
103 		*/
104 		filter = ":"
105 	}
106 
107 	/// Start location of the node in the source file.
108 	Location loc;
109 	/// Name of the node
110 	string name;
111 	/// A key-value set of attributes.
112 	Attribute[] attributes;
113 	/// The main contents of the node.
114 	NodeContent[] contents;
115 	/// Flags that control the parser and generator behavior.
116 	NodeAttribs attribs;
117 
118 	/// Constructs a new node.
119 	this(Location loc = Location.init, string name = null, Attribute[] attributes = null, NodeContent[] contents = null, NodeAttribs attribs = NodeAttribs.none)
120 	{
121 		this.loc = loc;
122 		this.name = name;
123 		this.attributes = attributes;
124 		this.contents = contents;
125 		this.attribs = attribs;
126 	}
127 
128 	/// Returns the "id" attribute.
129 	@property inout(Attribute) id() inout { return getAttribute("id"); }
130 	/// Returns "class" attribute - a white space separated list of style class identifiers.
131 	@property inout(Attribute) class_() inout { return getAttribute("class"); }
132 
133 	/** Adds a piece of text to the node's contents.
134 
135 		If the node already has some content and the last piece of content is
136 		also text, with a matching location, the text will be appended to that
137 		`NodeContent`'s value. Otherwise, a new `NodeContent` will be appended.
138 
139 		Params:
140 			text = The text to append to the node
141 			loc = Location in the source file
142 	*/
143 	void addText(string text, in ref Location loc)
144 	{
145 		if (contents.length && contents[$-1].kind == NodeContent.Kind.text && contents[$-1].loc == loc)
146 			contents[$-1].value ~= text;
147 		else contents ~= NodeContent.text(text, loc);
148 	}
149 
150 	/** Removes all content if it conists of only white space. */
151 	void stripIfOnlyWhitespace()
152 	{
153 		if (!this.hasNonWhitespaceContent)
154 			contents = null;
155 	}
156 
157 	/** Determines if this node has any non-whitespace contents. */
158 	bool hasNonWhitespaceContent()
159 	const {
160 		import std.algorithm.searching : any;
161 		return contents.any!(c => c.kind != NodeContent.Kind.text || c.value.ctstrip.length > 0);
162 	}
163 
164 	/** Strips any leading whitespace from the contents. */
165 	void stripLeadingWhitespace()
166 	{
167 		while (contents.length >= 1 && contents[0].kind == NodeContent.Kind.text) {
168 			contents[0].value = ctstripLeft(contents[0].value);
169 			if (contents[0].value.length == 0)
170 				contents = contents[1 .. $];
171 			else break;
172 		}
173 	}
174 
175 	/** Strips any trailign whitespace from the contents. */
176 	void stripTrailingWhitespace()
177 	{
178 		while (contents.length >= 1 && contents[$-1].kind == NodeContent.Kind.text) {
179 			contents[$-1].value = ctstripRight(contents[$-1].value);
180 			if (contents[$-1].value.length == 0)
181 				contents = contents[0 .. $-1];
182 			else break;
183 		}
184 	}
185 
186 	/// Tests if the node consists of only a single, static string.
187 	bool isTextNode() const { return contents.length == 1 && contents[0].kind == NodeContent.Kind.text; }
188 
189 	/// Tests if the node consists only of text and interpolations, but doesn't contain child nodes.
190 	bool isProceduralTextNode() const { import std.algorithm.searching : all; return contents.all!(c => c.kind != NodeContent.Kind.node); }
191 
192 	bool hasAttribute(string name)
193 	const {
194 
195 		foreach (ref a; this.attributes)
196 			if (a.name == name)
197 				return true;
198 		return false;
199 	}
200 
201 	/** Returns a given named attribute.
202 
203 		If the attribute doesn't exist, an empty value will be returned.
204 	*/
205 	inout(Attribute) getAttribute(string name)
206 	inout @trusted {
207 		foreach (ref a; this.attributes)
208 			if (a.name == name)
209 				return a;
210 		return cast(inout)Attribute(this.loc, name, null);
211 	}
212 
213 	void setAttribute(Attribute att)
214 	{
215 		foreach (ref da; attributes)
216 			if (da.name == att.name) {
217 				da = att;
218 				return;
219 			}
220 		attributes ~= att;
221 	}
222 
223 	/// Outputs a simple string representation of the node.
224 	override string toString() const {
225 		scope (failure) assert(false);
226 		import std..string : format;
227 		return format("Node(%s, %s, %s, %s, %s)", this.tupleof);
228 	}
229 
230 	/// Compares all properties of two nodes for equality.
231 	override bool opEquals(Object other_) {
232 		auto other = cast(Node)other_;
233 		if (!other) return false;
234 		return this.opEquals(other);
235 	}
236 
237 	bool opEquals(in Node other) const { return this.tupleof == other.tupleof; }
238 }
239 
240 
241 /** Flags that control parser or generator behavior.
242 */
243 enum NodeAttribs {
244 	none = 0,
245 	translated = 1<<0,  /// Translate node contents
246 	textNode = 1<<1,    /// All nested lines are treated as text
247 	rawTextNode = 1<<2, /// All nested lines are treated as raw text (no interpolations or inline tags)
248 	fitOutside = 1<<3,  /// Don't insert white space outside of the node when generating output (currently ignored by the HTML generator)
249 	fitInside = 1<<4,   /// Don't insert white space around the node contents when generating output (currently ignored by the HTML generator)
250 }
251 
252 
253 /** A single node attribute.
254 
255 	Attributes are key-value pairs, where the value can either be empty
256 	(considered as a Boolean value of `true`), a string with optional
257 	string interpolations, or a D expression (stored as a single
258 	`interpolation` `AttributeContent`).
259 */
260 struct Attribute {
261 	@safe nothrow:
262 
263 	/// Location in source file
264 	Location loc;
265 	/// Name of the attribute
266 	string name;
267 	/// Value of the attribute
268 	AttributeContent[] contents;
269 
270 	/// Creates a new attribute with a static text value.
271 	static Attribute text(string name, string value, Location loc) { return Attribute(loc, name, [AttributeContent.text(value)]); }
272 	/// Creates a new attribute with an expression based value.
273 	static Attribute expr(string name, string value, Location loc) { return Attribute(loc, name, [AttributeContent.interpolation(value)]); }
274 
275 	this(Location loc, string name, AttributeContent[] contents)
276 	{
277 		this.name = name;
278 		this.contents = contents;
279 		this.loc = loc;
280 	}
281 
282 	/// Creates a copy of the attribute.
283 	@property Attribute dup() const { return Attribute(loc, name, contents.dup); }
284 
285 	/** Appends raw text to the attribute.
286 
287 		If the attribute already has contents and the last piece of content is
288 		also text, then the text will be appended to the value of that
289 		`AttributeContent`. Otherwise, a new `AttributeContent` will be
290 		appended to `contents`.
291 	*/
292 	void addText(string str)
293 	{
294 		if (contents.length && contents[$-1].kind == AttributeContent.Kind.text)
295 			contents[$-1].value ~= str;
296 		else
297 			contents ~= AttributeContent.text(str);
298 	}
299 
300 	/** Appends a list of contents.
301 
302 		If the list of contents starts with a text `AttributeContent`, then this
303 		first part will be appended using the same rules as for `addText`. The
304 		remaining parts will be appended normally.
305 	*/
306 	void addContents(const(AttributeContent)[] contents)
307 	{
308 		if (contents.length > 0 && contents[0].kind == AttributeContent.Kind.text) {
309 			addText(contents[0].value);
310 			contents = contents[1 .. $];
311 		}
312 		this.contents ~= contents;
313 	}
314 }
315 
316 
317 /** A single piece of an attribute value.
318 */
319 struct AttributeContent {
320 	@safe nothrow:
321 
322 	/// 
323 	enum Kind {
324 		text,             /// Raw text (will be escaped by the generator as necessary)
325 		interpolation,    /// A D expression that will be converted to text at runtime (escaped as necessary)
326 		rawInterpolation  /// A D expression that will be converted to text at runtime (not escaped)
327 	}
328 
329 	/// Kind of this attribute content
330 	Kind kind;
331 	/// The value - either text or a D expression
332 	string value;
333 
334 	/// Creates a new text attribute content value.
335 	static AttributeContent text(string text) { return AttributeContent(Kind.text, text); }
336 	/// Creates a new string interpolation attribute content value.
337 	static AttributeContent interpolation(string expression) { return AttributeContent(Kind.interpolation, expression); }
338 	/// Creates a new raw string interpolation attribute content value.
339 	static AttributeContent rawInterpolation(string expression) { return AttributeContent(Kind.rawInterpolation, expression); }
340 }
341 
342 
343 /** A single piece of node content.
344 */
345 struct NodeContent {
346 	@safe nothrow:
347 
348 	///
349 	enum Kind {
350 		node,            /// A child node
351 		text,            /// Raw text (not escaped in the output)
352 		interpolation,   /// A D expression that will be converted to text at runtime (escaped as necessary)
353 		rawInterpolation /// A D expression that will be converted to text at runtime (not escaped)
354 	}
355 
356 	/// Kind of this node content
357 	Kind kind;
358 	/// Location of the content in the source file
359 	Location loc;
360 	/// The node - only used for `Kind.node`
361 	Node node;
362 	/// The string value - either text or a D expression
363 	string value;
364 
365 	/// Creates a new child node content value.
366 	static NodeContent tag(Node node) { return NodeContent(Kind.node, node.loc, node); }
367 	/// Creates a new text node content value.
368 	static NodeContent text(string text, Location loc) { return NodeContent(Kind.text, loc, Node.init, text); }
369 	/// Creates a new string interpolation node content value.
370 	static NodeContent interpolation(string text, Location loc) { return NodeContent(Kind.interpolation, loc, Node.init, text); }
371 	/// Creates a new raw string interpolation node content value.
372 	static NodeContent rawInterpolation(string text, Location loc) { return NodeContent(Kind.rawInterpolation, loc, Node.init, text); }
373 
374 	/// Compares node content for equality.
375 	bool opEquals(in ref NodeContent other)
376 	const {
377 		if (this.kind != other.kind) return false;
378 		if (this.loc != other.loc) return false;
379 		if (this.value != other.value) return false;
380 		if (this.node is other.node) return true;
381 		if (this.node is null || other.node is null) return false;
382 		return this.node.opEquals(other.node);
383 	}
384 }
385 
386 
387 /// Represents the location of an entity within the source file.
388 struct Location {
389 	/// Name of the source file
390 	string file;
391 	/// Zero based line index within the file
392 	int line;
393 }