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 }