1 2 // Copyright Tim Schendekehl 2023. 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE_1_0.txt or copy at 5 // https://www.boost.org/LICENSE_1_0.txt) 6 7 module dparsergen.generator.doc; 8 import dparsergen.core.utils; 9 import dparsergen.generator.ebnf; 10 import dparsergen.generator.grammarebnf; 11 import std.array; 12 import std.stdio; 13 import std.string; 14 import std.uni; 15 16 enum DocType 17 { 18 html, 19 markdown 20 } 21 22 string nameWithSpace(string name) 23 { 24 string r; 25 foreach (i, dchar c; name) 26 { 27 if (i && c.isUpper && !name[i - 1].isUpper) 28 r ~= " "; 29 r ~= c; 30 } 31 return r; 32 } 33 34 void genDoc(EBNF ebnf, string docfilename, DocType docType) 35 { 36 File f = File(docfilename, "w"); 37 if (docType == DocType.html) 38 { 39 f.writeln("<html>"); 40 f.writeln("<body>"); 41 } 42 Declaration[][string] declarationsByName; 43 foreach (d; ebnf.symbols) 44 declarationsByName[d.name] ~= d; 45 Declaration[][] blocks; 46 auto symbols = ebnf.symbols; 47 string[string] firstNamePerBlock; 48 while (symbols.length) 49 { 50 size_t combinedSymbols = 1; 51 while (combinedSymbols < symbols.length 52 && symbols[combinedSymbols].documentation.strip() == "ditto") 53 combinedSymbols++; 54 blocks ~= symbols[0 .. combinedSymbols]; 55 foreach (i; 0..combinedSymbols) 56 firstNamePerBlock[symbols[i].name] = symbols[0].name; 57 symbols = symbols[combinedSymbols .. $]; 58 } 59 60 string anchorName(string name) 61 { 62 if (name in firstNamePerBlock) 63 name = firstNamePerBlock[name]; 64 return nameWithSpace(name).replace(" ", "-").toLower; 65 } 66 67 string escape(string str) 68 { 69 if (docType == DocType.html) 70 return escapeHTML(str); 71 else 72 { 73 Appender!string app; 74 foreach (char c; str) 75 { 76 if (c == '&') 77 app.put("&"); 78 else if (c == '\"') 79 app.put("""); 80 else if (c == '$') 81 app.put("$"); 82 else if (c == '<') 83 app.put("<"); 84 else if (c == '>') 85 app.put(">"); 86 else if (c == '*') 87 app.put("\\*"); 88 else if (c == '_') 89 app.put("\\_"); 90 else if (c == '~') 91 app.put("\\~"); 92 else if (c == '\\') 93 app.put("\\\\"); 94 else if (c == '#') 95 app.put("\\#"); 96 else 97 app.put(c); 98 } 99 return app.data; 100 } 101 } 102 103 void delegate(Tree expr) writeExpressionDg; 104 void writeComment(string content) 105 { 106 bool inCodeBlock; 107 bool inParagraph; 108 string paragraphEnd; 109 foreach (line; content.splitLines) 110 { 111 string lineStripped = line.strip(); 112 if (docType == DocType.html && inParagraph && lineStripped == "") 113 { 114 f.writeln(paragraphEnd); 115 paragraphEnd = ""; 116 inParagraph = false; 117 } 118 else if (line.strip == "```") 119 { 120 if (inParagraph) 121 { 122 if (docType == DocType.html) 123 f.writeln(paragraphEnd); 124 paragraphEnd = ""; 125 inParagraph = false; 126 } 127 inCodeBlock = !inCodeBlock; 128 if (inCodeBlock) 129 f.writeln("<pre>"); 130 else 131 f.writeln("</pre>"); 132 } 133 else if (!inCodeBlock && lineStripped.startsWith("$TRANSITIVE_UNWRAP_TABLE(") 134 && lineStripped.endsWith(")")) 135 { 136 if (inParagraph && docType == DocType.html) 137 { 138 f.writeln(paragraphEnd); 139 paragraphEnd = ""; 140 inParagraph = false; 141 } 142 143 f.writeln("<table>"); 144 bool[string] done; 145 void onDeclaration(string name) 146 { 147 if (name in done) 148 return; 149 done[name] = true; 150 151 foreach (ref d; declarationsByName.get(name, [])) 152 { 153 Tree[] alternatives = [d.exprTree]; 154 while (alternatives[0].nonterminalID == nonterminalIDFor!"Alternation") 155 alternatives = [ 156 alternatives[0].childs[0], 157 alternatives[0].childs[2] 158 ] ~ alternatives[1 .. $]; 159 Tree[] realExprs; 160 string[] unwrapNames; 161 foreach (expr; alternatives) 162 { 163 bool unwrapProduction = false; 164 while (true) 165 { 166 if (expr.nonterminalID == nonterminalIDFor!"AnnotatedExpression") 167 { 168 foreach (c; expr.childs[2].memberOrDefault!"childs") 169 { 170 if (c.childs[0].content == "<") 171 unwrapProduction = true; 172 } 173 expr = expr.childs[$ - 1]; 174 } 175 else if (expr.nonterminalID == nonterminalIDFor!"Concatenation" 176 && expr.childs.length == 2) 177 expr = expr.childs[0]; 178 else 179 break; 180 } 181 if (expr.nonterminalID == nonterminalIDFor!"Name" && unwrapProduction) 182 { 183 unwrapNames ~= expr.childs[0].content; 184 } 185 else 186 realExprs ~= expr; 187 } 188 foreach (expr; realExprs) 189 { 190 f.writeln("<tr>"); 191 f.writeln("<td><a href=\"#", anchorName(d.name).escapeHTML, 192 "\">", escape(d.name), "</a></td>"); 193 f.writeln("<td>"); 194 Tree[] parts; 195 if (expr.nonterminalID == nonterminalIDFor!"Concatenation") 196 { 197 if (expr.childs.length == 3) 198 parts = expr.childs[0] 199 ~ expr.childs[1].memberOrDefault!"childs"; 200 if (expr.childs.length == 2) 201 parts = [expr.childs[0]]; 202 } 203 else 204 parts = [expr]; 205 foreach (part; parts) 206 { 207 f.write(" "); 208 writeExpressionDg(part); 209 } 210 f.writeln("</td>"); 211 f.writeln("</tr>"); 212 } 213 foreach (unwrapName; unwrapNames) 214 onDeclaration(unwrapName); 215 } 216 } 217 218 foreach (name; lineStripped[25 .. $ - 1].split(", ")) 219 onDeclaration(name.strip); 220 f.writeln("</table>"); 221 } 222 else if (inCodeBlock) 223 f.writeln(escape(line)); 224 else 225 { 226 if (!inParagraph) 227 { 228 inParagraph = true; 229 if (docType == DocType.html) 230 { 231 if (lineStripped.startsWith("*")) 232 { 233 f.writeln("<ul><li>"); 234 paragraphEnd = "</li></ul>"; 235 line = line.stripLeft[1 .. $].stripLeft; 236 } 237 else 238 { 239 f.writeln("<p>"); 240 paragraphEnd = "</p>"; 241 } 242 } 243 } 244 else 245 { 246 if (docType == DocType.html) 247 { 248 if (lineStripped.startsWith("*")) 249 { 250 if (paragraphEnd != "</li></ul>") 251 { 252 f.writeln(paragraphEnd); 253 f.writeln("<ul><li>"); 254 paragraphEnd = "</li></ul>"; 255 } 256 else 257 { 258 f.writeln("</li><li>"); 259 } 260 line = line.stripLeft[1 .. $].stripLeft; 261 } 262 } 263 } 264 while (line.length) 265 { 266 if (line[0] == '`') 267 { 268 size_t i = 1; 269 while (i < line.length && line[i] != '`') 270 i++; 271 if (i < line.length) 272 { 273 f.write("<a href=\"#", anchorName(line[1 .. i]).escapeHTML, 274 "\">", escape(line[1 .. i]), "</a>"); 275 line = line[i + 1 .. $]; 276 } 277 } 278 f.write(line[0]); 279 line = line[1 .. $]; 280 } 281 f.writeln(); 282 } 283 } 284 if (inCodeBlock) 285 f.writeln("</pre>"); 286 if (inParagraph && docType == DocType.html) 287 f.writeln(paragraphEnd); 288 f.writeln(); 289 } 290 291 void writeExpression(Tree expr) 292 { 293 Tree realExpr = expr; 294 while (realExpr.nonterminalID == nonterminalIDFor!"AnnotatedExpression") 295 { 296 realExpr = realExpr.childs[$ - 1]; 297 } 298 if (realExpr.nonterminalID == nonterminalIDFor!"Name") 299 { 300 f.write("<a href=\"#", anchorName(realExpr.childs[0].content).escapeHTML, 301 "\">", escape(realExpr.childs[0].content), "</a>"); 302 } 303 else if (realExpr.nonterminalID == nonterminalIDFor!"Token") 304 { 305 f.write("<b>", escape(realExpr.childs[0].content), "</b>"); 306 } 307 else if (realExpr.nonterminalID == nonterminalIDFor!"Optional") 308 { 309 writeExpression(realExpr.childs[0]); 310 f.write("?"); 311 } 312 else if (realExpr.nonterminalID == nonterminalIDFor!"Repetition") 313 { 314 writeExpression(realExpr.childs[0]); 315 f.write("*"); 316 } 317 else if (realExpr.nonterminalID == nonterminalIDFor!"RepetitionPlus") 318 { 319 writeExpression(realExpr.childs[0]); 320 f.write("+"); 321 } 322 else 323 { 324 f.write(ebnfTreeToString(realExpr).escapeHTML); 325 } 326 } 327 328 writeExpressionDg = &writeExpression; 329 330 writeComment(ebnf.globalDocumentation); 331 332 foreach (symbolsHere; blocks) 333 { 334 f.write(docType == DocType.html ? "<h3>" : "### ", nameWithSpace(symbolsHere[0].name).escapeHTML); 335 336 if (docType == DocType.html) 337 { 338 foreach (s; symbolsHere) 339 f.write("<a id=\"", anchorName(s.name).escapeHTML, "\">", "</a>"); 340 } 341 342 if (docType == DocType.html) 343 f.writeln("</h3>"); 344 else 345 f.writeln(); 346 f.writeln("<pre>"); 347 348 foreach (i, s; symbolsHere) 349 { 350 /*if (i) 351 f.writeln();*/ 352 if (s.type == DeclarationType.token) 353 f.write("token "); 354 else if (s.type == DeclarationType.fragment) 355 f.write("fragment "); 356 357 f.writeln(s.name); 358 359 Tree[] alternatives = [s.exprTree]; 360 while (alternatives[0].nonterminalID == nonterminalIDFor!"Alternation") 361 alternatives = [ 362 alternatives[0].childs[0], alternatives[0].childs[2] 363 ] ~ alternatives[1 .. $]; 364 365 foreach (j, alternative; alternatives) 366 { 367 f.write(" ", j ? "|" : "="); 368 369 Tree[] parts; 370 if (alternative.nonterminalID == nonterminalIDFor!"Concatenation") 371 { 372 if (alternative.childs.length == 3) 373 parts = alternative.childs[0] 374 ~ alternative.childs[1].memberOrDefault!"childs"; 375 if (alternative.childs.length == 2) 376 parts = [alternative.childs[0]]; 377 } 378 else 379 parts = [alternative]; 380 foreach (part; parts) 381 { 382 f.write(" "); 383 writeExpression(part); 384 } 385 if (parts.length == 0) 386 f.write(" <i>empty</i>"); 387 f.writeln(); 388 } 389 f.writeln(" ;"); 390 } 391 392 f.writeln("</pre>"); 393 394 writeComment(symbolsHere[0].documentation); 395 } 396 if (docType == DocType.html) 397 { 398 f.writeln("</body>"); 399 f.writeln("</html>"); 400 } 401 }