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