1 /*******************************************************************************
2  * Copyright: © 2014 Economic Modeling Specialists, Intl.
3  * Author: Brian Schott
4  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0)
5  * 
6  * Forked and modified in 2021 for hgen by Eugene 'Vindex' Stulin.
7  * The 'hgen' project: https://gitlab.com/vindexbit/hgen
8  * Author: Eugene 'Vindex' Stulin <tech.vindex@gmail.com>
9  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0)
10  */
11 
12 module ddoc.comments;
13 
14 import std.algorithm : among, each, find, startsWith, endsWith;
15 import std.exception : enforce;
16 import std.string : strip, toLower;
17 
18 import ddoc.sections;
19 import ddoc.lexer;
20 import ddoc.lexer : ParseExc = DdocParseException;
21 import ddoc.highlight : highlight;
22 import ddoc.macros : expand, KeyValuePair, parseKeyValuePair;
23 
24 
25 /*******************************************************************************
26  * Converts D comment test to Comment structure object.
27  * 
28  * Params:
29  *     text = doc string
30  *     macros = associative array storing the names and values ​​of macros
31  *     removeUnknown = whether to remove unknown macros
32  */
33 Comment parseComment(string text,
34                      string[string] macros = null,
35                      bool removeUnknown = true)
36 out(retVal) {
37     assert(retVal.sections.length >= 2);
38 }
39 do {
40     auto sections = splitSections(text);
41     string[string] sMacros = macros.dup;
42     auto m = sections.find!(p => p.name == "Macros");
43     const e = sections.find!(p => p.name == "Escapes");
44     auto p = sections.find!(p => p.name == "Params");
45     if (m.length) {
46         enforce(
47             doMapping(m[0]),
48             new ParseExc("Unable to parse Key/Value pairs", m[0].content)
49         );
50         foreach (kv; m[0].mapping) {
51             sMacros[kv[0]] = kv[1];
52         }
53     }
54     if (e.length) {
55         assert(0, "Escapes not handled yet");
56     }
57     if (p.length) {
58         enforce(
59             doMapping(p[0]), // p[0] - we can have one 'params' section only
60             new ParseExc("Unable to parse Key/Value pairs", p[0].content)
61         );
62         foreach (ref kv; p[0].mapping) {
63             kv[1] = expand(Lexer(highlight(kv[1])), sMacros, removeUnknown);
64         }
65     }
66 
67     foreach (ref Section s; sections) {
68         if (among(s.name, "Macros", "Escapes", "Params")) {
69             continue;
70         }
71         s.content = expand(Lexer(highlight(s.content)), sMacros, removeUnknown);
72     }
73 
74     return Comment(sections);
75 }
76 
77 
78 /*******************************************************************************
79  * Structure containing a set of doc comment sections.
80  */
81 struct Comment {
82     /// Array of comment sections.
83     Section[] sections;
84 
85     /// Whether the comment refer to the previous one by the phrase "ditto".
86     bool isDitto() const @property {
87         if (sections.length != 2) {
88             return false;
89         }
90         return sections[0].content.strip().toLower() == "ditto";
91     }
92 }
93 
94 
95 /// Matches parameter names to their descriptions
96 private bool doMapping(ref Section s) {
97     auto lex = Lexer(s.content);
98     KeyValuePair[] pairs;
99     if (!parseKeyValuePair(lex, pairs)) {
100         return false;
101     }
102     foreach (kv; pairs) {
103         s.mapping ~= kv;
104     }
105     return true;
106 }
107 
108 
109 unittest {
110     Comment test = parseComment("Description\nParams:\n    x = thing\n");
111     assert(test.sections.length == 3);
112     assert(test.sections[0].name == "");
113     assert(test.sections[0].content == "Description");
114     assert(test.sections[2].name == "Params");
115     assert(test.sections[2].content == "x = thing");
116     assert(test.sections[2].mapping[0][0] == "x");
117     assert(test.sections[2].mapping[0][1] == "thing");
118 }
119 
120 
121 unittest {
122     auto macros = ["A" : "<a href=\"$0\">"];
123     auto comment = `Best-comment-ever © 2014
124 
125 I thought the same. I was considering writing it, actually.
126 Imagine how having the $(A tool) would have influenced the "final by
127 default" discussion. Amongst others, of course.
128 
129 It essentially comes down to persistent compiler-as-a-library
130 issue. Tools like dscanner can help with some of more simple
131 transition cases but anything more complicated is likely to
132 require full semantic analysis.
133 Params:
134     a = $(A param)
135 Returns:
136     nothing of consequence
137 `;
138 
139     Comment c = parseComment(comment, macros);
140     import std.string : format;
141 
142     assert(c.sections.length == 4);
143 
144     assert(c.sections[0].name is null);
145     assert(c.sections[0].content == "Best-comment-ever © 2014");
146 
147     assert(c.sections[1].name is null);
148     assert(c.sections[1].content.startsWith("I thought the same"));
149     assert(c.sections[1].content.endsWith("full semantic analysis."));
150 
151     assert(c.sections[2].name == "Params");
152     assert(c.sections[2].mapping[0][0] == "a");
153     assert(c.sections[2].mapping[0][1] == `<a href="param">`);
154 
155     assert(c.sections[3].name == "Returns");
156     assert(c.sections[3].content == "nothing of consequence");
157 }
158 
159 
160 unittest {
161     auto macros = ["A" : "<a href=\"$0\">"];
162     auto text = `Best $(Unknown comment) ever`;
163 
164     Comment c = parseComment(text, macros, true);
165     assert(c.sections.length >= 1);
166     assert(c.sections[0].name is null);
167     assert(c.sections[0].content == "Best  ever");
168 
169     c = parseComment(text, macros, false);
170     assert(c.sections.length >= 1);
171     assert(c.sections[0].name is null);
172     assert(c.sections[0].content == "Best $(Unknown comment) ever");
173 }
174 
175 
176 unittest {
177     auto comment = `---
178 auto subcube(T...)(T values);
179 ---
180 Creates a new cube in a similar way to whereCube, but allows the user to
181 define a new root for specific dimensions.`c;
182     string[string] macros;
183     const Comment c = parseComment(comment, macros);
184 }
185 
186 
187 ///
188 unittest {
189     import std.conv : text;
190 
191     auto s1 = `Stop the world
192 
193 This function tells the Master to stop the world, taking effect immediately.
194 
195 Params:
196 reason = Explanation to give to the $(B Master)
197 duration = Time for which the world $(UNUSED)would be stopped
198            (as time itself stop, this is always $(F double.infinity))
199 
200 ---
201 void main() {
202   import std.datetime : msecs;
203   import master.universe.control;
204   stopTheWorld("Too fast", 42.msecs);
205   assert(0); // Will never be reached.
206 }
207 ---
208 
209 Returns:
210 Nothing, because nobody can restart it.
211 
212 Macros:
213 F= $0`;
214 
215     immutable expectedExamples = `<pre class="d_code">`
216     ~ "<font color=blue>void</font> main() {\n"
217     ~ "  <font color=blue>import</font> std.datetime : msecs;\n"
218     ~ "  <font color=blue>import</font> master.universe.control;\n"
219     ~ "  stopTheWorld(<font color=red>\"Too fast\"</font>, 42.msecs);\n"
220     ~ "  <font color=blue>assert</font>(0); "
221     ~ "<font color=green>// Will never be reached.</font>\n"
222     ~ "}</pre>";
223 
224     auto c = parseComment(s1, null);
225 
226     assert(c.sections.length == 6);
227     assert(c.sections[0].name is null);
228     assert(c.sections[0].content == "Stop the world");
229 
230     assert(c.sections[1].name is null);
231     const fnDescr = `This function tells the Master to stop the world, `
232                   ~ `taking effect immediately.`;
233     assert(c.sections[1].content == fnDescr);
234 
235     auto s2 = c.sections[2];
236 
237     assert(s2.name == "Params");
238     assert(s2.mapping[0][0] == "reason");
239     assert(s2.mapping[0][1] == "Explanation to give to the <b>Master</b>");
240     assert(s2.mapping[1][0] == "duration");
241     const durDescr = "Time for which the world would be stopped\n           "
242                    ~ "(as time itself stop, this is always double.infinity)";
243     assert(s2.mapping[1][1] == durDescr);
244 
245     assert(c.sections[3].name == "Examples");
246     assert(c.sections[3].content == expectedExamples);
247 
248     assert(c.sections[4].name == "Returns");
249     assert(c.sections[4].content == "Nothing, because nobody can restart it.");
250 
251     assert(c.sections[5].name == "Macros");
252     assert(c.sections[5].mapping[0][0] == "F");
253     assert(c.sections[5].mapping[0][1] == "$0");
254 }
255 
256 
257 unittest {
258     import std.stdio : writeln, writefln;
259 
260     auto comment = `Unrolled Linked List.
261 
262 Nodes are (by default) sized to fit within a 64-byte cache line. The number
263 of items stored per node can be read from the $(B nodeCapacity) field.
264 See_also: $(LINK http://en.wikipedia.org/wiki/Unrolled_linked_list)
265 Params:
266     T = the element type
267     supportGC = true to ensure that the GC scans the nodes of the unrolled
268         list, false if you are sure that no references to GC-managed memory
269         will be stored in this container.
270     cacheLineSize = Nodes will be sized to fit within this number of bytes.`;
271 
272     auto parsed = parseComment(comment, null);
273     assert(parsed.sections[3].name == "Params");
274     assert(parsed.sections[3].mapping.length == 3);
275     assert(parsed.sections[3].mapping[0][0] == "T");
276     assert(parsed.sections[3].mapping[0][1] == "the element type");
277     assert(parsed.sections[3].mapping[1][0] == "supportGC");
278     assert(parsed.sections[3].mapping[1][1].startsWith("true to ensure"));
279     assert(parsed.sections[3].mapping[2][0] == "cacheLineSize");
280     assert(parsed.sections[3].mapping[2][1].startsWith("Nodes will be sized"));
281 }