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