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 }