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("&amp;");
78                 else if (c == '\"')
79                     app.put("&quot;");
80                 else if (c == '$')
81                     app.put("&#36;");
82                 else if (c == '<')
83                     app.put("&lt;");
84                 else if (c == '>')
85                     app.put("&gt;");
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 }