1 /**
2  * Copyright: © 2014 Economic Modeling Specialists, Intl.
3  * Authors: 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.sections;
13 
14 import std.algorithm.searching : canFind, endsWith, find;
15 import std.array : appender;
16 import std.range : dropBack, enumerate, retro;
17 import std.string : strip;
18 
19 import ddoc.lexer;
20 import ddoc.macros;
21 import std.typecons;
22 
23 
24 /*******************************************************************************
25  * Standard section names.
26  */
27 immutable string[] STANDARD_SECTIONS = [
28     "Authors", "Bugs", "Copyright", "Date",
29     "Deprecated", "Examples", "History", "License", "Returns", "See_Also",
30     "Standards", "Throws", "Version"
31 ];
32 
33 
34 /*******************************************************************************
35  * Data structure containing the comment section name,
36  * comment section contents and pair mapping for 'Params', 'Macros', 'Escapes'.
37  */
38 struct Section {
39     /// The section name.
40     string name;
41 
42     /// The section content.
43     string content;
44 
45     /***************************************************************************
46      * Mapping used by the Params, Macros, and Escapes section types.
47      *
48      * $(UL
49      * $(LI "Params": key = parameter name, value = parameter description)
50      * $(LI "Macros": key = macro name, value = macro implementation)
51      * $(LI "Escapes": key = character to escape, value = replacement string)
52      * )
53      */
54     KeyValuePair[] mapping;
55 
56     /***************************************************************************
57      * Returns: true if $(B name) is one of $(B STANDARD_SECTIONS).
58      */
59     bool isStandard() const @property {
60         return STANDARD_SECTIONS.canFind(name);
61     }
62 
63     ///
64     unittest {
65         Section s;
66         s.name = "Authors";
67         assert(s.isStandard);
68         s.name = "Butterflies";
69         assert(!s.isStandard);
70     }
71 }
72 
73 
74 /*******************************************************************************
75  * Parses a Macros or Params section, filling in the mapping fields of the
76  * returned section.
77  */
78 Section parseMacrosOrParams(string name,
79                             ref Lexer lexer,
80                             ref string[string] macros) {
81     Section s;
82     s.name = name;
83     while (!lexer.empty && lexer.front.type != Type.header) {
84         if (!parseKeyValuePair(lexer, s.mapping)) {
85             break;
86         }
87         if (name == "Macros") {
88             foreach (kv; s.mapping) {
89                 macros[kv[0]] = kv[1];
90             }
91         }
92     }
93     return s;
94 }
95 
96 
97 /*******************************************************************************
98  * Split a text into sections.
99  *
100  * Takes a text, which is generally a full comment (usually you'll also call
101  * $(D unDecorateComment) before). It splits it in an array of $(D Section)
102  * and returns it.
103  * Whatever the content of $(D text) is, this function will always return an
104  * array of at least 2 items. Those 2 sections are the "Summary" and
105  * "Description" sections (which may be empty).
106  *
107  * Params:
108  * text = A DDOC-formatted comment.
109  *
110  * Returns:
111  * An array of $(D Section) with at least 2 elements.
112  */
113 Section[] splitSections(string text) {
114     // Note: The specs says those sections are unnamed. So some people could
115     // name one of it's section 'Summary' or 'Description', and it would be
116     // legal (but arguably wrong).
117     auto lex = Lexer(text);
118     auto app = appender!(Section[]);
119     bool hasSummary;
120     // Used to strip trailing newlines / whitespace.
121     lex.stripWhitespace();
122     size_t sliceStart = lex.offset - lex.front.text.length;
123     if (lex.front.type == Type.inlined) {
124         sliceStart -= 2; // opening and closing '`' characters
125     }
126     size_t sliceEnd = sliceStart;
127     string name;
128     app ~= Section();
129     app ~= Section();
130 
131     void finishSection() {
132         auto text = lex.text[sliceStart .. sliceEnd];
133         // remove the last line from the current section except for the last section
134         // (the last section doesn't have a following section)
135         auto endDashes = text.endsWith("---");
136         if (text.canFind("\n") && sliceEnd != lex.text.length && !endDashes) {
137             text = text.dropBack(
138                 text.retro.enumerate.find!(e => e.value == '\n').front.index
139             );
140         }
141 
142         if (!hasSummary) {
143             hasSummary = true;
144             app.data[0].content = text;
145         } else if (name is null) {
146             //immutable bool hadContent = app.data[1].content.length > 0;
147             app.data[1].content ~= text;
148         } else {
149             appendSection(name, text, app);
150         }
151         sliceStart = sliceEnd = lex.offset;
152     }
153 
154     while (!lex.empty) switch (lex.front.type) {
155         case Type.header:
156             finishSection();
157             name = lex.front.text;
158             lex.popFront();
159             lex.stripWhitespace();
160             break;
161         case Type.newline:
162             lex.popFront();
163             if (name is null && !lex.empty && lex.front.type == Type.newline)
164                 finishSection();
165             break;
166         case Type.embedded:
167             finishSection();
168             name = "Examples";
169             appendSection("Examples", "---\n" ~ lex.front.text ~ "\n---", app);
170             lex.popFront();
171             sliceStart = sliceEnd = lex.offset;
172             break;
173         default:
174             lex.popFront();
175             sliceEnd = lex.offset;
176             break;
177     }
178     finishSection();
179     foreach (ref section; app.data) {
180         section.content = section.content.strip();
181     }
182     return app.data;
183 }
184 
185 
186 unittest {
187     import std.conv : text;
188     import std.algorithm.iteration : map;
189     import std.algorithm.comparison : equal;
190 
191     auto s = `description
192 
193 Something else
194 
195 ---
196 // an example
197 ---
198 Throws: a fit
199 ---
200 /// another example
201 ---
202 `;
203     const sections = splitSections(s);
204     immutable expectedExample = `---
205 // an example
206 ---
207 ---
208 /// another example
209 ---`;
210     assert(sections.length == 4);
211     assert(sections.map!(a => a.name).equal(["", "", "Examples", "Throws"]));
212     assert(sections[0].content == "description");
213     assert(sections[1].content == "Something else");
214     assert(sections[2].content == expectedExample);
215     assert(sections[3].content == "a fit");
216 }
217 
218 unittest {
219     import std.conv : text;
220 
221     auto s1 = `Short comment.
222 Still comment.
223 
224 Description.
225 Still desc...
226 
227 Still
228 
229 Authors:
230 Me & he
231 Bugs:
232 None
233 Copyright:
234 Date:
235 
236 Deprecated:
237 Nope,
238 
239 ------
240 void foo() {}
241 ----
242 
243 History:
244 License:
245 Returns:
246 See_Also
247 See_Also:
248 Standards:
249 
250 Throws:
251 Version:
252 
253 
254 `;
255     auto cnt = [
256         "Short comment.\nStill comment.",
257         "Description.\nStill desc...\nStill",
258         "Me & he",
259         "None",
260         "",
261         "",
262         "Nope,",
263         "---\nvoid foo() {}\n---",
264         "",
265         "",
266         "See_Also",
267         "",
268         "",
269         "",
270         ""
271     ];
272     foreach (idx, sec; splitSections(s1)) {
273         if (idx < 2) {
274             // Summary & description
275             assert(sec.name is null, sec.name);
276         } else {
277             assert(sec.name == STANDARD_SECTIONS[idx - 2], sec.name);
278         }
279         assert(
280             sec.content == cnt[idx],
281             text(sec.name, " (", idx, "): ",
282             sec.content)
283         );
284     }
285 }
286 
287 
288 unittest {
289     immutable comment = `summary
290 
291 ---
292 some code!!!
293 ---`;
294     const sections = splitSections(comment);
295     assert(sections[0].content == "summary");
296     assert(sections[1].content == "");
297     assert(sections[2].content == "---\nsome code!!!\n---");
298 }
299 
300 // Split section content correctly (without next line)
301 unittest {
302     immutable comment = `Params:
303     pattern(s) = Regular expression(s) to match
304     flags = The _attributes (g, i, m and x accepted)
305 
306     Throws: $(D RegexException) if there were any errors during compilation.`;
307 
308     const sections = splitSections(comment);
309     const exp = "pattern(s) = Regular expression(s) to match\n"
310               ~ "    flags = The _attributes (g, i, m and x accepted)";
311     assert(sections[2].content == exp);
312 }
313 
314 
315 // Handle inlined code properly
316 unittest {
317     immutable comment = "`code` something";
318     const sections = splitSections(comment);
319     assert(sections[0].content == "`code` something");
320 }
321 
322 
323 /*******************************************************************************
324  * Append a section to the given output or merge it if a section with
325  * the same name already exists.
326  *
327  * Returns:
328  * $(D true) if the section did not already exists,
329  * $(D false) if the content was merged with an existing section.
330  */
331 private bool appendSection(O)(string name, string content, ref O output)
332 in {
333     assert(name !is null, "You should not call appendSection with a null name");
334 }
335 do {
336     for (size_t i = 2; i < output.data.length; ++i) {
337         if (output.data[i].name != name) {
338             continue;
339         }
340         if (output.data[i].content.length == 0) {
341             output.data[i].content = content;
342         } else if (content.length > 0) {
343             output.data[i].content ~= "\n" ~ content;
344         }
345         return false;
346     }
347     output ~= Section(name, content);
348     return true;
349 }