1 /* demmet - D implemention of emmet */ 2 module demmet; 3 4 import std.algorithm : canFind, splitter; 5 6 // dfmt off 7 import 8 std.array, 9 std.format, 10 std.regex, 11 std.string, 12 std.traits; 13 14 alias TagProcFunc = bool function(ref TagProp prop); 15 16 struct TagProp { 17 string tag; 18 string[] tags; 19 string[] attr; 20 string content; 21 string match; 22 string token; 23 string[] result; 24 } 25 26 TagProcFunc[] tagProcs = [&fabbr]; 27 28 private: 29 int toInt(string str) { 30 import core.stdc.stdio; 31 32 int result; 33 sscanf(str.toStringz, "%d", &result); 34 return result; 35 } 36 37 T pop(T)(ref T[] arr) { 38 if (!arr.length) 39 return T.init; 40 T back = arr[$-1]; 41 arr.popBack(); 42 return back; 43 } 44 45 enum { 46 // default elements 47 itags = [ 48 "audio":"source", 49 "colgroup":"col", 50 "datalist":"option", 51 "details":"summary", 52 "dl":"dt", 53 "em":"span", 54 "fieldset":"legend", 55 "figure":"figcaption", 56 "frameset":"frame", 57 "html":"body", 58 "input":"input", 59 "label":"input", 60 "map":"area", 61 "menu":"menuitem", 62 "menuitem":"menuitem", 63 "ul":"li", 64 "ol":"li", 65 "picture":"img", 66 "optgroup":"option", 67 "select":"option", 68 "table":"tr", 69 "tbody":"tr", 70 "thead":"tr", 71 "tfoot":"tr", 72 "tr":"td", 73 "video":"source", 74 ], 75 // tagname abbreviations 76 tabbr = [ 77 "!":"!DOCTYPE html", 78 "ab":"abbr", 79 "adr":"address", 80 "ar":"area", 81 "arti":"article", 82 "asd":"aside", 83 "bq":"blockquote", 84 "btn":"button", 85 "colg":"colgroup", 86 "cap":"caption", 87 "cmd":"command", 88 "cv":"canvas", 89 "dat":"data", 90 "datg":"datagrid", "datag":"datagrid", 91 "datl":"datalist", "datal":"datalist", 92 "det":"details", 93 "dlg":"dialog", 94 "emb":"embed", 95 "fig":"figure", 96 "figc":"figcaption", 97 "fm":"form", 98 "fset":"fieldset", 99 "ftr":"footer", 100 "hdr":"header", 101 "ifr":"iframe", 102 "inp":"input", 103 "lab":"label", 104 "leg":"legend", 105 "mk":"mark", 106 "obj":"object", 107 "opt":"option", 108 "optg":"optgroup", 109 "out":"output", 110 "pic":"picture", 111 "pr":"pre", 112 "prog":"progress", 113 "scr":"script", 114 "sect":"section", 115 "sel":"select", 116 "sm":"samp", 117 "summ":"summary", 118 "sp":"span", 119 "src":"source", 120 "str":"strong", 121 "sty":"style", 122 "tab":"table", 123 "tbd":"tbody", 124 "tem":"template", 125 "tft":"tfoot", 126 "thd":"thead", 127 "tpl":"template", 128 "trk":"track", 129 "txa":"textarea", 130 "vid":"video", 131 "wb":"wbr" 132 ], 133 // attribute abbreviations 134 aabbr = [ 135 "a":"alt", 136 "ak":"accesskey", 137 "autocap":"autocapitalize", 138 "ce":"contenteditable", 139 "d":"dir", 140 "dr":"draggable", 141 "dz":"dropzone", 142 "n":"name", 143 "h":"height", 144 "hid":"hidden", 145 "im":"inputmode", 146 "l":"lang", 147 "s":"style", 148 "sc":"spellcheck", 149 "t":"type", 150 "tt":"title", 151 "ti":"tabindex", 152 "v":"value", 153 "w":"width" 154 ], 155 // extended attributes 156 eabbr = [ 157 "css":` rel="stylesheet"`, 158 "print":` rel="stylesheet" media="print"`, 159 "favicon":` rel="shortcut icon" type="image/x-icon" href="favicon.ico"`, 160 "touch":` rel="apple-touch-icon"`, 161 "rss":` rel="alternate" type="application/rss+xml" title="RSS"`, 162 "atom":` rel="alternate" type="application/atom+xml" title="Atom"`, 163 "import":` rel="import"`, 164 "d":` disabled="disabled"`, 165 "hidden":` type="hidden"`, 166 "search":` type="search"`, 167 "email":` type="email"`, 168 "url":` type="url"`, 169 "pwd":` type="password"`, 170 "date":` type="date"`, 171 "dateloc":` type="datetime-local"`, 172 "tel":` type="tel"`, 173 "number":` type="number"`, 174 "checkbox":` type="checkbox"`, 175 "radio":` type="radio"`, 176 "range":` type="range"`, 177 "file":` type="file"`, 178 "submit":` type="submit"`, 179 "img":` type="image"`, 180 "button":` type="button"`, 181 "btn":` type="button"`, 182 ] 183 } 184 // dfmt on 185 186 public: 187 188 // two fns for counting single line nest tokens (.a>.b^.c) 189 int countChar(in char[] input, char c) nothrow { 190 int t; 191 for (size_t n; n < input.length; n++) 192 if (input[n] == c) 193 t++; 194 return t; 195 } 196 197 int countTokens(string str, char c) { 198 enum { 199 re1 = ctRegex!`[^\\]?".+?[^\\]"`, 200 re2 = ctRegex!`[^\\]?'.+?[^\\]'`, 201 re3 = ctRegex!`[^\\]?\{.+?[^\\]\}` 202 } 203 return str.replaceAll(re1, "").replaceAll(re2, "").replaceAll(re3, "").countChar(c); 204 } 205 206 int getTabLevel(string e, in char[] indent = "") { 207 int i; 208 if (indent.length) 209 e = e.replace(indent, "\t"); 210 while (i < e.length && e[i] == '\t') 211 i++; 212 return i; 213 } 214 215 // make `^>+` out of tabs (normally emmet does nesting like ".a>.b" and unnesting like ".b^.a_sibling", now we can use tabs) 216 string extractTabs(string input, in char[] indent = "") { 217 int r = -1, l; 218 auto res = appender!string; 219 for (size_t i; i < input.length;) { 220 size_t j = i; 221 loop: do { 222 switch (input[i++]) { 223 case '\n': 224 if (!l) 225 break loop; 226 break; 227 case '{': 228 if (l >= 0) 229 l++; 230 break; 231 case '[': 232 if (l < 1) 233 l--; 234 break; 235 case '}': 236 if (l > 0) 237 l--; 238 break; 239 case ']': 240 if (l < 0) 241 l++; 242 goto default; 243 default: 244 break; 245 } 246 } 247 while (i < input.length); 248 auto line = input[j .. i]; 249 int level = getTabLevel(line, indent); 250 auto str = line.strip(); 251 const s = str; 252 if (str.length) { 253 if (r >= 0) { 254 if (level > r || str[0] == '*') 255 str = '>' ~ str; 256 else if (level == r) 257 str = '+' ~ str; 258 else if (level < r) 259 str = "^".replicate(r - level) ~ str; 260 } 261 r = level + countTokens(s, '>') - countTokens(s, '^'); 262 } 263 res ~= str; 264 } 265 return res[]; 266 } 267 268 unittest { 269 auto str = extractTabs( 270 `ul 271 . 272 span`); 273 assert(str == "ul>.^span", str); 274 str = extractTabs( 275 `pre{ 276 . 277 }`); 278 assert(str == `pre{ 279 . 280 }`, str); 281 } 282 283 string emmet(S : const(char)[])(S input, S indent = "") 284 => zencode(indent.length ? extractTabs(input, indent) : input); 285 286 private string zencode(S : const(char)[])(S input) { 287 static closeTag(string tag) { 288 enum noCloseTags = ctRegex!( 289 `^!|^(area|base|br|col|embed|frame|hr|img|input|link|meta|param|source|wbr)\b`, "i"); 290 if (tag.length && !tag.matchFirst(noCloseTags)) 291 return "</" ~ tag ~ ">"; 292 return ""; 293 } 294 295 enum xmlComment = ctRegex!(`<!--.*?-->`, "s"); 296 297 input = input.replaceAll(xmlComment, ""); 298 auto s = appender!(string[]); 299 string[] tagList, lastGroup, result; 300 size_t[2][] groups; 301 size_t i, len = input.length, g, n; 302 int l; 303 for (; i < len; i++) { 304 const c = input[i]; 305 switch (c) { 306 case '{': 307 if (l >= 0) 308 l++; 309 break; 310 case '[': 311 if (l < 1) 312 l--; 313 break; 314 case '+': 315 case '>': 316 case '^': 317 case '(': 318 case ')': 319 if (!l) { 320 if (g < n) 321 s ~= input[g .. n]; 322 g = n + 1; 323 if (c != '>') 324 s ~= input[i .. i + 1]; 325 } 326 break; 327 328 case '*': 329 if (g < n && !l) { 330 s ~= input[g .. n]; 331 g = n; 332 } 333 break; 334 335 case '}': 336 if (l > 0) 337 l--; 338 if (!l && g < n) { 339 s ~= input[g .. n + 1]; 340 g = n + 1; 341 } 342 break; 343 344 case ']': 345 if (l < 0) 346 l++; 347 break; 348 default: 349 if (!l && i > 0 && input[i - 1] == '}') 350 s ~= "+"; 351 break; 352 } 353 n++; 354 } 355 if (g < len) 356 s ~= input[g .. len]; 357 foreach (token; s[]) { 358 string lastTag; 359 string[] attr = [""]; 360 size_t[2] prevg = void; 361 enum { 362 re1 = ctRegex!`(\$+)@(-?)(\d*)`, 363 re2 = ctRegex!(`((?:[^\[{\s]+(?:(?<=\$)\{[^}]+\})?)*)(\[.+?\])?(?:\{(.+)\})?`, "s"), 364 re3 = ctRegex!(`(?:!|\s).*`, "s") 365 } 366 switch (token[0]) { 367 case '^': 368 if (result.length) { 369 lastTag = result[$ - 1]; 370 if (lastTag.length < 2 || lastTag[0 .. 2] != "</") 371 result ~= closeTag(tagList.pop()); 372 } 373 result ~= closeTag(tagList.pop()); 374 break; 375 case '>': 376 break; 377 case '+': 378 if (result.length) { 379 lastTag = result[$ - 1]; 380 if (lastTag.length < 2 || lastTag[0 .. 2] != "</") 381 result ~= closeTag(tagList.pop()); 382 } 383 break; 384 case '(': 385 groups ~= [result.length, tagList.length]; 386 break; 387 case ')': 388 prevg = groups[$ - 1]; 389 len = prevg[1]; 390 for (g = tagList.length; g-- > len;) 391 result ~= closeTag(tagList.pop()); 392 lastGroup = result[prevg[0] .. $]; 393 break; 394 default: 395 if (token[0] == '*') { 396 string[] tags; 397 g = toInt(cast(string)token[1 .. $]); 398 if (lastGroup.length) { 399 tags = lastGroup; 400 result.length = groups.pop()[0]; 401 } else if (result.length) { 402 tags ~= result.pop(); 403 tags ~= closeTag(tagList.pop()); 404 } 405 for (n = 0; n < g; n++) 406 foreach (r; tags) { 407 result ~= r.replaceAll!((Captures!string c) { 408 import std.conv : text; 409 410 string digs = c[1], 411 direction = c[2], 412 start = c[3]; 413 int st; 414 if (start.length) 415 st = start.toInt; 416 auto v = text((direction.length ? -n : n) + (st ? st 417 : direction.length ? g - 1 : 0)); 418 for (int d = 0, dlen = cast(int)digs.length - cast(int)v.length; 419 d < dlen; d++) 420 v = '0' ~ v; 421 return v; 422 })(re1); 423 } 424 } else { 425 import std.ascii : isWhite; 426 427 lastGroup.length = 0; 428 auto t = token.matchFirst(re2); 429 auto buf = t[2]; // attrs 430 if (buf.length) 431 buf = buf[0 .. $ - 1]; 432 if (buf.length) { 433 string str; 434 for (i = 1; i < buf.length;) { 435 size_t j = i; 436 do 437 if (buf[i] == '=' || buf[i].isWhite) 438 break; 439 while (++i < buf.length); 440 auto key = buf[j .. i]; 441 str ~= aabbr.get(key, key); 442 if (i == buf.length) 443 break; 444 if (buf[i] == '=') { 445 str ~= '='; 446 j = ++i; 447 for (; i < buf.length; i++) 448 if (buf[i].isWhite) 449 break; 450 auto val = buf[j .. i]; 451 str ~= j == buf.length || (buf[j] != '"' && buf[j] != '\'') ? '"' ~ val ~ '"' 452 : val; 453 } 454 if (i == buf.length) 455 break; 456 i++; 457 str ~= ' '; 458 } 459 attr ~= str; 460 } 461 buf = ""; 462 bool first = true; 463 foreach (cls; t[1].splitter('.')) { 464 if (first) { 465 buf = cls; 466 first = false; 467 } else 468 attr[0] ~= attr[0].length ? ' ' ~ cls : cls; 469 } 470 string tag, content = t[3]; 471 first = true; 472 foreach (str; buf.splitter('#')) { 473 if (first) { 474 tag = str; 475 first = false; 476 } else { 477 attr ~= `id="` ~ str ~ '"'; 478 break; 479 } 480 } 481 buf = attr[0]; 482 if (buf.length) 483 attr[0] = ` class="` ~ buf ~ '"'; 484 if (!content.length || tag.length || buf.length || attr.length > 1) { 485 auto prop = TagProp(tag, tagList, attr, content, t[0], token, result); 486 foreach (tagProc; tagProcs) { 487 if (tagProc(prop)) 488 break; 489 } 490 tag = prop.tag; 491 attr = prop.attr; 492 if (tag.length) { 493 result ~= "<%r%(%r %)>%r".format(tag, attr, prop.content); 494 tagList ~= tag.replaceAll(re3, ""); 495 } 496 } else { 497 result ~= content; 498 tagList ~= ""; 499 } 500 } 501 } 502 } 503 for (i = tagList.length; i--;) 504 result ~= closeTag(tagList[i]); 505 return result.join(""); 506 } 507 508 bool fabbr(ref TagProp prop) { 509 enum RE = ctRegex!(`[\s>].*`, "s"); 510 511 auto tag = prop.tag; 512 auto tags = prop.tags; 513 auto result = prop.result; 514 auto s = tag.split(':'); 515 string t; 516 string* p = void; 517 if (s.length) { 518 tag = s[0]; 519 if (tag.length) { 520 t = tag.toLower; 521 p = t in tabbr; 522 while (p && (t = tag.replace(t, *p)) != tag) 523 p = (tag = t) in tabbr; 524 } 525 } 526 if (!tag.length) { 527 if (tags.length) 528 tag = tags[$ - 1]; 529 if (tag.length) 530 tag = itags.get(tag.toLower, ""); 531 if (!tag.length) { 532 if (result.length) 533 tag = result[$ - 1]; 534 if (tag.length) 535 tag = itags.get(tag[1 .. $].replaceFirst(RE, "").toLower, ""); 536 if (!tag.length) 537 tag = "div"; 538 } 539 } 540 t = ""; 541 if (s.length > 1) { 542 p = s[1] in eabbr; 543 if (p) 544 t = *p; 545 } 546 prop.tag = tag ~ t; 547 return false; 548 } 549 550 unittest { 551 void test(string a, string b) { 552 auto str = emmet(a, "\t"); 553 assert(str == b, a ~ ": " ~ str); 554 } 555 556 test("", ""); 557 test("ul>.>a", "<ul><li><a></a></li></ul>"); 558 test(`a[href=#]b`, `<a href="#"></a>`); 559 test(`a(b)`, `<a><b></b></a>`); 560 test(`* single line commet`, ``); 561 test(`a(b) { commet }*`, `<a><b></b></a>`); 562 test(`a(b)(a comment)*`, `<a><b></b></a>`); 563 test(`a 564 {x} 565 b{y} 566 i{z} 567 {0}`, "<a>x<b>y<i>z</i></b>0</a>"); 568 test(`a[data-a={]{foo{1}}b{bar{2}`, `<a data-a="{">foo{1}</a><b>bar{2</b>`); 569 test(`a[href=#t$@]*1 b+s+a*0`, `<a href="#t0"></a><s></s>`); 570 test("a[hid]", `<a hidden></a>`); 571 test("a[hid=]", `<a hidden=""></a>`); 572 test(`a[hid=""]`, `<a hidden=""></a>`); 573 test(`pre{ 574 . 575 }`, `<pre> 576 . 577 </pre>`); 578 test(`a[s="color: red"]`, `<a style="color: red"></a>`); 579 test(`ul 580 . 581 span`, "<ul><li></li></ul><span></span>"); 582 test( 583 `! 584 html 585 . 586 table 587 . 588 th{$@3}*3 589 (#l_$@ 590 td{$@-5}*3)*2`, `<!DOCTYPE html><html><body><table><tr><th>3</th><th>4</th><th>5</th></tr><tr id="l_0"><td>5</td><td>4</td><td>3</td></tr><tr id="l_1"><td>5</td><td>4</td><td>3</td></tr></table></body></html>` 591 ); 592 test("{$foo $bar{$baz}", "$foo $bar{$baz"); 593 test(`{$cond? [} 594 a 595 {]}b`, "$cond? [<a></a>]<b></b>"); 596 test( 597 `{$cond? [} 598 a 599 {] : [} 600 b 601 {]}.`, 602 "$cond? [<a></a>] : [<b></b>]<div></div>"); 603 test( 604 `{$map:key val[} 605 {$key=$value} 606 {]}`, "$map:key val[$key=$value]"); 607 test("{${filename}}b", "${filename}<b></b>"); 608 }