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