1 /** 2 Generic Diet format parser. 3 4 Performs generic parsing of a Diet template file. The resulting AST is 5 agnostic to the output format context in which it is used. Format 6 specific constructs, such as inline code or special tags, are parsed 7 as-is without any preprocessing. 8 9 The supported features of the are: 10 $(UL 11 $(LI string interpolations) 12 $(LI assignment expressions) 13 $(LI blocks/extensions) 14 $(LI includes) 15 $(LI text paragraphs) 16 $(LI translation annotations) 17 $(LI class and ID attribute shortcuts) 18 ) 19 */ 20 module diet.parser; 21 22 import diet.dom; 23 import diet.defs; 24 import diet.input; 25 import diet.internal..string; 26 27 import std.algorithm.searching : endsWith, startsWith; 28 import std.range.primitives : empty, front, popFront, popFrontN; 29 30 31 /** Parses a Diet template document and outputs the resulting DOM tree. 32 33 The overload that takes a list of files will automatically resolve 34 includes and extensions. 35 36 Params: 37 TR = An optional translation function that takes and returns a string. 38 This function will be invoked whenever node text contents need 39 to be translated at compile tile (for the `&` node suffix). 40 text = For the single-file overload, specifies the contents of the Diet 41 template. 42 filename = For the single-file overload, specifies the file name that 43 is displayed in error messages and stored in the DOM `Location`s. 44 files = A full set of Diet template files. All files referenced in 45 includes or extension directives must be present. 46 47 Returns: 48 The list of parsed root nodes is returned. 49 */ 50 Document parseDiet(alias TR = identity)(string text, string filename = "string") 51 if (is(typeof(TR(string.init)) == string)) 52 { 53 InputFile[1] f; 54 f[0].name = filename; 55 f[0].contents = text; 56 return parseDiet!TR(f); 57 } 58 59 Document parseDiet(alias TR = identity)(InputFile[] files) 60 if (is(typeof(TR(string.init)) == string)) 61 { 62 import diet.traits; 63 import std.algorithm.iteration : map; 64 import std.array : array; 65 FileInfo[] parsed_files = files.map!(f => FileInfo(f.name, parseDietRaw!TR(f))).array; 66 BlockInfo[] blocks; 67 return new Document(parseDietWithExtensions(parsed_files, 0, blocks, null)); 68 } 69 70 unittest { // test basic functionality 71 Location ln(int l) { return Location("string", l); } 72 73 // simple node 74 assert(parseDiet("test").nodes == [ 75 new Node(ln(0), "test") 76 ]); 77 78 // nested nodes 79 assert(parseDiet("foo\n bar").nodes == [ 80 new Node(ln(0), "foo", null, [ 81 NodeContent.tag(new Node(ln(1), "bar")) 82 ]) 83 ]); 84 85 // node with id and classes 86 assert(parseDiet("test#id.cls1.cls2").nodes == [ 87 new Node(ln(0), "test", [ 88 Attribute(ln(0), "id", [AttributeContent.text("id")]), 89 Attribute(ln(0), "class", [AttributeContent.text("cls1")]), 90 Attribute(ln(0), "class", [AttributeContent.text("cls2")]) 91 ]) 92 ]); 93 assert(parseDiet("test.cls1#id.cls2").nodes == [ // issue #9 94 new Node(ln(0), "test", [ 95 Attribute(ln(0), "class", [AttributeContent.text("cls1")]), 96 Attribute(ln(0), "id", [AttributeContent.text("id")]), 97 Attribute(ln(0), "class", [AttributeContent.text("cls2")]) 98 ]) 99 ]); 100 101 // empty tag name (only class) 102 assert(parseDiet(".foo").nodes == [ 103 new Node(ln(0), "", [ 104 Attribute(ln(0), "class", [AttributeContent.text("foo")]) 105 ]) 106 ]); 107 assert(parseDiet("a.download-button\n\t.bs-hbtn.right.black").nodes == [ 108 new Node(ln(0), "a", [ 109 Attribute(ln(0), "class", [AttributeContent.text("download-button")]), 110 ], [ 111 NodeContent.tag(new Node(ln(1), "", [ 112 Attribute(ln(1), "class", [AttributeContent.text("bs-hbtn")]), 113 Attribute(ln(1), "class", [AttributeContent.text("right")]), 114 Attribute(ln(1), "class", [AttributeContent.text("black")]) 115 ])) 116 ]) 117 ]); 118 119 // empty tag name (only id) 120 assert(parseDiet("#foo").nodes == [ 121 new Node(ln(0), "", [ 122 Attribute(ln(0), "id", [AttributeContent.text("foo")]) 123 ]) 124 ]); 125 126 // node with attributes 127 assert(parseDiet("test(foo1=\"bar\", foo2=2+3)").nodes == [ 128 new Node(ln(0), "test", [ 129 Attribute(ln(0), "foo1", [AttributeContent.text("bar")]), 130 Attribute(ln(0), "foo2", [AttributeContent.interpolation("2+3")]) 131 ]) 132 ]); 133 134 // node with pure text contents 135 assert(parseDiet("foo.\n\thello\n\t world").nodes == [ 136 new Node(ln(0), "foo", null, [ 137 NodeContent.text("hello", ln(1)), 138 NodeContent.text("\n world", ln(2)) 139 ], NodeAttribs.textNode) 140 ]); 141 assert(parseDiet("foo.\n\thello\n\n\t world").nodes == [ 142 new Node(ln(0), "foo", null, [ 143 NodeContent.text("hello", ln(1)), 144 NodeContent.text("\n", ln(2)), 145 NodeContent.text("\n world", ln(3)) 146 ], NodeAttribs.textNode) 147 ]); 148 149 // translated text 150 assert(parseDiet("foo& test").nodes == [ 151 new Node(ln(0), "foo", null, [ 152 NodeContent.text("test", ln(0)) 153 ], NodeAttribs.translated) 154 ]); 155 156 // interpolated text 157 assert(parseDiet("foo hello #{\"world\"} #bar \\#{baz}").nodes == [ 158 new Node(ln(0), "foo", null, [ 159 NodeContent.text("hello ", ln(0)), 160 NodeContent.interpolation(`"world"`, ln(0)), 161 NodeContent.text(" #bar #{baz}", ln(0)) 162 ]) 163 ]); 164 165 // expression 166 assert(parseDiet("foo= 1+2").nodes == [ 167 new Node(ln(0), "foo", null, [ 168 NodeContent.interpolation(`1+2`, ln(0)), 169 ]) 170 ]); 171 172 // expression with empty tag name 173 assert(parseDiet("= 1+2").nodes == [ 174 new Node(ln(0), "", null, [ 175 NodeContent.interpolation(`1+2`, ln(0)), 176 ]) 177 ]); 178 179 // raw expression 180 assert(parseDiet("foo!= 1+2").nodes == [ 181 new Node(ln(0), "foo", null, [ 182 NodeContent.rawInterpolation(`1+2`, ln(0)), 183 ]) 184 ]); 185 186 // interpolated attribute text 187 assert(parseDiet("foo(att='hello #{\"world\"} #bar')").nodes == [ 188 new Node(ln(0), "foo", [ 189 Attribute(ln(0), "att", [ 190 AttributeContent.text("hello "), 191 AttributeContent.interpolation(`"world"`), 192 AttributeContent.text(" #bar") 193 ]) 194 ]) 195 ]); 196 197 // attribute expression 198 assert(parseDiet("foo(att=1+2)").nodes == [ 199 new Node(ln(0), "foo", [ 200 Attribute(ln(0), "att", [ 201 AttributeContent.interpolation(`1+2`), 202 ]) 203 ]) 204 ]); 205 206 // multiline attribute expression 207 assert(parseDiet("foo(\n\tatt=1+2,\n\tfoo=bar\n)").nodes == [ 208 new Node(ln(0), "foo", [ 209 Attribute(ln(0), "att", [ 210 AttributeContent.interpolation(`1+2`), 211 ]), 212 Attribute(ln(0), "foo", [ 213 AttributeContent.interpolation(`bar`), 214 ]) 215 ]) 216 ]); 217 218 // special nodes 219 assert(parseDiet("//comment").nodes == [ 220 new Node(ln(0), Node.SpecialName.comment, null, [NodeContent.text("comment", ln(0))], NodeAttribs.rawTextNode) 221 ]); 222 assert(parseDiet("//-hide").nodes == [ 223 new Node(ln(0), Node.SpecialName.hidden, null, [NodeContent.text("hide", ln(0))], NodeAttribs.rawTextNode) 224 ]); 225 assert(parseDiet("!!! 5").nodes == [ 226 new Node(ln(0), "doctype", null, [NodeContent.text("5", ln(0))]) 227 ]); 228 assert(parseDiet("<inline>").nodes == [ 229 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("<inline>", ln(0))]) 230 ]); 231 assert(parseDiet("|text").nodes == [ 232 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) 233 ]); 234 assert(parseDiet("|text\n").nodes == [ 235 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) 236 ]); 237 assert(parseDiet("| text\n").nodes == [ 238 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) 239 ]); 240 assert(parseDiet("|.").nodes == [ 241 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text(".", ln(0))]) 242 ]); 243 assert(parseDiet("|:").nodes == [ 244 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text(":", ln(0))]) 245 ]); 246 assert(parseDiet("|&x").nodes == [ 247 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("x", ln(0))], NodeAttribs.translated) 248 ]); 249 assert(parseDiet("-if(x)").nodes == [ 250 new Node(ln(0), Node.SpecialName.code, null, [NodeContent.text("if(x)", ln(0))]) 251 ]); 252 assert(parseDiet("-if(x)\n\t|bar").nodes == [ 253 new Node(ln(0), Node.SpecialName.code, null, [ 254 NodeContent.text("if(x)", ln(0)), 255 NodeContent.tag(new Node(ln(1), Node.SpecialName.text, null, [ 256 NodeContent.text("bar", ln(1)) 257 ])) 258 ]) 259 ]); 260 assert(parseDiet(":foo\n\tbar").nodes == [ 261 new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ 262 NodeContent.text("bar", ln(1)) 263 ], NodeAttribs.textNode) 264 ]); 265 assert(parseDiet(":foo :bar baz").nodes == [ 266 new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo bar")])], [ 267 NodeContent.text("baz", ln(0)) 268 ], NodeAttribs.textNode) 269 ]); 270 assert(parseDiet(":foo\n\t:bar baz").nodes == [ 271 new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ 272 NodeContent.text(":bar baz", ln(1)) 273 ], NodeAttribs.textNode) 274 ]); 275 assert(parseDiet(":foo\n\tbar\n\t\t:baz").nodes == [ 276 new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ 277 NodeContent.text("bar", ln(1)), 278 NodeContent.text("\n\t:baz", ln(2)) 279 ], NodeAttribs.textNode) 280 ]); 281 282 // nested nodes 283 assert(parseDiet("a: b").nodes == [ 284 new Node(ln(0), "a", null, [ 285 NodeContent.tag(new Node(ln(0), "b")) 286 ]) 287 ]); 288 289 assert(parseDiet("a: b\n\tc\nd").nodes == [ 290 new Node(ln(0), "a", null, [ 291 NodeContent.tag(new Node(ln(0), "b", null, [ 292 NodeContent.tag(new Node(ln(1), "c")) 293 ])) 294 ]), 295 new Node(ln(2), "d") 296 ]); 297 298 // inline nodes 299 assert(parseDiet("a #[b]").nodes == [ 300 new Node(ln(0), "a", null, [ 301 NodeContent.tag(new Node(ln(0), "b")) 302 ]) 303 ]); 304 assert(parseDiet("a #[b #[c d]]").nodes == [ 305 new Node(ln(0), "a", null, [ 306 NodeContent.tag(new Node(ln(0), "b", null, [ 307 NodeContent.tag(new Node(ln(0), "c", null, [ 308 NodeContent.text("d", ln(0)) 309 ])) 310 ])) 311 ]) 312 ]); 313 314 // whitespace fitting 315 assert(parseDiet("a<>").nodes == [ 316 new Node(ln(0), "a", null, [], NodeAttribs.fitInside|NodeAttribs.fitOutside) 317 ]); 318 assert(parseDiet("a><").nodes == [ 319 new Node(ln(0), "a", null, [], NodeAttribs.fitInside|NodeAttribs.fitOutside) 320 ]); 321 assert(parseDiet("a<").nodes == [ 322 new Node(ln(0), "a", null, [], NodeAttribs.fitInside) 323 ]); 324 assert(parseDiet("a>").nodes == [ 325 new Node(ln(0), "a", null, [], NodeAttribs.fitOutside) 326 ]); 327 } 328 329 unittest { 330 Location ln(int l) { return Location("string", l); } 331 332 // angular2 html attributes tests 333 assert(parseDiet("div([value]=\"firstName\")").nodes == [ 334 new Node(ln(0), "div", [ 335 Attribute(ln(0), "[value]", [ 336 AttributeContent.text("firstName"), 337 ]) 338 ]) 339 ]); 340 341 assert(parseDiet("div([attr.role]=\"myRole\")").nodes == [ 342 new Node(ln(0), "div", [ 343 Attribute(ln(0), "[attr.role]", [ 344 AttributeContent.text("myRole"), 345 ]) 346 ]) 347 ]); 348 349 assert(parseDiet("div([attr.role]=\"{foo:myRole}\")").nodes == [ 350 new Node(ln(0), "div", [ 351 Attribute(ln(0), "[attr.role]", [ 352 AttributeContent.text("{foo:myRole}"), 353 ]) 354 ]) 355 ]); 356 357 assert(parseDiet("div([attr.role]=\"{foo:myRole, bar:MyRole}\")").nodes == [ 358 new Node(ln(0), "div", [ 359 Attribute(ln(0), "[attr.role]", [ 360 AttributeContent.text("{foo:myRole, bar:MyRole}") 361 ]) 362 ]) 363 ]); 364 365 assert(parseDiet("div((attr.role)=\"{foo:myRole, bar:MyRole}\")").nodes == [ 366 new Node(ln(0), "div", [ 367 Attribute(ln(0), "(attr.role)", [ 368 AttributeContent.text("{foo:myRole, bar:MyRole}") 369 ]) 370 ]) 371 ]); 372 373 assert(parseDiet("div([class.extra-sparkle]=\"isDelightful\")").nodes == [ 374 new Node(ln(0), "div", [ 375 Attribute(ln(0), "[class.extra-sparkle]", [ 376 AttributeContent.text("isDelightful") 377 ]) 378 ]) 379 ]); 380 381 auto t = parseDiet("div((click)=\"readRainbow($event)\")"); 382 assert(t.nodes == [ 383 new Node(ln(0), "div", [ 384 Attribute(ln(0), "(click)", [ 385 AttributeContent.text("readRainbow($event)") 386 ]) 387 ]) 388 ]); 389 390 assert(parseDiet("div([(title)]=\"name\")").nodes == [ 391 new Node(ln(0), "div", [ 392 Attribute(ln(0), "[(title)]", [ 393 AttributeContent.text("name") 394 ]) 395 ]) 396 ]); 397 398 assert(parseDiet("div(*myUnless=\"myExpression\")").nodes == [ 399 new Node(ln(0), "div", [ 400 Attribute(ln(0), "*myUnless", [ 401 AttributeContent.text("myExpression") 402 ]) 403 ]) 404 ]); 405 406 assert(parseDiet("div([ngClass]=\"{active: isActive, disabled: isDisabled}\")").nodes == [ 407 new Node(ln(0), "div", [ 408 Attribute(ln(0), "[ngClass]", [ 409 AttributeContent.text("{active: isActive, disabled: isDisabled}") 410 ]) 411 ]) 412 ]); 413 414 t = parseDiet("div(*ngFor=\"\\#item of list\")"); 415 assert(t.nodes == [ 416 new Node(ln(0), "div", [ 417 Attribute(ln(0), "*ngFor", [ 418 AttributeContent.text("#"), 419 AttributeContent.text("item of list") 420 ]) 421 ]) 422 ]); 423 424 t = parseDiet("div(({*ngFor})=\"{args:\\#item of list}\")"); 425 assert(t.nodes == [ 426 new Node(ln(0), "div", [ 427 Attribute(ln(0), "({*ngFor})", [ 428 AttributeContent.text("{args:"), 429 AttributeContent.text("#"), 430 AttributeContent.text("item of list}") 431 ]) 432 ]) 433 ]); 434 } 435 436 unittest { // translation 437 import std..string : toUpper; 438 439 static Location ln(int l) { return Location("string", l); } 440 441 static string tr(string str) { return "("~toUpper(str)~")"; } 442 443 assert(parseDiet!tr("foo& test").nodes == [ 444 new Node(ln(0), "foo", null, [ 445 NodeContent.text("(TEST)", ln(0)) 446 ], NodeAttribs.translated) 447 ]); 448 449 assert(parseDiet!tr("foo& test #{x} it").nodes == [ 450 new Node(ln(0), "foo", null, [ 451 NodeContent.text("(TEST ", ln(0)), 452 NodeContent.interpolation("X", ln(0)), 453 NodeContent.text(" IT)", ln(0)), 454 ], NodeAttribs.translated) 455 ]); 456 457 assert(parseDiet!tr("|&x").nodes == [ 458 new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("(X)", ln(0))], NodeAttribs.translated) 459 ]); 460 461 assert(parseDiet!tr("foo&.\n\tbar\n\tbaz").nodes == [ 462 new Node(ln(0), "foo", null, [ 463 NodeContent.text("(BAR)", ln(1)), 464 NodeContent.text("\n(BAZ)", ln(2)) 465 ], NodeAttribs.translated|NodeAttribs.textNode) 466 ]); 467 } 468 469 unittest { // test expected errors 470 void testFail(string diet, string msg) 471 { 472 try { 473 parseDiet(diet); 474 assert(false, "Expected exception was not thrown."); 475 } catch (DietParserException ex) assert(ex.msg == msg, "Unexpected error message: "~ex.msg); 476 } 477 478 testFail("+test", "Expected node text separated by a space character or end of line, but got '+test'."); 479 testFail(" test", "First node must not be indented."); 480 testFail("test\n test\n\ttest", "Mismatched indentation style."); 481 testFail("test\n\ttest\n\t\t\ttest", "Line is indented too deeply."); 482 testFail("test#", "Expected identifier but got nothing."); 483 testFail("test.()", "Expected identifier but got '('."); 484 testFail("a #[b.]", "Multi-line text nodes are not permitted for inline-tags."); 485 testFail("a #[b: c]", "Nested inline-tags not allowed."); 486 testFail("a#foo#bar", "Only one \"id\" definition using '#' is allowed."); 487 } 488 489 unittest { // includes 490 Node[] parse(string diet) { 491 auto files = [ 492 InputFile("main.dt", diet), 493 InputFile("inc.dt", "p") 494 ]; 495 return parseDiet(files).nodes; 496 } 497 498 void testFail(string diet, string msg) 499 { 500 try { 501 parse(diet); 502 assert(false, "Expected exception was not thrown"); 503 } catch (DietParserException ex) { 504 assert(ex.msg == msg, "Unexpected error message: "~ex.msg); 505 } 506 } 507 508 assert(parse("include inc") == [ 509 new Node(Location("inc.dt", 0), "p", null, null) 510 ]); 511 testFail("include main", "Dependency cycle detected for this module."); 512 testFail("include inc2", "Missing include input file: inc2"); 513 testFail("include #{p}", "Dynamic includes are not supported."); 514 testFail("include inc\n\tp", "Includes cannot have children."); 515 testFail("p\ninclude inc\n\tp", "Includes cannot have children."); 516 } 517 518 unittest { // extensions 519 Node[] parse(string diet) { 520 auto files = [ 521 InputFile("main.dt", diet), 522 InputFile("root.dt", "html\n\tblock a\n\tblock b"), 523 InputFile("intermediate.dt", "extends root\nblock a\n\tp"), 524 InputFile("direct.dt", "block a") 525 ]; 526 return parseDiet(files).nodes; 527 } 528 529 void testFail(string diet, string msg) 530 { 531 try { 532 parse(diet); 533 assert(false, "Expected exception was not thrown"); 534 } catch (DietParserException ex) { 535 assert(ex.msg == msg, "Unexpected error message: "~ex.msg); 536 } 537 } 538 539 assert(parse("extends root") == [ 540 new Node(Location("root.dt", 0), "html", null, null) 541 ]); 542 assert(parse("extends root\nblock a\n\tdiv\nblock b\n\tpre") == [ 543 new Node(Location("root.dt", 0), "html", null, [ 544 NodeContent.tag(new Node(Location("main.dt", 2), "div", null, null)), 545 NodeContent.tag(new Node(Location("main.dt", 4), "pre", null, null)) 546 ]) 547 ]); 548 assert(parse("extends intermediate\nblock b\n\tpre") == [ 549 new Node(Location("root.dt", 0), "html", null, [ 550 NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), 551 NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)) 552 ]) 553 ]); 554 assert(parse("extends intermediate\nblock a\n\tpre") == [ 555 new Node(Location("root.dt", 0), "html", null, [ 556 NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)) 557 ]) 558 ]); 559 assert(parse("extends intermediate\nappend a\n\tpre") == [ 560 new Node(Location("root.dt", 0), "html", null, [ 561 NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), 562 NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)) 563 ]) 564 ]); 565 assert(parse("extends intermediate\nprepend a\n\tpre") == [ 566 new Node(Location("root.dt", 0), "html", null, [ 567 NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)), 568 NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)) 569 ]) 570 ]); 571 assert(parse("extends intermediate\nprepend a\n\tfoo\nappend a\n\tbar") == [ // issue #13 572 new Node(Location("root.dt", 0), "html", null, [ 573 NodeContent.tag(new Node(Location("main.dt", 2), "foo", null, null)), 574 NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), 575 NodeContent.tag(new Node(Location("main.dt", 4), "bar", null, null)) 576 ]) 577 ]); 578 assert(parse("extends intermediate\nprepend a\n\tfoo\nprepend a\n\tbar\nappend a\n\tbaz\nappend a\n\tbam") == [ 579 new Node(Location("root.dt", 0), "html", null, [ 580 NodeContent.tag(new Node(Location("main.dt", 2), "foo", null, null)), 581 NodeContent.tag(new Node(Location("main.dt", 4), "bar", null, null)), 582 NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), 583 NodeContent.tag(new Node(Location("main.dt", 6), "baz", null, null)), 584 NodeContent.tag(new Node(Location("main.dt", 8), "bam", null, null)) 585 ]) 586 ]); 587 assert(parse("extends direct") == []); 588 assert(parse("extends direct\nblock a\n\tp") == [ 589 new Node(Location("main.dt", 2), "p", null, null) 590 ]); 591 } 592 593 unittest { // test CTFE-ability 594 static const result = parseDiet("foo#id.cls(att=\"val\", att2=1+3, att3='test#{4}it')\n\tbar"); 595 static assert(result.nodes.length == 1); 596 } 597 598 unittest { // regression tests 599 Location ln(int l) { return Location("string", l); } 600 601 // last line contains only whitespace 602 assert(parseDiet("test\n\t").nodes == [ 603 new Node(ln(0), "test") 604 ]); 605 } 606 607 unittest { // issue #14 - blocks in includes 608 auto files = [ 609 InputFile("main.dt", "extends layout\nblock nav\n\tbaz"), 610 InputFile("layout.dt", "foo\ninclude inc"), 611 InputFile("inc.dt", "bar\nblock nav"), 612 ]; 613 assert(parseDiet(files).nodes == [ 614 new Node(Location("layout.dt", 0), "foo", null, null), 615 new Node(Location("inc.dt", 0), "bar", null, null), 616 new Node(Location("main.dt", 2), "baz", null, null) 617 ]); 618 } 619 620 unittest { // issue #32 - numeric id/class 621 Location ln(int l) { return Location("string", l); } 622 assert(parseDiet("foo.01#02").nodes == [ 623 new Node(ln(0), "foo", [ 624 Attribute(ln(0), "class", [AttributeContent.text("01")]), 625 Attribute(ln(0), "id", [AttributeContent.text("02")]) 626 ]) 627 ]); 628 } 629 630 631 /** Dummy translation function that returns the input unmodified. 632 */ 633 string identity(string str) nothrow @safe @nogc { return str; } 634 635 636 private string parseIdent(in ref string str, ref size_t start, 637 string breakChars, in ref Location loc) 638 { 639 import std.array : back; 640 /* The stack is used to keep track of opening and 641 closing character pairs, so that when we hit a break char of 642 breakChars we know if we can actually break parseIdent. 643 */ 644 char[] stack; 645 size_t i = start; 646 outer: while(i < str.length) { 647 if(stack.length == 0) { 648 foreach(char it; breakChars) { 649 if(str[i] == it) { 650 break outer; 651 } 652 } 653 } 654 655 if(stack.length && stack.back == str[i]) { 656 stack = stack[0 .. $ - 1]; 657 } else if(str[i] == '"') { 658 stack ~= '"'; 659 } else if(str[i] == '(') { 660 stack ~= ')'; 661 } else if(str[i] == '[') { 662 stack ~= ']'; 663 } else if(str[i] == '{') { 664 stack ~= '}'; 665 } 666 ++i; 667 } 668 669 /* We could have consumed the complete string and still have elements 670 on the stack or have ended non breakChars character. 671 */ 672 if(stack.length == 0) { 673 foreach(char it; breakChars) { 674 if(str[i] == it) { 675 size_t startC = start; 676 start = i; 677 return str[startC .. i]; 678 } 679 } 680 } 681 enforcep(false, "Identifier was not ended by any of these characters: " 682 ~ breakChars, loc); 683 assert(false); 684 } 685 686 private Node[] parseDietWithExtensions(FileInfo[] files, size_t file_index, ref BlockInfo[] blocks, size_t[] import_stack) 687 { 688 import std.algorithm : all, any, canFind, countUntil, filter, find, map; 689 import std.array : array; 690 import std.path : stripExtension; 691 import std.typecons : Nullable; 692 693 auto floc = Location(files[file_index].name, 0); 694 enforcep(!import_stack.canFind(file_index), "Dependency cycle detected for this module.", floc); 695 696 auto nodes = files[file_index].nodes; 697 if (!nodes.length) return null; 698 699 if (nodes[0].name == "extends") { 700 // extract base template name/index 701 enforcep(nodes[0].isTextNode, "'extends' cannot contain children or interpolations.", nodes[0].loc); 702 enforcep(nodes[0].attributes.length == 0, "'extends' cannot have attributes.", nodes[0].loc); 703 704 string base_template = nodes[0].contents[0].value.ctstrip; 705 auto base_idx = files.countUntil!(f => matchesName(f.name, base_template, files[file_index].name)); 706 assert(base_idx >= 0, "Missing base template: "~base_template); 707 708 // collect all blocks 709 foreach (n; nodes[1 .. $]) { 710 BlockInfo.Mode mode; 711 switch (n.name) { 712 default: 713 enforcep(false, "Extension templates may only contain blocks definitions at the root level.", n.loc); 714 break; 715 case Node.SpecialName.comment, Node.SpecialName.hidden: continue; // also allow comments at the root level 716 case "block": mode = BlockInfo.Mode.replace; break; 717 case "prepend": mode = BlockInfo.Mode.prepend; break; 718 case "append": mode = BlockInfo.Mode.append; break; 719 } 720 enforcep(n.contents.length > 0 && n.contents[0].kind == NodeContent.Kind.text, 721 "'block' must have a name.", n.loc); 722 auto name = n.contents[0].value.ctstrip; 723 auto contents = n.contents[1 .. $].filter!(n => n.kind == NodeContent.Kind.node).map!(n => n.node).array; 724 blocks ~= BlockInfo(name, mode, contents); 725 } 726 727 // parse base template 728 return parseDietWithExtensions(files, base_idx, blocks, import_stack ~ file_index); 729 } 730 731 static string extractFilename(Node n) 732 { 733 enforcep(n.contents.length >= 1 && n.contents[0].kind != NodeContent.Kind.node, 734 "Missing block name.", n.loc); 735 enforcep(n.contents[0].kind == NodeContent.Kind.text, 736 "Dynamic includes are not supported.", n.loc); 737 enforcep(n.contents.length == 1 || n.contents[1 .. $].all!(nc => nc.kind == NodeContent.Kind.node), 738 "'"~n.name~"' must only contain a block name and child nodes.", n.loc); 739 enforcep(n.attributes.length == 0, "'"~n.name~"' cannot have attributes.", n.loc); 740 return n.contents[0].value.ctstrip; 741 } 742 743 Nullable!(Node[]) processNode(Node n) { 744 Nullable!(Node[]) ret; 745 746 void insert(Node[] nodes) { 747 foreach (i, n; nodes) { 748 auto np = processNode(n); 749 if (!np.isNull()) { 750 if (ret.isNull) ret = nodes[0 .. i]; 751 ret ~= np; 752 } else if (!ret.isNull) ret ~= n; 753 } 754 if (ret.isNull && nodes.length) ret = nodes; 755 } 756 757 if (n.name == "block") { 758 auto name = extractFilename(n); 759 auto blockdefs = blocks.filter!(b => b.name == name); 760 761 foreach (b; blockdefs.save.filter!(b => b.mode == BlockInfo.Mode.prepend)) 762 insert(b.contents); 763 764 auto replblocks = blockdefs.save.find!(b => b.mode == BlockInfo.Mode.replace); 765 if (!replblocks.empty) { 766 insert(replblocks.front.contents); 767 } else { 768 insert(n.contents[1 .. $].map!((nc) { 769 assert(nc.kind == NodeContent.Kind.node, "Block contains non-node child!?"); 770 return nc.node; 771 }).array); 772 } 773 774 foreach (b; blockdefs.save.filter!(b => b.mode == BlockInfo.Mode.append)) 775 insert(b.contents); 776 777 if (ret.isNull) ret = []; 778 } else if (n.name == "include") { 779 auto name = extractFilename(n); 780 enforcep(n.contents.length == 1, "Includes cannot have children.", n.loc); 781 auto fidx = files.countUntil!(f => matchesName(f.name, name, files[file_index].name)); 782 enforcep(fidx >= 0, "Missing include input file: "~name, n.loc); 783 insert(parseDietWithExtensions(files, fidx, blocks, import_stack ~ file_index)); 784 } else { 785 n.contents.modifyArray!((nc) { 786 Nullable!(NodeContent[]) rn; 787 if (nc.kind == NodeContent.Kind.node) { 788 auto mod = processNode(nc.node); 789 if (!mod.isNull()) rn = mod.map!(n => NodeContent.tag(n)).array; 790 } 791 assert(rn.isNull || rn.get.all!(n => n.node.name != "block")); 792 return rn; 793 }); 794 } 795 796 assert(ret.isNull || ret.get.all!(n => n.name != "block")); 797 798 return ret; 799 } 800 801 nodes.modifyArray!(processNode); 802 803 assert(nodes.all!(n => n.name != "block")); 804 805 return nodes; 806 } 807 808 private struct BlockInfo { 809 enum Mode { 810 prepend, 811 replace, 812 append 813 } 814 string name; 815 Mode mode = Mode.replace; 816 Node[] contents; 817 } 818 819 private struct FileInfo { 820 string name; 821 Node[] nodes; 822 } 823 824 825 /** Parses a single Diet template file, without resolving includes and extensions. 826 827 See_Also: `parseDiet` 828 */ 829 Node[] parseDietRaw(alias TR)(InputFile file) 830 { 831 import std.algorithm.iteration : map; 832 import std.algorithm.comparison : among; 833 import std.array : array; 834 835 string indent_style; 836 auto loc = Location(file.name, 0); 837 int prevlevel = -1; 838 string input = file.contents; 839 Node[] ret; 840 // nested stack of nodes 841 // the first dimension is corresponds to indentation based nesting 842 // the second dimension is for in-line nested nodes 843 Node[][] stack; 844 stack.length = 8; 845 string previndent; // inherited by blank lines 846 847 next_line: 848 while (input.length) { 849 Node pnode; 850 if (prevlevel >= 0 && stack[prevlevel].length) pnode = stack[prevlevel][$-1]; 851 852 // skip whitespace at the beginning of the line 853 string indent = input.skipIndent(); 854 855 // treat empty lines as if they had the indendation level of the last non-empty line 856 if (input.empty || input[0].among('\n', '\r')) 857 indent = previndent; 858 else previndent = indent; 859 860 enforcep(prevlevel >= 0 || indent.length == 0, "First node must not be indented.", loc); 861 862 // determine the indentation style (tabs/spaces) from the first indented 863 // line of the file 864 if (indent.length && !indent_style.length) indent_style = indent; 865 866 // determine nesting level 867 bool is_text_line = pnode && (pnode.attribs & (NodeAttribs.textNode|NodeAttribs.rawTextNode)) != 0; 868 int level = 0; 869 if (indent_style.length) { 870 while (indent.startsWith(indent_style)) { 871 if (level > prevlevel) { 872 enforcep(is_text_line, "Line is indented too deeply.", loc); 873 break; 874 } 875 level++; 876 indent = indent[indent_style.length .. $]; 877 } 878 } 879 880 enforcep(is_text_line || indent.length == 0, "Mismatched indentation style.", loc); 881 882 // read the whole line as text if the parent node is a pure text node 883 // ("." suffix) or pure raw text node (e.g. comments) 884 if (level > prevlevel && prevlevel >= 0) { 885 if (pnode.attribs & NodeAttribs.textNode) { 886 if (!pnode.contents.empty) 887 pnode.addText("\n", loc); 888 if (indent.length) pnode.addText(indent, loc); 889 if (pnode.attribs & NodeAttribs.translated) { 890 size_t idx; 891 Location loccopy = loc; 892 auto ln = TR(skipLine(input, idx, loc)); 893 input = input[idx .. $]; 894 parseTextLine(ln, pnode, loccopy); 895 } else parseTextLine(input, pnode, loc); 896 continue; 897 } else if (pnode.attribs & NodeAttribs.rawTextNode) { 898 if (!pnode.contents.empty) 899 pnode.addText("\n", loc); 900 if (indent.length) pnode.addText(indent, loc); 901 auto tmploc = loc; 902 pnode.addText(skipLine(input, loc), tmploc); 903 continue; 904 } 905 } 906 907 // skip empty lines 908 if (input.empty) break; 909 else if (input[0] == '\n') { loc.line++; input.popFront(); continue; } 910 else if (input[0] == '\r') { 911 loc.line++; 912 input.popFront(); 913 if (!input.empty && input[0] == '\n') 914 input.popFront(); 915 continue; 916 } 917 918 // parse the line and write it to the stack: 919 920 if (stack.length < level+1) stack.length = level+1; 921 922 if (input.startsWith("//")) { 923 // comments 924 auto n = new Node; 925 n.loc = loc; 926 if (input[2 .. $].startsWith("-")) { n.name = Node.SpecialName.hidden; input = input[3 .. $]; } 927 else { n.name = Node.SpecialName.comment; input = input[2 .. $]; } 928 n.attribs |= NodeAttribs.rawTextNode; 929 auto tmploc = loc; 930 n.addText(skipLine(input, loc), tmploc); 931 stack[level] = [n]; 932 } else if (input.startsWith('-')) { 933 // D statements 934 input = input[1 .. $]; 935 auto n = new Node; 936 n.loc = loc; 937 n.name = Node.SpecialName.code; 938 auto tmploc = loc; 939 n.addText(skipLine(input, loc), tmploc); 940 stack[level] = [n]; 941 } else if (input.startsWith(':')) { 942 // filters 943 stack[level] = []; 944 945 946 string chain; 947 948 do { 949 input = input[1 .. $]; 950 size_t idx = 0; 951 if (chain.length) chain ~= ' '; 952 chain ~= skipIdent(input, idx, "-_", loc, false, true); 953 input = input[idx .. $]; 954 if (input.startsWith(' ')) input = input[1 .. $]; 955 } while (input.startsWith(':')); 956 957 Node chn = new Node; 958 chn.loc = loc; 959 chn.name = Node.SpecialName.filter; 960 chn.attribs = NodeAttribs.textNode; 961 chn.attributes = [Attribute(loc, "filterChain", [AttributeContent.text(chain)])]; 962 stack[level] ~= chn; 963 964 /*auto tmploc = loc; 965 auto trailing = skipLine(input, loc); 966 if (trailing.length) parseTextLine(input, chn, tmploc);*/ 967 parseTextLine(input, chn, loc); 968 } else { 969 // normal tag line 970 bool has_nested; 971 stack[level] = null; 972 do stack[level] ~= parseTagLine!TR(input, loc, has_nested); 973 while (has_nested); 974 } 975 976 // add it to its parent contents 977 foreach (i; 1 .. stack[level].length) 978 stack[level][i-1].contents ~= NodeContent.tag(stack[level][i]); 979 if (level > 0) stack[level-1][$-1].contents ~= NodeContent.tag(stack[level][0]); 980 else ret ~= stack[0][0]; 981 982 // remember the nesting level for the next line 983 prevlevel = level; 984 } 985 986 return ret; 987 } 988 989 private Node parseTagLine(alias TR)(ref string input, ref Location loc, out bool has_nested) 990 { 991 size_t idx = 0; 992 993 auto ret = new Node; 994 ret.loc = loc; 995 996 if (input.startsWith("!!! ")) { // legacy doctype support 997 input = input[4 .. $]; 998 ret.name = "doctype"; 999 parseTextLine(input, ret, loc); 1000 return ret; 1001 } 1002 1003 if (input.startsWith('<')) { // inline HTML/XML 1004 ret.name = Node.SpecialName.text; 1005 parseTextLine(input, ret, loc); 1006 return ret; 1007 } 1008 1009 if (input.startsWith('|')) { // text line 1010 input = input[1 .. $]; 1011 ret.name = Node.SpecialName.text; 1012 if (idx < input.length && input[idx] == '&') { ret.attribs |= NodeAttribs.translated; idx++; } 1013 } else { // normal tag 1014 if (parseTag(input, idx, ret, has_nested, loc)) 1015 return ret; 1016 } 1017 1018 if (idx+1 < input.length && input[idx .. idx+2] == "!=") { 1019 enforcep(!(ret.attribs & NodeAttribs.translated), "Compile-time translation is not supported for (raw) assignments.", ret.loc); 1020 idx += 2; 1021 auto l = loc; 1022 ret.contents ~= NodeContent.rawInterpolation(ctstrip(skipLine(input, idx, loc)), l); 1023 input = input[idx .. $]; 1024 } else if (idx < input.length && input[idx] == '=') { 1025 enforcep(!(ret.attribs & NodeAttribs.translated), "Compile-time translation is not supported for assignments.", ret.loc); 1026 idx++; 1027 auto l = loc; 1028 ret.contents ~= NodeContent.interpolation(ctstrip(skipLine(input, idx, loc)), l); 1029 input = input[idx .. $]; 1030 } else { 1031 auto tmploc = loc; 1032 auto remainder = skipLine(input, idx, loc); 1033 input = input[idx .. $]; 1034 1035 if (remainder.length && remainder[0] == ' ') { 1036 // parse the rest of the line as text contents (if any non-ws) 1037 remainder = remainder[1 .. $]; 1038 if (ret.attribs & NodeAttribs.translated) 1039 remainder = TR(remainder); 1040 parseTextLine(remainder, ret, tmploc); 1041 } else if (ret.name == Node.SpecialName.text) { 1042 // allow omitting the whitespace for "|" text nodes 1043 if (ret.attribs & NodeAttribs.translated) 1044 remainder = TR(remainder); 1045 parseTextLine(remainder, ret, tmploc); 1046 } else { 1047 import std..string : strip; 1048 enforcep(remainder.strip().length == 0, 1049 "Expected node text separated by a space character or end of line, but got '"~remainder~"'.", loc); 1050 } 1051 } 1052 1053 return ret; 1054 } 1055 1056 private bool parseTag(ref string input, ref size_t idx, ref Node dst, ref bool has_nested, ref Location loc) 1057 { 1058 import std.ascii : isWhite; 1059 1060 dst.name = skipIdent(input, idx, ":-_", loc, true); 1061 1062 // a trailing ':' is not part of the tag name, but signals a nested node 1063 if (dst.name.endsWith(":")) { 1064 dst.name = dst.name[0 .. $-1]; 1065 idx--; 1066 } 1067 1068 bool have_id = false; 1069 while (idx < input.length) { 1070 if (input[idx] == '#') { 1071 // node ID 1072 idx++; 1073 auto value = skipIdent(input, idx, "-_", loc); 1074 enforcep(value.length > 0, "Expected id.", loc); 1075 enforcep(!have_id, "Only one \"id\" definition using '#' is allowed.", loc); 1076 have_id = true; 1077 dst.attributes ~= Attribute.text("id", value, loc); 1078 } else if (input[idx] == '.') { 1079 // node classes 1080 if (idx+1 >= input.length || input[idx+1].isWhite) 1081 goto textBlock; 1082 idx++; 1083 auto value = skipIdent(input, idx, "-_", loc); 1084 enforcep(value.length > 0, "Expected class name identifier.", loc); 1085 dst.attributes ~= Attribute.text("class", value, loc); 1086 } else break; 1087 } 1088 1089 // generic attributes 1090 if (idx < input.length && input[idx] == '(') 1091 parseAttributes(input, idx, dst, loc); 1092 1093 // avoid whitespace inside of tag 1094 if (idx < input.length && input[idx] == '<') { 1095 idx++; 1096 dst.attribs |= NodeAttribs.fitInside; 1097 } 1098 1099 // avoid whitespace outside of tag 1100 if (idx < input.length && input[idx] == '>') { 1101 idx++; 1102 dst.attribs |= NodeAttribs.fitOutside; 1103 } 1104 1105 // avoid whitespace inside of tag (also allowed after >) 1106 if (!(dst.attribs & NodeAttribs.fitInside) && idx < input.length && input[idx] == '<') { 1107 idx++; 1108 dst.attribs |= NodeAttribs.fitInside; 1109 } 1110 1111 // translate text contents 1112 if (idx < input.length && input[idx] == '&') { 1113 idx++; 1114 dst.attribs |= NodeAttribs.translated; 1115 } 1116 1117 // treat nested lines as text 1118 if (idx < input.length && input[idx] == '.') { 1119 textBlock: 1120 dst.attribs |= NodeAttribs.textNode; 1121 idx++; 1122 skipLine(input, idx, loc); // ignore the rest of the line 1123 input = input[idx .. $]; 1124 return true; 1125 } 1126 1127 // another nested tag on the same line 1128 if (idx < input.length && input[idx] == ':') { 1129 idx++; 1130 1131 // skip trailing whitespace (but no line breaks) 1132 while (idx < input.length && (input[idx] == ' ' || input[idx] == '\t')) 1133 idx++; 1134 1135 // see if we got anything left on the line 1136 if (idx < input.length) { 1137 if (input[idx] == '\n' || input[idx] == '\r') { 1138 // FIXME: should we rather error out here? 1139 skipLine(input, idx, loc); 1140 } else { 1141 // leaves the rest of the line to parse another tag 1142 has_nested = true; 1143 } 1144 } 1145 input = input[idx .. $]; 1146 return true; 1147 } 1148 1149 return false; 1150 } 1151 1152 /** 1153 Parses a single line of text (possibly containing interpolations and inline tags). 1154 1155 If there a a newline at the end, it will be appended to the contents of the 1156 destination node. 1157 */ 1158 private void parseTextLine(ref string input, ref Node dst, ref Location loc) 1159 { 1160 import std.algorithm.comparison : among; 1161 1162 size_t sidx = 0, idx = 0; 1163 1164 void flushText() 1165 { 1166 if (idx > sidx) dst.addText(input[sidx .. idx], loc); 1167 } 1168 1169 while (idx < input.length) { 1170 char cur = input[idx]; 1171 switch (cur) { 1172 default: idx++; break; 1173 case '\\': 1174 if (idx+1 < input.length && input[idx+1].among('#', '!')) { 1175 flushText(); 1176 sidx = idx+1; 1177 idx += 2; 1178 } else idx++; 1179 break; 1180 case '!', '#': 1181 if (idx+1 < input.length && input[idx+1] == '{') { 1182 flushText(); 1183 idx += 2; 1184 auto expr = skipUntilClosingBrace(input, idx, loc); 1185 idx++; 1186 if (cur == '#') dst.contents ~= NodeContent.interpolation(expr, loc); 1187 else dst.contents ~= NodeContent.rawInterpolation(expr, loc); 1188 sidx = idx; 1189 } else if (cur == '#' && idx+1 < input.length && input[idx+1] == '[') { 1190 flushText(); 1191 idx += 2; 1192 auto tag = skipUntilClosingBracket(input, idx, loc); 1193 idx++; 1194 bool has_nested; 1195 auto itag = parseTagLine!identity(tag, loc, has_nested); 1196 enforcep(!(itag.attribs & (NodeAttribs.textNode|NodeAttribs.rawTextNode)), 1197 "Multi-line text nodes are not permitted for inline-tags.", loc); 1198 enforcep(!(itag.attribs & NodeAttribs.translated), 1199 "Inline-tags cannot be translated individually.", loc); 1200 enforcep(!has_nested, "Nested inline-tags not allowed.", loc); 1201 dst.contents ~= NodeContent.tag(itag); 1202 sidx = idx; 1203 } else idx++; 1204 break; 1205 case '\r': 1206 flushText(); 1207 idx++; 1208 if (idx < input.length && input[idx] == '\n') idx++; 1209 input = input[idx .. $]; 1210 loc.line++; 1211 return; 1212 case '\n': 1213 flushText(); 1214 idx++; 1215 input = input[idx .. $]; 1216 loc.line++; 1217 return; 1218 } 1219 } 1220 1221 flushText(); 1222 assert(idx == input.length); 1223 input = null; 1224 } 1225 1226 private string skipLine(ref string input, ref size_t idx, ref Location loc) 1227 { 1228 auto sidx = idx; 1229 1230 while (idx < input.length) { 1231 char cur = input[idx]; 1232 switch (cur) { 1233 default: idx++; break; 1234 case '\r': 1235 auto ret = input[sidx .. idx]; 1236 idx++; 1237 if (idx < input.length && input[idx] == '\n') idx++; 1238 loc.line++; 1239 return ret; 1240 case '\n': 1241 auto ret = input[sidx .. idx]; 1242 idx++; 1243 loc.line++; 1244 return ret; 1245 } 1246 } 1247 1248 return input[sidx .. $]; 1249 } 1250 1251 private string skipLine(ref string input, ref Location loc) 1252 { 1253 size_t idx = 0; 1254 auto ret = skipLine(input, idx, loc); 1255 input = input[idx .. $]; 1256 return ret; 1257 } 1258 1259 private void parseAttributes(ref string input, ref size_t i, ref Node node, in ref Location loc) 1260 { 1261 assert(i < input.length && input[i] == '('); 1262 i++; 1263 1264 skipAnyWhitespace(input, i); 1265 while (i < input.length && input[i] != ')') { 1266 string name = parseIdent(input, i, ",)=", loc); 1267 string value; 1268 skipAnyWhitespace(input, i); 1269 if( i < input.length && input[i] == '=' ){ 1270 i++; 1271 skipAnyWhitespace(input, i); 1272 enforcep(i < input.length, "'=' must be followed by attribute string.", loc); 1273 value = skipExpression(input, i, loc, true); 1274 assert(i <= input.length); 1275 if (isStringLiteral(value) && value[0] == '\'') { 1276 auto tmp = dstringUnescape(value[1 .. $-1]); 1277 value = '"' ~ dstringEscape(tmp) ~ '"'; 1278 } 1279 } else value = "true"; 1280 1281 enforcep(i < input.length, "Unterminated attribute section.", loc); 1282 enforcep(input[i] == ')' || input[i] == ',', "Unexpected text following attribute: '"~input[0..i]~"' ('"~input[i..$]~"')", loc); 1283 if (input[i] == ',') { 1284 i++; 1285 skipAnyWhitespace(input, i); 1286 } 1287 1288 if (name == "class" && value == `""`) continue; 1289 1290 if (isStringLiteral(value)) { 1291 AttributeContent[] content; 1292 parseAttributeText(value[1 .. $-1], content, loc); 1293 node.attributes ~= Attribute(loc, name, content); 1294 } else { 1295 node.attributes ~= Attribute.expr(name, value, loc); 1296 } 1297 } 1298 1299 enforcep(i < input.length, "Missing closing clamp.", loc); 1300 i++; 1301 } 1302 1303 private void parseAttributeText(string input, ref AttributeContent[] dst, in ref Location loc) 1304 { 1305 size_t sidx = 0, idx = 0; 1306 1307 void flushText() 1308 { 1309 if (idx > sidx) dst ~= AttributeContent.text(input[sidx .. idx]); 1310 } 1311 1312 while (idx < input.length) { 1313 char cur = input[idx]; 1314 switch (cur) { 1315 default: idx++; break; 1316 case '\\': 1317 flushText(); 1318 dst ~= AttributeContent.text(dstringUnescape(sanitizeEscaping(input[idx .. idx+2]))); 1319 idx += 2; 1320 sidx = idx; 1321 break; 1322 case '!', '#': 1323 if (idx+1 < input.length && input[idx+1] == '{') { 1324 flushText(); 1325 idx += 2; 1326 auto expr = dstringUnescape(skipUntilClosingBrace(input, idx, loc)); 1327 idx++; 1328 if (cur == '#') dst ~= AttributeContent.interpolation(expr); 1329 else dst ~= AttributeContent.rawInterpolation(expr); 1330 sidx = idx; 1331 } else idx++; 1332 break; 1333 } 1334 } 1335 1336 flushText(); 1337 input = input[idx .. $]; 1338 } 1339 1340 private string skipUntilClosingBrace(in ref string s, ref size_t idx, in ref Location loc) 1341 { 1342 import std.algorithm.comparison : among; 1343 1344 int level = 0; 1345 auto start = idx; 1346 while( idx < s.length ){ 1347 if( s[idx] == '{' ) level++; 1348 else if( s[idx] == '}' ) level--; 1349 enforcep(!s[idx].among('\n', '\r'), "Missing '}' before end of line.", loc); 1350 if( level < 0 ) return s[start .. idx]; 1351 idx++; 1352 } 1353 enforcep(false, "Missing closing brace", loc); 1354 assert(false); 1355 } 1356 1357 private string skipUntilClosingBracket(in ref string s, ref size_t idx, in ref Location loc) 1358 { 1359 import std.algorithm.comparison : among; 1360 1361 int level = 0; 1362 auto start = idx; 1363 while( idx < s.length ){ 1364 if( s[idx] == '[' ) level++; 1365 else if( s[idx] == ']' ) level--; 1366 enforcep(!s[idx].among('\n', '\r'), "Missing ']' before end of line.", loc); 1367 if( level < 0 ) return s[start .. idx]; 1368 idx++; 1369 } 1370 enforcep(false, "Missing closing bracket", loc); 1371 assert(false); 1372 } 1373 1374 private string skipIdent(in ref string s, ref size_t idx, string additional_chars, in ref Location loc, bool accept_empty = false, bool require_alpha_start = false) 1375 { 1376 import std.ascii : isAlpha; 1377 1378 size_t start = idx; 1379 while (idx < s.length) { 1380 if (isAlpha(s[idx])) idx++; 1381 else if ((!require_alpha_start || start != idx) && s[idx] >= '0' && s[idx] <= '9') idx++; 1382 else { 1383 bool found = false; 1384 foreach (ch; additional_chars) 1385 if (s[idx] == ch) { 1386 found = true; 1387 idx++; 1388 break; 1389 } 1390 if (!found) { 1391 enforcep(accept_empty || start != idx, "Expected identifier but got '"~s[idx]~"'.", loc); 1392 return s[start .. idx]; 1393 } 1394 } 1395 } 1396 enforcep(start != idx, "Expected identifier but got nothing.", loc); 1397 return s[start .. idx]; 1398 } 1399 1400 /// Skips all trailing spaces and tab characters of the input string. 1401 private string skipIndent(ref string input) 1402 { 1403 size_t idx = 0; 1404 while (idx < input.length && isIndentChar(input[idx])) 1405 idx++; 1406 auto ret = input[0 .. idx]; 1407 input = input[idx .. $]; 1408 return ret; 1409 } 1410 1411 private bool isIndentChar(dchar ch) { return ch == ' ' || ch == '\t'; } 1412 1413 private string skipAnyWhitespace(in ref string s, ref size_t idx) 1414 { 1415 import std.ascii : isWhite; 1416 1417 size_t start = idx; 1418 while (idx < s.length) { 1419 if (s[idx].isWhite) idx++; 1420 else break; 1421 } 1422 return s[start .. idx]; 1423 } 1424 1425 private bool isStringLiteral(string str) 1426 { 1427 size_t i = 0; 1428 1429 // skip leading white space 1430 while (i < str.length && (str[i] == ' ' || str[i] == '\t')) i++; 1431 1432 // no string literal inside 1433 if (i >= str.length) return false; 1434 1435 char delimiter = str[i++]; 1436 if (delimiter != '"' && delimiter != '\'') return false; 1437 1438 while (i < str.length && str[i] != delimiter) { 1439 if (str[i] == '\\') i++; 1440 i++; 1441 } 1442 1443 // unterminated string literal 1444 if (i >= str.length) return false; 1445 1446 i++; // skip delimiter 1447 1448 // skip trailing white space 1449 while (i < str.length && (str[i] == ' ' || str[i] == '\t')) i++; 1450 1451 // check if the string has ended with the closing delimiter 1452 return i == str.length; 1453 } 1454 1455 unittest { 1456 assert(isStringLiteral(`""`)); 1457 assert(isStringLiteral(`''`)); 1458 assert(isStringLiteral(`"hello"`)); 1459 assert(isStringLiteral(`'hello'`)); 1460 assert(isStringLiteral(` "hello" `)); 1461 assert(isStringLiteral(` 'hello' `)); 1462 assert(isStringLiteral(`"hel\"lo"`)); 1463 assert(isStringLiteral(`"hel'lo"`)); 1464 assert(isStringLiteral(`'hel\'lo'`)); 1465 assert(isStringLiteral(`'hel"lo'`)); 1466 assert(isStringLiteral(`'#{"address_"~item}'`)); 1467 assert(!isStringLiteral(`"hello\`)); 1468 assert(!isStringLiteral(`"hello\"`)); 1469 assert(!isStringLiteral(`"hello\"`)); 1470 assert(!isStringLiteral(`"hello'`)); 1471 assert(!isStringLiteral(`'hello"`)); 1472 assert(!isStringLiteral(`"hello""world"`)); 1473 assert(!isStringLiteral(`"hello" "world"`)); 1474 assert(!isStringLiteral(`"hello" world`)); 1475 assert(!isStringLiteral(`'hello''world'`)); 1476 assert(!isStringLiteral(`'hello' 'world'`)); 1477 assert(!isStringLiteral(`'hello' world`)); 1478 assert(!isStringLiteral(`"name" value="#{name}"`)); 1479 } 1480 1481 private string skipExpression(in ref string s, ref size_t idx, in ref Location loc, bool multiline = false) 1482 { 1483 string clamp_stack; 1484 size_t start = idx; 1485 outer: 1486 while (idx < s.length) { 1487 switch (s[idx]) { 1488 default: break; 1489 case '\n', '\r': 1490 enforcep(multiline, "Unexpected end of line.", loc); 1491 break; 1492 case ',': 1493 if (clamp_stack.length == 0) 1494 break outer; 1495 break; 1496 case '"', '\'': 1497 idx++; 1498 skipAttribString(s, idx, s[idx-1], loc); 1499 break; 1500 case '(': clamp_stack ~= ')'; break; 1501 case '[': clamp_stack ~= ']'; break; 1502 case '{': clamp_stack ~= '}'; break; 1503 case ')', ']', '}': 1504 if (s[idx] == ')' && clamp_stack.length == 0) 1505 break outer; 1506 enforcep(clamp_stack.length > 0 && clamp_stack[$-1] == s[idx], 1507 "Unexpected '"~s[idx]~"'", loc); 1508 clamp_stack.length--; 1509 break; 1510 } 1511 idx++; 1512 } 1513 1514 enforcep(clamp_stack.length == 0, "Expected '"~clamp_stack[$-1]~"' before end of attribute expression.", loc); 1515 return ctstrip(s[start .. idx]); 1516 } 1517 1518 private string skipAttribString(in ref string s, ref size_t idx, char delimiter, in ref Location loc) 1519 { 1520 size_t start = idx; 1521 while( idx < s.length ){ 1522 if( s[idx] == '\\' ){ 1523 // pass escape character through - will be handled later by buildInterpolatedString 1524 idx++; 1525 enforcep(idx < s.length, "'\\' must be followed by something (escaped character)!", loc); 1526 } else if( s[idx] == delimiter ) break; 1527 idx++; 1528 } 1529 enforcep(idx < s.length, "Unterminated attribute string: "~s[start-1 .. $]~"||", loc); 1530 return s[start .. idx]; 1531 } 1532 1533 private bool matchesName(string filename, string logical_name, string parent_name) 1534 { 1535 import std.path : extension; 1536 if (filename == logical_name) return true; 1537 auto ext = extension(parent_name); 1538 if (filename.endsWith(ext) && filename[0 .. $-ext.length] == logical_name) return true; 1539 return false; 1540 } 1541 1542 private void modifyArray(alias modify, T)(ref T[] arr) 1543 { 1544 size_t i = 0; 1545 while (i < arr.length) { 1546 auto mod = modify(arr[i]); 1547 if (mod.isNull()) i++; 1548 else { 1549 arr = arr[0 .. i] ~ mod.get() ~ arr[i+1 .. $]; 1550 i += mod.length; 1551 } 1552 } 1553 }