1 /**
2  * Very simple write-only API for building XML documents.
3  * Abuses operator overloading to allow a very terse syntax.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <vladimir@thecybershadow.net>
13  */
14 
15 module ae.utils.xmlbuild;
16 
17 import ae.utils.xmlwriter;
18 
19 /// Create an XML node. Entry point.
20 XmlBuildNode newXml()
21 {
22 	return new XmlBuildNode();
23 }
24 
25 /// The node type. Avoid using directly.
26 // Can't use struct pointers, because node["attr"] is
27 // interpreted as indexing the pointer.
28 final class XmlBuildNode
29 {
30 	/// Create a child node by calling a "method" on the node
31 	XmlBuildNode opDispatch(string name)(string[string] attributes = null)
32 	{
33 		auto result = new XmlBuildNode();
34 		result._xmlbuild_info.tag = name;
35 		foreach (key, value; attributes)
36 			result._xmlbuild_info.attributes ~= StringPair(key, value);
37 		_xmlbuild_info.children ~= result;
38 		return result;
39 	}
40 
41 	/// Add attribute by assigning a "field" on the node
42 	@property string opDispatch(string name)(string value)
43 	{
44 		_xmlbuild_info.attributes ~= StringPair(name, value);
45 		return value;
46 	}
47 
48 	/// Add attribute via index
49 	string opIndexAssign(string value, string name)
50 	{
51 		_xmlbuild_info.attributes ~= StringPair(name, value);
52 		return value;
53 	}
54 
55 	/// Get/set inner text via node[]
56 	ref string opSlice()
57 	{
58 		return _xmlbuild_info.text;
59 	}
60 
61 	/// Set inner text via assigning a string
62 	deprecated ("Use `node[] = str` instead of node = str")
63 	void opAssign(string text)
64 	{
65 		_xmlbuild_info.text = text;
66 	}
67 
68 	override string toString() const
69 	{
70 		XmlWriter writer;
71 		writeTo(writer);
72 		return writer.output.get();
73 	}
74 
75 	string toPrettyString() const
76 	{
77 		PrettyXmlWriter writer;
78 		writeTo(writer);
79 		return writer.output.get();
80 	}
81 
82 	final void writeTo(XmlWriter)(ref XmlWriter output) const
83 	{
84 		with (_xmlbuild_info)
85 		{
86 			output.startTagWithAttributes(tag);
87 			foreach (ref attribute; attributes)
88 				output.addAttribute(attribute.key, attribute.value);
89 			if (!children.length && !text)
90 			{
91 				output.endAttributesAndTag();
92 				return;
93 			}
94 			output.endAttributes();
95 
96 			foreach (child; children)
97 				child.writeTo(output);
98 			output.text(text);
99 
100 			output.endTag(tag);
101 		}
102 	}
103 
104 	// Use a unique name, unlikely to occur in an XML file as a field or attribute.
105 	private XmlBuildInfo _xmlbuild_info;
106 }
107 
108 private:
109 
110 struct StringPair { string key, value; }
111 
112 struct XmlBuildInfo
113 {
114 	string tag, text;
115 	StringPair[] attributes;
116 	XmlBuildNode[] children;
117 }
118 
119 version(unittest) import std.array : split;
120 version(unittest) import std.algorithm.sorting : sort;
121 
122 unittest
123 {
124 	auto svg = newXml().svg();
125 	svg.xmlns = "http://www.w3.org/2000/svg";
126 	svg["version"] = "1.1";
127 	auto text = svg.text(["x" : "0", "y" : "15", "fill" : "red"]);
128 	text[] = "I love SVG";
129 
130 	auto s = svg.toString();
131 
132 	enum start = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1"><text `;
133 	assert(s[0..start.length] == start);
134 	s = s[start.length..$];
135 
136 	enum end = `>I love SVG</text></svg>`;
137 	assert(s[$-end.length..$] == end);
138 	s = s[0..$-end.length];
139 
140 	assert(s.split.sort == `x="0" y="15" fill="red"`.split.sort);
141 }