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 }