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 <ae@cy.md>
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 	/// Write to an `XmlWriter`.
83 	final void writeTo(XmlWriter)(ref XmlWriter output) const
84 	{
85 		with (_xmlbuild_info)
86 		{
87 			output.startTagWithAttributes(tag);
88 			foreach (ref attribute; attributes)
89 				output.addAttribute(attribute.key, attribute.value);
90 			if (!children.length && !text)
91 			{
92 				output.endAttributesAndTag();
93 				return;
94 			}
95 			output.endAttributes();
96 
97 			foreach (child; children)
98 				child.writeTo(output);
99 			output.text(text);
100 
101 			output.endTag(tag);
102 		}
103 	}
104 
105 	// Use a unique name, unlikely to occur in an XML file as a field or attribute.
106 	private XmlBuildInfo _xmlbuild_info;
107 }
108 
109 private:
110 
111 struct StringPair { string key, value; }
112 
113 struct XmlBuildInfo
114 {
115 	string tag, text;
116 	StringPair[] attributes;
117 	XmlBuildNode[] children;
118 }
119 
120 version(unittest) import std.array : split;
121 version(unittest) import std.algorithm.sorting : sort;
122 
123 unittest
124 {
125 	auto svg = newXml().svg();
126 	svg.xmlns = "http://www.w3.org/2000/svg";
127 	svg["version"] = "1.1";
128 	auto text = svg.text(["x" : "0", "y" : "15", "fill" : "red"]);
129 	text[] = "I love SVG";
130 
131 	auto s = svg.toString();
132 
133 	enum start = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1"><text `;
134 	assert(s[0..start.length] == start);
135 	s = s[start.length..$];
136 
137 	enum end = `>I love SVG</text></svg>`;
138 	assert(s[$-end.length..$] == end);
139 	s = s[0..$-end.length];
140 
141 	assert(s.split.sort == `x="0" y="15" fill="red"`.split.sort);
142 }