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 }