1 /**
2  * Perform highlighting on code section.
3  *
4  * DDOC string can contains embedded code. Those code can be highlighted by
5  * means of macros (keywork will be surrounded by $(DOLLAR)(D_KEYWORD),
6  * comments by $(DOLLAR)(D_COMMENT), etc...
7  * This module performs the highlighting.
8  *
9  * Copyright: © 2014 Economic Modeling Specialists, Intl.
10  * Authors: Brian Schott, Mathias 'Geod24' Lang
11  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
12  */
13 module ddoc.highlight;
14 
15 /**
16  * Parses a string and replace embedded code (code between at least 3 '-') with
17  * the relevant macros.
18  *
19  * Params:
20  * str = A string that might contain embedded code. Only code will be modified.
21  *     If the string doesn't contain any embedded code, it will be returned as is.
22  *
23  * Returns:
24  * A (possibly new) string containing the embedded code put in the proper macros.
25  */
26 string highlight(string str)
27 {
28     // Note: I don't think DMD is conformant w.r.t ddoc.
29     // The following file:
30     // Ddoc
31     // ----
32     // int main(string[] args) { return 0;}
33     // void test(int hello, string other);
34     // ----
35     //
36     // Produce the following document ($(DDOC) boilerplate excluded:
37     //
38     // <pre class="d_code"><font color=blue>int</font> main(string[] args) { <font color=blue>return</font> 0;}
39     // <font color=blue>void</font> test(<font color=blue>int</font> hello, string other);
40     // </pre>
41 
42     import ddoc.lexer;
43     import ddoc.macros : tokOffset;
44     import std.array : appender;
45 
46     auto lex = Lexer(str, true);
47     auto output = appender!string;
48     size_t start;
49     // We need this because there's no way to tell how many dashes precede
50     // an embedded.
51     size_t end;
52     while (!lex.empty)
53     {
54         if (lex.front.type == Type.embedded)
55         {
56             if (start != end)
57                 output.put(lex.text[start .. end]);
58             output.put("$(D_CODE ");
59             highlightCode(lex.front.text, output);
60             output.put(")");
61             start = lex.offset;
62         }
63         else if (lex.front.type == Type.inlined)
64         {
65             if (start != end)
66                 output.put(lex.text[start .. end]);
67             output.put("$(DDOC_BACKQUOTED ");
68             highlightCode(lex.front.text, output);
69             output.put(")");
70             start = lex.offset;
71         }
72         end = lex.offset;
73         lex.popFront();
74     }
75     if (start)
76         output.put(lex.text[start .. end]);
77     return start ? output.data : str;
78 }
79 
80 ///
81 unittest
82 {
83     import ddoc.lexer;
84 
85     auto s1 = `Here is some embedded D code I'd like to show you:
86 $(MY_D_CODE
87 ------
88 // Entry point...
89 void main() {
90   import std.stdio : writeln;
91   writeln("Hello,", " ", "world", "!");
92 }
93 ------
94 )
95 Isn't it pretty ?`;
96     // Embedded code is surrounded by D_CODE macro, and tokens have their own
97     // macros (see: D_KEYWORD for example).
98     auto r1 = highlight(s1);
99     auto e1 = `Here is some embedded D code I'd like to show you:
100 $(MY_D_CODE
101 $(D_CODE $(D_COMMENT // Entry point...)
102 $(D_KEYWORD void) main() {
103   $(D_KEYWORD import) std.stdio : writeln;
104   writeln($(D_STRING "Hello,"), $(D_STRING " "), $(D_STRING "world"), $(D_STRING "!"));
105 })
106 )
107 Isn't it pretty ?`;
108     assert(r1 == e1, r1);
109 
110     // No allocation is performed if the string doesn't contain inline code.
111     auto s2 = `This is some simple string
112 --
113 It doesn't do much
114 --
115 Hope you won't allocate`;
116     auto r2 = highlight(s2);
117     assert(r2 is s2, r2);
118 }
119 
120 // Test multiple embedded code.
121 unittest
122 {
123     auto s1 = `----
124 void main() {}
125 ----
126 ----
127 int a = 42;
128 ----
129 ---
130 unittest {
131     assert(42, "Life, universe, stuff");
132 }
133 ---`;
134     auto e1 = `$(D_CODE $(D_KEYWORD void) main() {})
135 $(D_CODE $(D_KEYWORD int) a = 42;)
136 $(D_CODE $(D_KEYWORD unittest) {
137     $(D_KEYWORD assert)(42, $(D_STRING "Life, universe, stuff"));
138 })`;
139     auto r1 = highlight(s1);
140     assert(r1 == e1, r1);
141 }
142 
143 unittest
144 {
145     auto s = `
146         ---------
147         asm pure nothrow @nogc @trusted
148         {
149             // the compiler does not check the attributes
150             ret;
151         }
152         ---------
153 `;
154     auto e = `
155         $(D_CODE $(D_KEYWORD asm) $(D_KEYWORD pure) $(D_KEYWORD nothrow) @nogc @trusted
156 {
157     $(D_COMMENT // the compiler does not check the attributes)
158     ret;
159 }
160 )
161 `;
162     auto r = highlight(s);
163     assert(r == e, r);
164 }
165 
166 private:
167 void highlightCode(O)(string code, ref O output)
168 {
169     import dparse.lexer;
170     import std.string : representation;
171 
172     enum fName = "<embedded-code-in-documentation>";
173 
174     auto cache = StringCache(StringCache.defaultBucketCount);
175     auto toks = code.representation.dup.byToken(LexerConfig(fName,
176         StringBehavior.source, WhitespaceBehavior.include), &cache);
177     while (!toks.empty)
178     {
179         if (toks.front.type.isStringLiteral)
180         {
181             output.put("$(D_STRING ");
182             output.put(toks.front.text);
183             output.put(")");
184         }
185         else if (toks.front == tok!"comment")
186         {
187             output.put("$(D_COMMENT ");
188             output.put(toks.front.text);
189             output.put(")");
190         }
191         else if (toks.front.type.isKeyword || toks.front.type.isBasicType)
192         {
193             output.put("$(D_KEYWORD ");
194             output.put(toks.front.type.str);
195             output.put(")");
196         }
197         else if (toks.front.text.length)
198         {
199             output.put(toks.front.text);
200         }
201         else
202         {
203             output.put(toks.front.type.str);
204         }
205         toks.popFront();
206     }
207 }