1 /**
2 	D syntax highlighting.
3 
4 	Copyright: © 2015 RejectedSoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module ddox.highlight;
9 
10 import std.algorithm : any;
11 import std.array : Appender, appender, replace;
12 import std.range;
13 import std.string : strip;
14 import std.uni : isLower, isUpper;
15 
16 
17 /**
18 	Takes a piece of D code and outputs a sequence of HTML elements useful for syntax highlighting.
19 
20 	The output will contain $(LT)span$(GT) elements with the class attribute
21 	set to the kind of entity that it contains. The class names are kept
22 	compatible with the ones used for Google's prettify library: "typ", "kwd",
23 	"com", "str", "lit", "pun", "pln", "spc"
24 
25 	The only addition is "spc", which denotes a special token sequence starting
26 	with a "#", such as "#line" or "#!/bin/sh".
27 
28 	Note that this function will only perform actual syntax highlighting if
29 	the libdparse package is available as a DUB dependency.
30 
31 	---
32 	void main(string[] args)
33 	{
34 		#line 2
35 		import std.stdio; // yeah
36 		writefln("Hello, "~"World!");
37 		Package pack;
38 		ddox.entities.Module mod;
39 	}
40 	---
41 
42 	Params:
43 		dst = Output range where to write the HTML output
44 		code = The D source code to process
45 		ident_render = Optional delegate to customize how (qualified)
46 			identifiers are rendered
47 */
48 void highlightDCode(R)(ref R dst, string code, scope IdentifierRenderCallback ident_render = null)
49 	if (isOutputRange!(R, char))
50 {
51 	string last_class;
52 	dst.highlightDCodeImpl(code, ident_render, last_class);
53 	if (last_class.length) dst.put("</span>");
54 }
55 
56 /// ditto
57 string highlightDCode(string str, IdentifierRenderCallback ident_render = null)
58 {
59 	auto dst = appender!string();
60 	dst.highlightDCode(str, ident_render);
61 	return dst.data;
62 }
63 
64 
65 void highlightDCodeImpl(R)(ref R dst, string code, scope IdentifierRenderCallback ident_render, ref string last_class)
66 	if (isOutputRange!(R, char))
67 {
68 	import dparse.lexer : DLexer, LexerConfig, StringBehavior, StringCache, WhitespaceBehavior,
69 		isBasicType, isKeyword, isStringLiteral, isNumberLiteral,
70 		isOperator, str, tok;
71 	import std.algorithm : endsWith;
72 
73 	StringCache cache = StringCache(1024 * 4);
74 
75 	LexerConfig config;
76 	config.stringBehavior = StringBehavior.source;
77 	config.whitespaceBehavior = WhitespaceBehavior.include;
78 
79 	void writeWithClass(string text, string cls)
80 	{
81 		import std.format : formattedWrite;
82 		if (last_class != cls) {
83 			if (last_class.length) dst.put("</span>");
84 			dst.formattedWrite("<span class=\"%s\">", cls);
85 			last_class = cls;
86 		}
87 
88 		foreach (char ch; text) {
89 			switch (ch) {
90 				default: dst.put(ch); break;
91 				case '&': dst.put("&amp;"); break;
92 				case '<': dst.put("&lt;");  break;
93 				case '>': dst.put("&gt;"); break;
94 			}
95 		}
96 	}
97 
98 
99 	auto symbol = appender!string;
100 
101 	foreach (t; DLexer(cast(ubyte[])code, config, &cache)) {
102 		if (ident_render) {
103 			if (t.type == tok!"." && !symbol.data.endsWith(".")) {
104 				symbol ~= ".";
105 				continue;
106 			} else if (t.type == tok!"identifier" && (symbol.data.empty || symbol.data.endsWith("."))) {
107 				symbol ~= t.text;
108 				continue;
109 			} else if (symbol.data.length) {
110 				ident_render(symbol.data, { highlightDCodeImpl(dst, symbol.data, null, last_class); });
111 				symbol = appender!string();
112 			}
113 		}
114 
115 		if (t.type == tok!".") dst.put("<wbr/>");
116 
117 		if (isBasicType(t.type)) writeWithClass(str(t.type), "typ");
118 		else if (isKeyword(t.type)) writeWithClass(str(t.type), "kwd");
119 		else if (t.type == tok!"comment") writeWithClass(t.text, "com");
120 		else if (isStringLiteral(t.type) || t.type == tok!"characterLiteral") writeWithClass(t.text, "str");
121 		else if (isNumberLiteral(t.type)) writeWithClass(t.text, "lit");
122 		else if (isOperator(t.type)) writeWithClass(str(t.type), "pun");
123 		else if (t.type == tok!"specialTokenSequence" || t.type == tok!"scriptLine") writeWithClass(t.text, "spc");
124 		else if (t.text.strip == "string") writeWithClass(t.text, "typ");
125 		else if (t.type == tok!"identifier" && t.text.isCamelCase) writeWithClass(t.text, "typ");
126 		else if (t.type == tok!"identifier") writeWithClass(t.text, "pln");
127 		else if (t.type == tok!"whitespace") writeWithClass(t.text, last_class.length ? last_class : "pln");
128 		else writeWithClass(t.text, "pun");
129 	}
130 
131 	if (symbol.data.length)
132 		ident_render(symbol.data, { highlightDCodeImpl(dst, symbol.data, null, last_class); });
133 }
134 
135 
136 alias IdentifierRenderCallback = void delegate(string ident, scope void delegate() insert_ident);
137 
138 private bool isCamelCase(string text)
139 {
140 	text = text.strip();
141 	if (text.length < 2) return false;
142 	if (!text[0].isUpper) return false;
143 	if (!text.any!(ch => ch.isLower)) return false;
144 	return true;
145 }