1 module godot.tools.generator.doc;
2 
3 import godot.tools.generator.classes;
4 import godot.tools.generator.methods;
5 import godot.tools.generator.util;
6 import godot.util.string;
7 
8 import std.stdio : writeln, writefln;
9 import std.string, std.range;
10 import std.algorithm.searching;
11 import std.exception : enforce;
12 
13 import dxml.dom;
14 import dxml.util : stripIndent;
15 
16 void parseClassDoc(GodotClass c, string xml) {
17     string ddoc;
18     auto dom = parseDOM!simpleXML(xml);
19 
20     enforce(dom.children.length == 1 && dom.children[0].name == "class",
21         "Expected a single <class name=\"" ~ c.name.dType ~ "\"> in the doc XML");
22     auto cDoc = dom.children[0];
23 
24     if (cDoc.hasChild("brief_description")) {
25         auto bd = cDoc.child("brief_description");
26         if (bd.childText) {
27             ddoc ~= bd.childText;
28             c.ddocBrief = bd.childText;
29         }
30     }
31 
32     if (cDoc.hasChild("description")) {
33         auto fd = cDoc.child("description");
34         if (fd.childText) {
35             string t = fd.childText;
36             // in case it starts with brief_description; only possible to detect if exactly the same, unfortunately
37             t = t.chompPrefix(c.ddocBrief).strip;
38             if (t) {
39                 if (ddoc)
40                     ddoc ~= "\n\n";
41                 ddoc ~= t;
42             }
43         }
44     }
45 
46     /// TODO: remove any */ from inside comment and change BBCode-style stuff to ddoc macros
47     c.ddocBrief = c.ddocBrief.godotToDdoc;
48     c.ddoc = ddoc.godotToDdoc;
49 
50     if (cDoc.hasChild("methods")) {
51         auto methods = cDoc.child("methods");
52         foreach (mDoc; methods.children) {
53             auto index = c.methods.countUntil!"a.name == b"(mDoc.attribute("name"));
54             if (index != -1) {
55                 parseMethodDoc(c.methods[index], mDoc);
56             } else
57                 writefln("No method %s.%s", c.name.godotType, mDoc.attribute("name"));
58         }
59     }
60 
61     if (cDoc.hasChild("members")) {
62         auto members = cDoc.child("members");
63         foreach (mDoc; members.children) {
64             auto index = c.properties.countUntil!"a.name == b"(mDoc.attribute("name"));
65             if (index != -1) {
66                 parsePropertyDoc(c.properties[index], mDoc);
67             } else
68                 writefln("No property %s.%s", c.name.godotType, mDoc.attribute("name"));
69         }
70     }
71 
72     if (cDoc.hasChild("constants")) {
73         auto constants = cDoc.child("constants");
74         foreach (ceDoc; constants.children) {
75             /*
76 			auto cPtr = ceDoc.attribute("name") in c.constants;
77 			if(cPtr) c.ddocConstants[ceDoc.attribute("name")] = ceDoc.childText.godotToDdoc;
78 			
79 			if(!ceDoc.attribute("enum").empty)
80 			{
81 				auto index = c.enums.countUntil!"a.name == b"(ceDoc.attribute("enum"));
82 				if(index != -1) c.enums[index].ddoc[ceDoc.attribute("name")] = ceDoc.childText.godotToDdoc;
83 			}
84 			*/
85         }
86     }
87 
88     /// TODO: enums
89 }
90 
91 void parseMethodDoc(GodotMethod m, DOMEntity!string mDoc) {
92     if (mDoc.hasChild("description")) {
93         auto fd = mDoc.child("description");
94         if (fd.childText)
95             m.ddoc = fd.childText.godotToDdoc;
96     }
97 }
98 
99 void parsePropertyDoc(GodotProperty p, DOMEntity!string pDoc) {
100     p.ddoc = pDoc.childText.godotToDdoc;
101 }
102 
103 // ddoc util functions
104 
105 string godotToDdoc(string input) {
106     import std.algorithm, std.string, std.regex;
107 
108     string ret = input;
109 
110     // handle [tags] like Godot's doc/tools/makerst.py
111 
112     ret = ret.replaceAll!((Captures!string s) {
113         auto split = s[1].findSplit(".");
114         if (split[1].empty)
115             return "$(D " ~ s[1].snakeToCamel.escapeDType ~ ")";
116         else
117             return "$(D " ~ split[0].escapeGodotType ~ "." ~ split[2].snakeToCamel.escapeDType ~ ")";
118     })(ctRegex!(`\[(?:method|member|signal|enum) (.+?)\]`));
119 
120     /// TODO: not D code, can't use --- blocks. Maybe the GDScript can be parsed and converted to D...
121     ret = ret.replaceAll!((Captures!string s) => "\n" ~ s[1] ~ "\n")(
122         ctRegex!(`\[codeblock\]([\s\S]*?)\[/codeblock\]`));
123 
124     ret = ret.replaceAll(ctRegex!`\[br\][ \t]*`, "$(BR)");
125 
126     ret = ret.replaceAll!((Captures!string s) => "$(" ~ s[1].toUpper ~ " " ~ s[2] ~ ")")(
127         ctRegex!`\[([bui])\](.*?)\[/\1\]`);
128 
129     ret = ret.replaceAll!((Captures!string s) => "`" ~ s[1] ~ "`")(
130         ctRegex!`\[code\](.*?)\[/code\]`);
131 
132     // fallback for [Class]
133     ret = ret.replaceAll!((Captures!string s) => "$(D " ~ s[1].escapeGodotType ~ ")")(
134         ctRegex!`\[(.+?)\]`);
135 
136     return ret;
137 }
138 
139 // dxml util functions
140 
141 string attribute(DOMEntity!string e, string name) {
142     auto index = e.attributes.countUntil!"a.name == b"(name);
143     if (index == -1)
144         return null;
145     return e.attributes[index].value;
146 }
147 
148 bool hasChild(DOMEntity!string e, string name) {
149     return e.children.countUntil!"a.name == b"(name) != -1;
150 }
151 
152 DOMEntity!string child(DOMEntity!string e, string name) {
153     auto index = e.children.countUntil!"a.name == b"(name);
154     assert(index != -1);
155     return e.children[index];
156 }
157 
158 string childText(DOMEntity!string e) {
159     auto index = e.children.countUntil!"a.type == b"(EntityType.text);
160     if (index == -1)
161         return null;
162     return e.children[index].text.stripIndent;
163 }