1 /**
2 Contains the pegged grammar to parse a DUI file.
3 */
4 module declui.parser;
5 
6 import pegged.grammar;
7 import std.format;
8 import std.algorithm;
9 import std.array;
10 import std.range;
11 import std.uni;
12 
13 /**
14 Parses a DUI script.
15 Params:
16   content = The content of the script.
17 */
18 Tag parseDUIScript(string content)()
19 {
20 	const tree = DUI(content).children[0];
21 	static assert(tree.successful, tree.failMsg);
22 	return tree.parseTreeAsTag();
23 }
24 
25 /**
26 Imports and parses a DUI script.
27 Param:
28   content = The content of the script.
29 */
30 Tag parseDUI(string file)()
31 {
32 	return parseDUIScript!(import(file ~ ".dui"));
33 }
34 
35 /**
36 Finds all callbacks in for a given tag.
37 Param:
38   tag = The tag that should be searched for callbacks.
39 Returns: A list of the name of each callback.
40 This list will not contain any dupliates.
41 */
42 string[] findCallbacks(const Tag tag)
43 {
44 	return findAllCallbacks(tag)
45 		.dup
46 		.sort
47 		.uniq
48 		.array;
49 }
50 
51 private const(string[]) findAllCallbacks(const Tag tag)
52 {
53 	auto callbacks = tag.attributes
54 		.filter!(attribute => attribute.type == AttributeType.callback)
55 		.map!(attribute => attribute.value)
56 		.array();
57 
58 	foreach (child; tag.children)
59 	{
60 		callbacks ~= findAllCallbacks(child);
61 	}
62 	return callbacks;
63 }
64 
65 /**
66 Gets a list of all tags with a given id.
67 Param:
68   tag = The tag that should be searched for ids.
69 Returns: A list of the id of this and every child tag.
70 This list will not contain any duplicated.
71 */
72 inout(Tag)[] findIds(inout Tag tag)
73 {
74 	inout(Tag)[] callbacks;
75 
76 	if (tag.id != "")
77 		callbacks = [tag];
78 	foreach (child; tag.children)
79 		callbacks ~= findIds(child);
80 	return callbacks;
81 }
82 
83 private Tag parseTreeAsTag(const ParseTree tree)
84 {
85 	Tag tag = Tag(tree.matches[0]);
86 	const descriptor = tree.children[0];
87 
88 	// Parse id
89 	if (descriptor.hasChildTree("DUI.Id"))
90 	{
91 		tag.id = descriptor.findChildTree("DUI.Id").matches[0];
92 	}
93 
94 	// Parse attributes
95 	if (descriptor.hasChildTree("DUI.AttributeList"))
96 	{
97 		foreach (child; descriptor.findChildTree("DUI.AttributeList").children)
98 		{
99 			auto value = child.children[1];
100 			tag.attributes ~= Attribute(child.children[0].matches[0], value.stringValueOf(), value.attributeTypeOf());
101 		}
102 	}
103 
104 	// Parse children
105 	foreach (childTree; tree.children)
106 	{
107 		if (childTree.name == "DUI.Element")
108 			tag.children ~= childTree.parseTreeAsTag();
109 	}
110 
111 	return tag;
112 }
113 
114 private AttributeType attributeTypeOf(const ParseTree tree)
115 {
116 	if (tree.name == "DUI.Value")
117 		return attributeTypeOf(tree.children[0]);
118 	switch (tree.name)
119 	{
120 		case "DUI.String":
121 			return AttributeType..string;
122 		case "DUI.Callback":
123 			return AttributeType.callback;
124 		case "DUI.Integer":
125 			return AttributeType.integer;
126 		case "DUI.Bool":
127 			return AttributeType.boolean;
128 		default:
129 			assert(0, "Unknown ParseTree type " ~ tree.name);
130 	}
131 }
132 
133 private auto stringValueOf(const ParseTree tree)
134 {
135 	assert(tree.name == "DUI.Value", "Must pass in a value");
136 	switch (tree.attributeTypeOf)
137 	{
138 		case AttributeType..string:
139 			return tree.matches[0][1 .. $-1];
140 		default:
141 			return tree.matches[0];
142 	}
143 }
144 
145 private inout(ParseTree) findChildTree(inout(ParseTree) parent, const string childName)
146 {
147 	foreach (child; parent.children)
148 	{
149 		if (child.name == childName)
150 			return child;
151 	}
152 	assert(0, "Could not find child tree");
153 }
154 
155 private bool hasChildTree(const ParseTree parent, const string childName)
156 {
157 	foreach (child; parent.children)
158 	{
159 		if (child.name == childName)
160 			return true;
161 	}
162 	return false;
163 }
164 
165 private mixin(grammar(`
166 DUI:
167 	# Base types
168 	Element       <  Descriptor :'{' Element* :'}' / Descriptor
169 	Descriptor    <  identifier ('#' Id)? ( :'(' AttributeList :')' )?
170 	Id            <  identifier
171 	AttributeList <  (Attribute :','?)*
172 	Attribute     <  Identifier :'=' Value
173 
174 	# Value types
175 	Identifier    <  identifier
176 	Value         <  String / Callback / Integer / Bool
177 	String        <~ ;doublequote (!doublequote Char)* ;doublequote
178 	Char          <~
179 	              / backslash (
180 					/ doublequote
181 					/ quote
182 					/ backslash
183 					/ [bnfrt]
184 					/ [0-2][0-7][0-7]
185 					/ [0-7][0-7]?
186 					/ 'x' Hex Hex
187 					/ 'u' Hex Hex Hex Hex
188 					/ 'U' Hex Hex Hex Hex Hex Hex Hex Hex
189 					)
190 	              / .
191 	Bool          <- "true" / "false"
192 	Integer       <- [0-9]+
193 	Hex           <- '0x' [0-9a-fA-F]+
194 	Callback      <  identifier
195 `));
196 
197 
198 /**
199 A tag in a DUI file.
200 */
201 struct Tag
202 {
203 	/// The name of the tag.
204 	string name;
205 
206 	/// The id of the tag.
207 	/// This is an empty string of the tag has no id.
208 	string id;
209 
210 	/// All attributes of a tag.
211 	//Attribute[string] attributes;
212 	Attribute[] attributes;
213 
214 	/// All children of a tag.
215 	Tag[] children;
216 
217 	/// Gets an attribute by its name.
218 	Attribute opIndex(string name) const pure @safe
219 	{
220 		foreach (Attribute attribute; attributes)
221 		{
222 			if (attribute.name == name)
223 				return attribute;
224 		}
225 		assert(0, "no such element " ~ name);
226 	}
227 
228 	string toString(string indentation = "") const pure
229 	{
230 		return format!"%s%s%s%s\n"(indentation, name, attributesToString(), childrenToString(indentation));
231 	}
232 
233 	private string attributesToString() const pure
234 	{
235 		if (attributes.length == 0)
236 			return " ()";
237 
238 		return " (" ~ attributes
239 			.map!(attribute => format!`%s="%s"`(attribute.name, attribute.value))
240 			.join(", ") ~ ")";
241 	}
242 
243 	private string childrenToString(string indents) const pure
244 	{
245 		if (children.length == 0)
246 			return " {}";
247 
248 		return " {\n" ~ children
249 			.map!(tag => tag.toString(indents ~ "    "))
250 			.join() ~ indents ~ "}";
251 	}
252 }
253 
254 /**
255 A attribute of a tag.
256 It contains an iddentifier and a value.
257 */
258 struct Attribute
259 {
260 	/// The name of the attribute.
261 	string name;
262 
263 	/// The value of the attribute.
264 	string value;
265 
266 	/// The type of the attribute.
267 	AttributeType type;
268 
269 	string toString() const pure
270 	{
271 		return format!`%s="%s"`(name, value);
272 	}
273 }
274 
275 /**
276 An enumeration of all possible types of attribute.
277 */
278 enum AttributeType
279 {
280 	string,
281 	integer,
282 	callback,
283 	boolean
284 }
285 
286 @("Can parse the name of an element")
287 unittest
288 {
289 	const dui = parseDUIScript!"tagname {}";
290 	assert(dui.name == "tagname");
291 }
292 
293 @("Can parse attributes of an element")
294 unittest
295 {
296 	const dui = parseDUIScript!`tagname (foo="bar", foo2="bar2")`;
297 	assert(dui.attributes.length == 2);
298 	assert(dui.attributes[0] == Attribute("foo", "bar", AttributeType..string));
299 	assert(dui.attributes[1] == Attribute("foo2", "bar2", AttributeType..string));
300 	assert(dui["foo"] == Attribute("foo", "bar", AttributeType..string));
301 }
302 
303 @("Can parse children of an element")
304 unittest
305 {
306 	const dui = parseDUIScript!`parent (id="cool") { childA childB}`;
307 	assert(dui.children.length == 2, "Number of children is incorrect.");
308 	assert(dui.children[0].name == "childA", "Name of first child is incorrect");
309 	assert(dui.children[1].name == "childB", "Name of second child is incorrect");
310 }
311 
312 @("Can parse id of an element")
313 unittest
314 {
315 	const dui = parseDUIScript!`foo#bar()`;
316 	assert(dui.id == "bar", "Did not parse id of element");
317 }
318 
319 @("Can parse id of a child element")
320 unittest
321 {
322 	const dui = parseDUIScript!`parent { foo1#bar1 foo2#bar2 }`;
323 	assert(dui.children[0].id == "bar1", "Did not parse id of child element");
324 	assert(dui.children[1].id == "bar2", "Did not parse id of child element");
325 }
326 
327 @("Empty string attributes are allowed")
328 unittest
329 {
330 	const dui = parseDUIScript!`tag (attr="")`;
331 	assert(dui["attr"].value == "");
332 }