* Mono.Documentation/monodocs2html.cs: Regenerate all index.{opts.ext}
[mono.git] / mcs / tools / mdoc / Mono.Documentation / monodocs2html.cs
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.IO;
6 using System.Linq;
7 using System.Reflection;
8 using System.Text;
9 using System.Xml;
10 using System.Xml.Xsl;
11 using System.Xml.XPath;
12
13 using Mono.Documentation;
14 using Mono.Options;
15
16 [assembly: AssemblyTitle("Monodocs-to-HTML")]
17 [assembly: AssemblyCopyright("Copyright (c) 2004 Joshua Tauberer <tauberer@for.net>, released under the GPL.")]
18 [assembly: AssemblyDescription("Convert Monodoc XML documentation to static HTML.")]
19
20 namespace Mono.Documentation {
21
22 class MDocToHtmlConverterOptions {
23         public string dest;
24         public string ext = "html";
25         public string onlytype;
26         public string template;
27         public bool   dumptemplate;
28         public bool   forceUpdate;
29         public HashSet<string> versions = new HashSet<string> ();
30 }
31
32 class MDocToHtmlConverter : MDocCommand {
33
34         static Dictionary<string, string[]> profiles = new Dictionary<string, string[]>() {
35                 //                      FxVersions-----                   VsVersions-----
36                 { "monotouch",    new[]{"0.0.0.0", "2.0.5.0"              } },
37                 { "net_1_0",      new[]{"1.0.3300.0",                     "7.0.3300.0"} },
38                 { "net_1_1",      new[]{"1.0.5000.0",                     "7.0.5000.0"} },
39                 { "net_2_0",      new[]{"2.0.0.0",                        "8.0.0.0"} },
40                 { "net_3_0",      new[]{"2.0.0.0", "3.0.0.0",             "8.0.0.0"} },
41                 { "net_3_5",      new[]{"2.0.0.0", "3.0.0.0", "3.5.0.0",  "8.0.0.0"} },
42                 { "net_4_0",      new[]{"4.0.0.0"                         } },
43                 { "silverlight",  new[]{"2.0.5.0",                        "9.0.0.0"} },
44         };
45
46         public override void Run (IEnumerable<string> args)
47         {
48                 opts = new MDocToHtmlConverterOptions ();
49                 var p = new OptionSet () {
50                         { "default-template",
51                                 "Writes the default XSLT to stdout.",
52                                 v => opts.dumptemplate = v != null },
53                         { "ext=",
54                                 "The file {EXTENSION} to use for created files.  "+
55                                         "This defaults to \"html\".",
56                                 v => opts.ext = v },
57                         { "force-update",
58                                 "Always generate new files.  If not specified, will only generate a " + 
59                                         "new file if the source .xml file is newer than the current output " +
60                                         "file.",
61                                 v => opts.forceUpdate = v != null },
62                         { "o|out=",
63                                 "The {DIRECTORY} to place the generated files and directories.",
64                                 v => opts.dest = v },
65                         { "template=",
66                                 "An XSLT {FILE} to use to generate the created " + 
67                                         "files.If not specified, uses the template generated by " + 
68                                         "--default-template.",
69                                 v => opts.template = v },
70                         { "with-profile=",
71                                 "The .NET {PROFILE} to generate documentation for.  This is " + 
72                                         "equivalent to using --with-version for all of the " +
73                                         "versions that a profile uses.  Valid profiles are:\n  " +
74                                         string.Join ("\n  ", profiles.Keys.OrderBy (v => v).ToArray ()),
75                                 v => {
76                                         if (!profiles.ContainsKey (v))
77                                                 throw new ArgumentException (string.Format ("Unsupported profile '{0}'.", v));
78                                         foreach (var ver in profiles [v.ToLowerInvariant ()])
79                                                 opts.versions.Add (ver);
80                                 } },
81                         { "with-version=",
82                                 "The assembly {VERSION} to generate documentation for.  This allows " + 
83                                         "display of a subset of types/members that correspond to the given " +
84                                         "assembly version.  May be specified multiple times.  " + 
85                                         "If not specified, all versions are displayed.",
86                                 v => opts.versions.Add (v) }
87                 };
88                 List<string> extra = Parse (p, args, "export-html", 
89                                 "[OPTIONS]+ DIRECTORIES",
90                                 "Export mdoc documentation within DIRECTORIES to HTML.");
91                 if (extra == null)
92                         return;
93                 if (opts.dumptemplate)
94                         DumpTemplate ();
95                 else
96                         ProcessDirectories (extra);
97                 opts.onlytype = "ignore"; // remove warning about unused member
98         }
99
100         static MDocToHtmlConverterOptions opts;
101
102         void ProcessDirectories (List<string> sourceDirectories)
103         {
104                 if (sourceDirectories.Count == 0 || opts.dest == null || opts.dest == "")
105                         throw new ApplicationException("The source and dest options must be specified.");
106                 
107                 Directory.CreateDirectory(opts.dest);
108
109                 // Load the stylesheets, overview.xml, and resolver
110                 
111                 XslCompiledTransform overviewxsl = LoadTransform("overview.xsl", sourceDirectories);
112                 XslCompiledTransform stylesheet = LoadTransform("stylesheet.xsl", sourceDirectories);
113                 XslCompiledTransform template;
114                 if (opts.template == null) {
115                         template = LoadTransform("defaulttemplate.xsl", sourceDirectories);
116                 } else {
117                         try {
118                                 XmlDocument templatexsl = new XmlDocument();
119                                 templatexsl.Load(opts.template);
120                                 template = new XslCompiledTransform (DebugOutput);
121                                 template.Load(templatexsl);
122                         } catch (Exception e) {
123                                 throw new ApplicationException("There was an error loading " + opts.template, e);
124                         }
125                 }
126                 
127                 XmlDocument overview = GetOverview (sourceDirectories);
128
129                 ArrayList extensions = GetExtensionMethods (overview);
130                 
131                 // Create the master page
132                 XsltArgumentList overviewargs = new XsltArgumentList();
133                 overviewargs.AddParam("Index", "", overview.CreateNavigator ());
134
135                 var regenIndex = ShouldRegenIndexes (opts, overview, sourceDirectories);
136                 if (regenIndex) {
137                         overviewargs.AddParam("ext", "", opts.ext);
138                         overviewargs.AddParam("basepath", "", "./");
139                         Generate(overview, overviewxsl, overviewargs, opts.dest + "/index." + opts.ext, template, sourceDirectories);
140                         overviewargs.RemoveParam("basepath", "");
141                 }
142                 overviewargs.AddParam("basepath", "", "../");
143                 
144                 // Create the namespace & type pages
145                 
146                 XsltArgumentList typeargs = new XsltArgumentList();
147                 typeargs.AddParam("ext", "", opts.ext);
148                 typeargs.AddParam("basepath", "", "../");
149                 typeargs.AddParam("Index", "", overview.CreateNavigator ());
150                 
151                 foreach (XmlElement ns in overview.SelectNodes("Overview/Types/Namespace")) {
152                         string nsname = ns.GetAttribute("Name");
153
154                         if (opts.onlytype != null && !opts.onlytype.StartsWith(nsname + "."))
155                                 continue;
156                                 
157                         System.IO.DirectoryInfo d = new System.IO.DirectoryInfo(opts.dest + "/" + nsname);
158                         if (!d.Exists) d.Create();
159                         
160                         // Create the NS page
161                         string nsDest = opts.dest + "/" + nsname + "/index." + opts.ext;
162                         if (regenIndex) {
163                                 overviewargs.AddParam("namespace", "", nsname);
164                                 Generate(overview, overviewxsl, overviewargs, nsDest, template, sourceDirectories);
165                                 overviewargs.RemoveParam("namespace", "");
166                         }
167                         
168                         foreach (XmlElement ty in ns.SelectNodes("Type")) {
169                                 string typename, typefile, destfile;
170                                 GetTypePaths (opts, ty, out typename, out typefile, out destfile);
171
172                                 if (DestinationIsNewer (typefile, destfile))
173                                         // target already exists, and is newer.  why regenerate?
174                                         continue;
175
176                                 XmlDocument typexml = new XmlDocument();
177                                 typexml.Load(typefile);
178                                 PreserveMembersInVersions (typexml);
179                                 if (extensions != null) {
180                                         DocLoader loader = CreateDocLoader (overview);
181                                         XmlDocUtils.AddExtensionMethods (typexml, extensions, loader);
182                                 }
183                                 
184                                 Console.WriteLine(nsname + "." + typename);
185                                 
186                                 Generate(typexml, stylesheet, typeargs, destfile, template, sourceDirectories);
187                         }
188                 }
189         }
190
191         private static ArrayList GetExtensionMethods (XmlDocument doc)
192         {
193                 XmlNodeList extensions = doc.SelectNodes ("/Overview/ExtensionMethods/*");
194                 if (extensions.Count == 0)
195                         return null;
196                 ArrayList r = new ArrayList (extensions.Count);
197                 foreach (XmlNode n in extensions)
198                         r.Add (n);
199                 return r;
200         }
201
202         static bool ShouldRegenIndexes (MDocToHtmlConverterOptions opts, XmlDocument overview, List<string> sourceDirectories)
203         {
204                 string overviewDest   = opts.dest + "/index." + opts.ext;
205                 if (sourceDirectories.Any (
206                                         d => !DestinationIsNewer (Path.Combine (d, "index.xml"), overviewDest)))
207                         return true;
208
209                 foreach (XmlElement type in overview.SelectNodes("Overview/Types/Namespace/Type")) {
210                         string _, srcfile, destfile;
211                         GetTypePaths (opts, type, out _, out srcfile, out destfile);
212
213                         if (srcfile == null || destfile == null)
214                                 continue;
215                         if (DestinationIsNewer (srcfile, destfile))
216                                 return true;
217                 }
218
219                 return false;
220         }
221
222         static void GetTypePaths (MDocToHtmlConverterOptions opts, XmlElement type, out string typename, out string srcfile, out string destfile)
223         {
224                 srcfile   = null;
225                 destfile  = null;
226
227                 string nsname       = type.ParentNode.Attributes ["Name"].Value;
228                 string typefilebase = type.GetAttribute("Name");
229                 string sourceDir    = type.GetAttribute("SourceDirectory");
230                 typename            = type.GetAttribute("DisplayName");
231                 if (typename.Length == 0)
232                         typename = typefilebase;
233                 
234                 if (opts.onlytype != null && !(nsname + "." + typename).StartsWith(opts.onlytype))
235                         return;
236
237                 srcfile = CombinePath (sourceDir, nsname, typefilebase + ".xml");
238                 if (srcfile == null)
239                         return;
240
241                 destfile = CombinePath (opts.dest, nsname, typefilebase + "." + opts.ext);
242         }
243         
244         private static void DumpTemplate() {
245                 Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream("defaulttemplate.xsl");
246                 Stream o = Console.OpenStandardOutput ();
247                 byte[] buf = new byte[1024];
248                 int r;
249                 while ((r = s.Read (buf, 0, buf.Length)) > 0) {
250                         o.Write (buf, 0, r);
251                 }
252         }
253         
254         private static void Generate(XmlDocument source, XslCompiledTransform transform, XsltArgumentList args, string output, XslCompiledTransform template, List<string> sourceDirectories) {
255                 using (TextWriter textwriter = new StreamWriter(new FileStream(output, FileMode.Create))) {
256                         XmlTextWriter writer = new XmlTextWriter(textwriter);
257                         writer.Formatting = Formatting.Indented;
258                         writer.Indentation = 2;
259                         writer.IndentChar = ' ';
260                         
261                         try {
262                                 var intermediate = new StringBuilder ();
263                                 transform.Transform (
264                                                 new XmlNodeReader (source), 
265                                                 args, 
266                                                 XmlWriter.Create (intermediate, transform.OutputSettings),
267                                                 new ManifestResourceResolver(sourceDirectories.ToArray ()));
268                                 template.Transform (
269                                                 XmlReader.Create (new StringReader (intermediate.ToString ())),
270                                                 new XsltArgumentList (),
271                                                 new XhtmlWriter (writer),
272                                                 null);
273                         } catch (Exception e) {
274                                 throw new ApplicationException("An error occured while generating " + output, e);
275                         }
276                 }
277         }
278         
279         private XslCompiledTransform LoadTransform(string name, List<string> sourceDirectories) {
280                 try {
281                         XmlDocument xsl = new XmlDocument();
282                         xsl.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream(name));
283                         
284                         if (name == "overview.xsl") {
285                                 // bit of a hack.  overview needs the templates in stylesheet
286                                 // for doc formatting, and rather than write a resolver, I'll
287                                 // just do the import for it.
288                                 
289                                 XmlNode importnode = xsl.DocumentElement.SelectSingleNode("*[name()='xsl:include']");
290                                 xsl.DocumentElement.RemoveChild(importnode);
291                                 
292                                 XmlDocument xsl2 = new XmlDocument();
293                                 xsl2.Load(Assembly.GetExecutingAssembly().GetManifestResourceStream("stylesheet.xsl"));
294                                 foreach (XmlNode node in xsl2.DocumentElement.ChildNodes)
295                                         xsl.DocumentElement.AppendChild(xsl.ImportNode(node, true));
296                         }
297                         
298                         XslCompiledTransform t = new XslCompiledTransform (DebugOutput);
299                         t.Load (
300                                         xsl, 
301                                         XsltSettings.TrustedXslt,
302                                         new ManifestResourceResolver (sourceDirectories.ToArray ()));
303                         
304                         return t;
305                 } catch (Exception e) {
306                         throw new ApplicationException("Error loading " + name + " from internal resource", e);
307                 }
308         }
309
310         private static DocLoader CreateDocLoader (XmlDocument overview)
311         {
312                 Hashtable docs = new Hashtable ();
313                 DocLoader loader = delegate (string s) {
314                         XmlDocument d = null;
315                         if (!docs.ContainsKey (s)) {
316                                 foreach (XmlNode n in overview.SelectNodes ("//Type")) {
317                                         string ns = n.ParentNode.Attributes ["Name"].Value;
318                                         string t  = n.Attributes ["Name"].Value;
319                                         string sd = n.Attributes ["SourceDirectory"].Value;
320                                         if (s == ns + "." + t.Replace ("+", ".")) {
321                                                 string f = CombinePath (sd, ns, t + ".xml");
322                                                 if (File.Exists (f)) {
323                                                         d = new XmlDocument ();
324                                                         d.Load (f);
325                                                 }
326                                                 docs.Add (s, d);
327                                                 break;
328                                         }
329                                 }
330                         }
331                         else
332                                 d = (XmlDocument) docs [s];
333                         return d;
334                 };
335                 return loader;
336         }
337
338         static string CombinePath (params string[] paths)
339         {
340                 if (paths == null)
341                         return null;
342                 if (paths.Length == 1)
343                         return paths [0];
344                 var path = Path.Combine (paths [0], paths [1]);
345                 for (int i = 2; i < paths.Length; ++i)
346                         path = Path.Combine (path, paths [i]);
347                 return path;
348         }
349
350         private XmlDocument GetOverview (IEnumerable<string> directories)
351         {
352                 var index = new XmlDocument ();
353
354                 var overview  = index.CreateElement ("Overview");
355                 var assemblies= index.CreateElement ("Assemblies");
356                 var types     = index.CreateElement ("Types");
357                 var ems       = index.CreateElement ("ExtensionMethods");
358
359                 index.AppendChild (overview);
360                 overview.AppendChild (assemblies);
361                 overview.AppendChild (types);
362                 overview.AppendChild (ems);
363
364                 bool first = true;
365
366                 foreach (var dir in directories) {
367                         var indexFile = Path.Combine (dir, "index.xml");
368                         try {
369                                 var doc = new XmlDocument ();
370                                 doc.Load (indexFile);
371                                 if (first) {
372                                         var c = doc.SelectSingleNode ("/Overview/Copyright");
373                                         var t = doc.SelectSingleNode ("/Overview/Title");
374                                         var r = doc.SelectSingleNode ("/Overview/Remarks");
375                                         if (c != null && t != null && r != null) {
376                                                 var e = index.CreateElement ("Copyright");
377                                                 e.InnerXml = c.InnerXml;
378                                                 overview.AppendChild (e);
379
380                                                 e = index.CreateElement ("Title");
381                                                 e.InnerXml = t.InnerXml;
382                                                 overview.AppendChild (e);
383
384                                                 e = index.CreateElement ("Remarks");
385                                                 e.InnerXml = r.InnerXml;
386                                                 overview.AppendChild (e);
387
388                                                 first = false;
389                                         }
390                                 }
391                                 AddAssemblies (assemblies, doc);
392                                 AddTypes (types, doc, dir);
393                                 AddChildren (ems, doc, "/Overview/ExtensionMethods");
394                         }
395                         catch (Exception e) {
396                                 Message (TraceLevel.Warning, "Could not load documentation index '{0}': {1}",
397                                                 indexFile, e.Message);
398                         }
399                 }
400
401                 return index;
402         }
403
404         static void AddChildren (XmlNode dest, XmlDocument source, string path)
405         {
406                 var n = source.SelectSingleNode (path);
407                 if (n != null)
408                         foreach (XmlNode c in n.ChildNodes)
409                                 dest.AppendChild (dest.OwnerDocument.ImportNode (c, true));
410         }
411
412         static void AddAssemblies (XmlNode dest, XmlDocument source)
413         {
414                 foreach (XmlNode asm in source.SelectNodes ("/Overview/Assemblies/Assembly")) {
415                         var n = asm.Attributes ["Name"].Value;
416                         var v = asm.Attributes ["Version"].Value;
417                         if (dest.SelectSingleNode (string.Format ("Assembly[@Name='{0}'][@Value='{1}']", n, v)) == null) {
418                                 dest.AppendChild (dest.OwnerDocument.ImportNode (asm, true));
419                         }
420                 }
421         }
422
423         static void AddTypes (XmlNode dest, XmlDocument source, string sourceDirectory)
424         {
425                 var types = source.SelectSingleNode ("/Overview/Types");
426                 if (types == null)
427                         return;
428                 foreach (XmlNode ns in types.ChildNodes) {
429                         var n = ns.Attributes ["Name"].Value;
430                         var nsd = dest.SelectSingleNode (string.Format ("Namespace[@Name='{0}']", n));
431                         if (nsd == null) {
432                                 nsd = dest.OwnerDocument.CreateElement ("Namespace");
433                                 AddAttribute (nsd, "Name", n);
434                                 dest.AppendChild (nsd);
435                         }
436                         foreach (XmlNode t in ns.ChildNodes) {
437                                 if (!TypeInVersions (sourceDirectory, n, t))
438                                         continue;
439                                 var c = dest.OwnerDocument.ImportNode (t, true);
440                                 AddAttribute (c, "SourceDirectory", sourceDirectory);
441                                 nsd.AppendChild (c);
442                         }
443                         if (nsd.ChildNodes.Count == 0)
444                                 dest.RemoveChild (nsd);
445                 }
446         }
447
448         static bool TypeInVersions (string sourceDirectory, string ns, XmlNode type)
449         {
450                 if (opts.versions.Count == 0)
451                         return true;
452                 var file = Path.Combine (Path.Combine (sourceDirectory, ns), type.Attributes ["Name"].Value + ".xml");
453                 if (!File.Exists (file))
454                         return false;
455                 XPathDocument doc;
456                 using (var s = File.OpenText (file))
457                         doc = new XPathDocument (s);
458                 return MemberInVersions (doc.CreateNavigator ().SelectSingleNode ("/Type"));
459         }
460
461         static bool MemberInVersions (XPathNavigator nav)
462         {
463                 return nav.Select ("AssemblyInfo/AssemblyVersion")
464                         .Cast<object> ()
465                         .Any (v => opts.versions.Contains (v.ToString ()));
466         }
467
468         static void AddAttribute (XmlNode self, string name, string value)
469         {
470                 var a = self.OwnerDocument.CreateAttribute (name);
471                 a.Value = value;
472                 self.Attributes.Append (a);
473         }
474
475         private static bool DestinationIsNewer (string source, string dest)
476         {
477                 return !opts.forceUpdate && File.Exists (dest) &&
478                         File.GetLastWriteTime (source) < File.GetLastWriteTime (dest);
479         }
480
481         private static void PreserveMembersInVersions (XmlDocument doc)
482         {
483                 if (opts.versions.Count == 0)
484                         return;
485                 var remove = new List<XmlNode>();
486                 foreach (XmlNode m in doc.SelectNodes ("/Type/Members/Member")) {
487                         if (!MemberInVersions (m.CreateNavigator ()))
488                                 remove.Add (m);
489                 }
490                 XmlNode members = doc.SelectSingleNode ("/Type/Members");
491                 foreach (var m in remove)
492                         members.RemoveChild (m);
493         }
494 }
495
496 }