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("&"); break; 92 case '<': dst.put("<"); break; 93 case '>': dst.put(">"); 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 }