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 12 13 string expectText(const(Attribute) att) 14 { 15 import diet.defs : enforcep; 16 if (att.contents.length == 0) return null; 17 enforcep(att.isText, "'"~att.name~"' expected to be a pure text attribute.", att.loc); 18 return att.contents[0].value; 19 } 20 21 string expectText(const(Node) n) 22 { 23 import diet.defs : enforcep; 24 if (n.contents.length == 0) return null; 25 enforcep(n.contents.length > 0 && n.contents[0].kind == NodeContent.Kind.text && 26 (n.contents.length == 1 || n.contents[1].kind != NodeContent.Kind.node), 27 "Expected pure text node.", n.loc); 28 return n.contents[0].value; 29 } 30 31 string expectExpression(const(Attribute) att) 32 { 33 import diet.defs : enforcep; 34 enforcep(att.isExpression, "'"~att.name~"' expected to be an expression attribute.", att.loc); 35 return att.contents[0].value; 36 } 37 38 bool isExpression(const(Attribute) att) { return att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.interpolation; } 39 bool isText(const(Attribute) att) { return att.contents.length == 0 || att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.text; } 40 41 /** Converts an array of attribute contents to node contents. 42 */ 43 NodeContent[] toNodeContent(in AttributeContent[] contents, Location loc) 44 { 45 auto ret = new NodeContent[](contents.length); 46 foreach (i, ref c; contents) { 47 final switch (c.kind) { 48 case AttributeContent.Kind.text: ret[i] = NodeContent.text(c.value, loc); break; 49 case AttributeContent.Kind.interpolation: ret[i] = NodeContent.interpolation(c.value, loc); break; 50 case AttributeContent.Kind.rawInterpolation: ret[i] = NodeContent.rawInterpolation(c.value, loc); break; 51 } 52 } 53 return ret; 54 } 55 56 57 /** Encapsulates a full Diet template document. 58 */ 59 /*final*/ class Document { // non-final because of https://issues.dlang.org/show_bug.cgi?id=17146 60 Node[] nodes; 61 62 this(Node[] nodes) { this.nodes = nodes; } 63 } 64 65 66 /** Represents a single node in the DOM tree. 67 */ 68 /*final*/ class Node { // non-final because of https://issues.dlang.org/show_bug.cgi?id=17146 69 @safe nothrow: 70 71 /// A set of names that identify special-purpose nodes 72 enum SpecialName { 73 /** Normal comment. The content will appear in the output if the output 74 format supports comments. 75 */ 76 comment = "//", 77 78 /** Hidden comment. The content will never appear in the output. 79 */ 80 hidden = "//-", 81 82 /** D statement. A node that has pure text as its first content, 83 optionally followed by any number of child nodes. The text content 84 is either a complete D statement, or an open block statement 85 (without a block statement appended). In the latter case, all nested 86 nodes are considered to be part of the block statement's body by 87 the generator. 88 */ 89 code = "-", 90 91 /** A dummy node that contains only text and string interpolations. 92 These nodes behave the same as if their node content would be 93 inserted in their place, except that they will cause whitespace 94 (usually a space or a newline) to be prepended in the output, if 95 they are not the first child of their parent. 96 */ 97 text = "|", 98 99 /** Filter node. These nodes contain only text and string interpolations 100 and have a "filterChain" attribute that contains a space separated 101 list of filter names that are applied in reverse order when the 102 traits (see `diet.traits.applyTraits`) are applied by the generator. 103 */ 104 filter = ":" 105 } 106 107 /// Start location of the node in the source file. 108 Location loc; 109 /// Name of the node 110 string name; 111 /// A key-value set of attributes. 112 Attribute[] attributes; 113 /// The main contents of the node. 114 NodeContent[] contents; 115 /// Flags that control the parser and generator behavior. 116 NodeAttribs attribs; 117 118 /// Constructs a new node. 119 this(Location loc = Location.init, string name = null, Attribute[] attributes = null, NodeContent[] contents = null, NodeAttribs attribs = NodeAttribs.none) 120 { 121 this.loc = loc; 122 this.name = name; 123 this.attributes = attributes; 124 this.contents = contents; 125 this.attribs = attribs; 126 } 127 128 /// Returns the "id" attribute. 129 @property inout(Attribute) id() inout { return getAttribute("id"); } 130 /// Returns "class" attribute - a white space separated list of style class identifiers. 131 @property inout(Attribute) class_() inout { return getAttribute("class"); } 132 133 /** Adds a piece of text to the node's contents. 134 135 If the node already has some content and the last piece of content is 136 also text, with a matching location, the text will be appended to that 137 `NodeContent`'s value. Otherwise, a new `NodeContent` will be appended. 138 139 Params: 140 text = The text to append to the node 141 loc = Location in the source file 142 */ 143 void addText(string text, in ref Location loc) 144 { 145 if (contents.length && contents[$-1].kind == NodeContent.Kind.text && contents[$-1].loc == loc) 146 contents[$-1].value ~= text; 147 else contents ~= NodeContent.text(text, loc); 148 } 149 150 /** Removes all content if it conists of only white space. */ 151 void stripIfOnlyWhitespace() 152 { 153 if (!this.hasNonWhitespaceContent) 154 contents = null; 155 } 156 157 /** Determines if this node has any non-whitespace contents. */ 158 bool hasNonWhitespaceContent() 159 const { 160 import std.algorithm.searching : any; 161 return contents.any!(c => c.kind != NodeContent.Kind.text || c.value.ctstrip.length > 0); 162 } 163 164 /** Strips any leading whitespace from the contents. */ 165 void stripLeadingWhitespace() 166 { 167 while (contents.length >= 1 && contents[0].kind == NodeContent.Kind.text) { 168 contents[0].value = ctstripLeft(contents[0].value); 169 if (contents[0].value.length == 0) 170 contents = contents[1 .. $]; 171 else break; 172 } 173 } 174 175 /** Strips any trailign whitespace from the contents. */ 176 void stripTrailingWhitespace() 177 { 178 while (contents.length >= 1 && contents[$-1].kind == NodeContent.Kind.text) { 179 contents[$-1].value = ctstripRight(contents[$-1].value); 180 if (contents[$-1].value.length == 0) 181 contents = contents[0 .. $-1]; 182 else break; 183 } 184 } 185 186 /// Tests if the node consists of only a single, static string. 187 bool isTextNode() const { return contents.length == 1 && contents[0].kind == NodeContent.Kind.text; } 188 189 /// Tests if the node consists only of text and interpolations, but doesn't contain child nodes. 190 bool isProceduralTextNode() const { import std.algorithm.searching : all; return contents.all!(c => c.kind != NodeContent.Kind.node); } 191 192 bool hasAttribute(string name) 193 const { 194 195 foreach (ref a; this.attributes) 196 if (a.name == name) 197 return true; 198 return false; 199 } 200 201 /** Returns a given named attribute. 202 203 If the attribute doesn't exist, an empty value will be returned. 204 */ 205 inout(Attribute) getAttribute(string name) 206 inout @trusted { 207 foreach (ref a; this.attributes) 208 if (a.name == name) 209 return a; 210 return cast(inout)Attribute(this.loc, name, null); 211 } 212 213 void setAttribute(Attribute att) 214 { 215 foreach (ref da; attributes) 216 if (da.name == att.name) { 217 da = att; 218 return; 219 } 220 attributes ~= att; 221 } 222 223 /// Outputs a simple string representation of the node. 224 override string toString() const { 225 scope (failure) assert(false); 226 import std..string : format; 227 return format("Node(%s, %s, %s, %s, %s)", this.tupleof); 228 } 229 230 /// Compares all properties of two nodes for equality. 231 override bool opEquals(Object other_) { 232 auto other = cast(Node)other_; 233 if (!other) return false; 234 return this.opEquals(other); 235 } 236 237 bool opEquals(in Node other) const { return this.tupleof == other.tupleof; } 238 } 239 240 241 /** Flags that control parser or generator behavior. 242 */ 243 enum NodeAttribs { 244 none = 0, 245 translated = 1<<0, /// Translate node contents 246 textNode = 1<<1, /// All nested lines are treated as text 247 rawTextNode = 1<<2, /// All nested lines are treated as raw text (no interpolations or inline tags) 248 fitOutside = 1<<3, /// Don't insert white space outside of the node when generating output (currently ignored by the HTML generator) 249 fitInside = 1<<4, /// Don't insert white space around the node contents when generating output (currently ignored by the HTML generator) 250 } 251 252 253 /** A single node attribute. 254 255 Attributes are key-value pairs, where the value can either be empty 256 (considered as a Boolean value of `true`), a string with optional 257 string interpolations, or a D expression (stored as a single 258 `interpolation` `AttributeContent`). 259 */ 260 struct Attribute { 261 @safe nothrow: 262 263 /// Location in source file 264 Location loc; 265 /// Name of the attribute 266 string name; 267 /// Value of the attribute 268 AttributeContent[] contents; 269 270 /// Creates a new attribute with a static text value. 271 static Attribute text(string name, string value, Location loc) { return Attribute(loc, name, [AttributeContent.text(value)]); } 272 /// Creates a new attribute with an expression based value. 273 static Attribute expr(string name, string value, Location loc) { return Attribute(loc, name, [AttributeContent.interpolation(value)]); } 274 275 this(Location loc, string name, AttributeContent[] contents) 276 { 277 this.name = name; 278 this.contents = contents; 279 this.loc = loc; 280 } 281 282 /// Creates a copy of the attribute. 283 @property Attribute dup() const { return Attribute(loc, name, contents.dup); } 284 285 /** Appends raw text to the attribute. 286 287 If the attribute already has contents and the last piece of content is 288 also text, then the text will be appended to the value of that 289 `AttributeContent`. Otherwise, a new `AttributeContent` will be 290 appended to `contents`. 291 */ 292 void addText(string str) 293 { 294 if (contents.length && contents[$-1].kind == AttributeContent.Kind.text) 295 contents[$-1].value ~= str; 296 else 297 contents ~= AttributeContent.text(str); 298 } 299 300 /** Appends a list of contents. 301 302 If the list of contents starts with a text `AttributeContent`, then this 303 first part will be appended using the same rules as for `addText`. The 304 remaining parts will be appended normally. 305 */ 306 void addContents(const(AttributeContent)[] contents) 307 { 308 if (contents.length > 0 && contents[0].kind == AttributeContent.Kind.text) { 309 addText(contents[0].value); 310 contents = contents[1 .. $]; 311 } 312 this.contents ~= contents; 313 } 314 } 315 316 317 /** A single piece of an attribute value. 318 */ 319 struct AttributeContent { 320 @safe nothrow: 321 322 /// 323 enum Kind { 324 text, /// Raw text (will be escaped by the generator as necessary) 325 interpolation, /// A D expression that will be converted to text at runtime (escaped as necessary) 326 rawInterpolation /// A D expression that will be converted to text at runtime (not escaped) 327 } 328 329 /// Kind of this attribute content 330 Kind kind; 331 /// The value - either text or a D expression 332 string value; 333 334 /// Creates a new text attribute content value. 335 static AttributeContent text(string text) { return AttributeContent(Kind.text, text); } 336 /// Creates a new string interpolation attribute content value. 337 static AttributeContent interpolation(string expression) { return AttributeContent(Kind.interpolation, expression); } 338 /// Creates a new raw string interpolation attribute content value. 339 static AttributeContent rawInterpolation(string expression) { return AttributeContent(Kind.rawInterpolation, expression); } 340 } 341 342 343 /** A single piece of node content. 344 */ 345 struct NodeContent { 346 @safe nothrow: 347 348 /// 349 enum Kind { 350 node, /// A child node 351 text, /// Raw text (not escaped in the output) 352 interpolation, /// A D expression that will be converted to text at runtime (escaped as necessary) 353 rawInterpolation /// A D expression that will be converted to text at runtime (not escaped) 354 } 355 356 /// Kind of this node content 357 Kind kind; 358 /// Location of the content in the source file 359 Location loc; 360 /// The node - only used for `Kind.node` 361 Node node; 362 /// The string value - either text or a D expression 363 string value; 364 365 /// Creates a new child node content value. 366 static NodeContent tag(Node node) { return NodeContent(Kind.node, node.loc, node); } 367 /// Creates a new text node content value. 368 static NodeContent text(string text, Location loc) { return NodeContent(Kind.text, loc, Node.init, text); } 369 /// Creates a new string interpolation node content value. 370 static NodeContent interpolation(string text, Location loc) { return NodeContent(Kind.interpolation, loc, Node.init, text); } 371 /// Creates a new raw string interpolation node content value. 372 static NodeContent rawInterpolation(string text, Location loc) { return NodeContent(Kind.rawInterpolation, loc, Node.init, text); } 373 374 /// Compares node content for equality. 375 bool opEquals(in ref NodeContent other) 376 const { 377 if (this.kind != other.kind) return false; 378 if (this.loc != other.loc) return false; 379 if (this.value != other.value) return false; 380 if (this.node is other.node) return true; 381 if (this.node is null || other.node is null) return false; 382 return this.node.opEquals(other.node); 383 } 384 } 385 386 387 /// Represents the location of an entity within the source file. 388 struct Location { 389 /// Name of the source file 390 string file; 391 /// Zero based line index within the file 392 int line; 393 }