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 }