c8c89158c43c372e4fc9335bab26c3740f5cd3fd
[mono.git] / mcs / tools / mdoc / Mono.Documentation / assembler.cs
1 //
2 // The assembler: Help compiler.
3 //
4 // Author:
5 //   Miguel de Icaza (miguel@gnome.org)
6 //
7 // (C) 2003 Ximian, Inc.
8 //
9 using System;
10 using System.Collections.Generic;
11 using System.Linq;
12 using System.Xml;
13 using Monodoc;
14 using Monodoc.Providers;
15 using Mono.Options;
16 using System.IO;
17 using System.Xml.Linq;
18 using System.Xml.XPath;
19 using Monodoc.Ecma;
20
21 namespace Mono.Documentation {
22
23 public class MDocAssembler : MDocCommand {
24         static readonly string[] ValidFormats = {
25                 "ecma", 
26                 "ecmaspec", 
27                 "error", 
28                 "hb", 
29                 "man", 
30                 "simple", 
31                 "xhtml"
32         };
33
34         string droppedNamespace = null;
35
36         public static Option[] CreateFormatOptions (MDocCommand self, Dictionary<string, List<string>> formats)
37         {
38                 string cur_format = "ecma";
39                 var options = new OptionSet () {
40                         { "f|format=",
41                                 "The documentation {FORMAT} used in DIRECTORIES.  " + 
42                                         "Valid formats include:\n  " +
43                                         string.Join ("\n  ", ValidFormats) + "\n" +
44                                         "If not specified, the default format is `ecma'.",
45                                 v => {
46                                         if (Array.IndexOf (ValidFormats, v) < 0)
47                                                 self.Error ("Invalid documentation format: {0}.", v);
48                                         cur_format = v;
49                                 } },
50                         { "<>", v => AddFormat (self, formats, cur_format, v) },
51                 };
52                 return new Option[]{options[0], options[1]};
53         }
54
55         public override void Run (IEnumerable<string> args)
56         {
57                 bool replaceNTypes = false;
58                 var formats = new Dictionary<string, List<string>> ();
59                 string prefix = "tree";
60                 var formatOptions = CreateFormatOptions (this, formats);
61                 var options = new OptionSet () {
62                         formatOptions [0],
63                         { "o|out=",
64                                 "Provides the output file prefix; the files {PREFIX}.zip and " + 
65                                         "{PREFIX}.tree will be created.\n" +
66                                         "If not specified, `tree' is the default PREFIX.",
67                                 v => prefix = v },
68                         formatOptions [1],
69                         {"dropns=","The namespace that has been dropped from this version of the assembly.", v => droppedNamespace = v },
70                         {"ntypes","Replace references to native types with their original types.", v => replaceNTypes=true },
71                 };
72                 List<string> extra = Parse (options, args, "assemble", 
73                                 "[OPTIONS]+ DIRECTORIES",
74                                 "Assemble documentation within DIRECTORIES for use within the monodoc browser.");
75                 if (extra == null)
76                         return;
77
78                 List<Provider> list = new List<Provider> ();
79                 EcmaProvider ecma = null;
80                 bool sort = false;
81                 
82                 foreach (string format in formats.Keys) {
83                         switch (format) {
84                         case "ecma":
85                                 if (ecma == null) {
86                                         ecma = new EcmaProvider ();
87                                         list.Add (ecma);
88                                         sort = true;
89                                 }
90                                 ecma.FileSource = new MDocFileSource(droppedNamespace, string.IsNullOrWhiteSpace(droppedNamespace) ? ApiStyle.Unified : ApiStyle.Classic) {
91                                         ReplaceNativeTypes = replaceNTypes
92                                 };
93                                 foreach (string dir in formats [format])
94                                         ecma.AddDirectory (dir);
95                                 break;
96
97                         case "xhtml":
98                         case "hb":
99                                 list.AddRange (formats [format].Select (d => (Provider) new XhtmlProvider (d)));
100                                 break;
101
102                         case "man":
103                                 list.Add (new ManProvider (formats [format].ToArray ()));
104                                 break;
105
106                         case "error":
107                                 list.AddRange (formats [format].Select (d => (Provider) new ErrorProvider (d)));
108                                 break;
109
110                         case "ecmaspec":
111                                 list.AddRange (formats [format].Select (d => (Provider) new EcmaSpecProvider (d)));
112                                 break;
113
114                         case "addins":
115                                 list.AddRange (formats [format].Select (d => (Provider) new AddinsProvider (d)));
116                                 break;
117                         }
118                 }
119
120                 HelpSource hs = new HelpSource (prefix, true);
121                 hs.TraceLevel = TraceLevel;
122
123                 foreach (Provider p in list) {
124                         p.PopulateTree (hs.Tree);
125                 }
126
127                 if (sort && hs.Tree != null)
128                         hs.Tree.RootNode.Sort ();
129                               
130                 //
131                 // Flushes the EcmaProvider
132                 //
133                 foreach (Provider p in list)
134                         p.CloseTree (hs, hs.Tree);
135
136                 hs.Save ();
137         }
138
139         private static void AddFormat (MDocCommand self, Dictionary<string, List<string>> d, string format, string file)
140         {
141                 if (format == null)
142                         self.Error ("No format specified.");
143                 List<string> l;
144                 if (!d.TryGetValue (format, out l)) {
145                         l = new List<string> ();
146                         d.Add (format, l);
147                 }
148                 l.Add (file);
149         }
150 }
151
152         /// <summary>
153         /// A custom provider file source that lets us modify the source files before they are processed by monodoc.
154         /// </summary>
155         internal class MDocFileSource : IEcmaProviderFileSource {
156                 private string droppedNamespace;
157                 private bool shouldDropNamespace = false;
158                 private ApiStyle styleToDrop;
159
160                 public bool ReplaceNativeTypes { get; set; }
161
162                 /// <param name="ns">The namespace that is being dropped.</param>
163                 /// <param name="style">The style that is being dropped.</param>
164                 public MDocFileSource(string ns, ApiStyle style) 
165                 {
166                         droppedNamespace = ns;
167                         shouldDropNamespace = !string.IsNullOrWhiteSpace (ns);
168                         styleToDrop = style;
169                 }
170
171                 public XmlReader GetIndexReader(string path) 
172                 {
173                         XDocument doc = XDocument.Load (path);
174
175                         DropApiStyle (doc, path);
176                         DropNSFromDocument (doc);
177
178                         // now put the modified contents into a stream for the XmlReader that monodoc will use.
179                         MemoryStream io = new MemoryStream ();
180                         using (var writer = XmlWriter.Create (io)) {
181                                 doc.WriteTo (writer);
182                         }
183                         io.Seek (0, SeekOrigin.Begin);
184
185                         return XmlReader.Create (io);
186                 }
187
188                 public XElement GetNamespaceElement(string path) 
189                 {
190                         var element = XElement.Load (path);
191
192                         var attributes = element.Descendants ().Concat(new XElement[] { element }).SelectMany (n => n.Attributes ());
193                         var textNodes = element.Nodes ().OfType<XText> ();
194
195                         DropNS (attributes, textNodes);
196
197                         return element;
198                 }
199
200                 void DropApiStyle(XDocument doc, string path) 
201                 {
202                         string styleString = styleToDrop.ToString ().ToLower ();
203                         var items = doc
204                                 .Descendants ()
205                                 .Where (n => n.Attributes ()
206                                         .Any (a => a.Name.LocalName == "apistyle" && a.Value == styleString))
207                                 .ToArray ();
208
209                         foreach (var element in items) {
210                                 element.Remove ();
211                         }
212
213                         if (styleToDrop == ApiStyle.Classic && ReplaceNativeTypes) {
214                                 RewriteCrefsIfNecessary (doc, path);
215                         }
216                 }
217
218                 void RewriteCrefsIfNecessary (XDocument doc, string path)
219                 {
220                         // we also have to rewrite crefs
221                         var sees = doc.Descendants ().Where (d => d.Name.LocalName == "see").ToArray ();
222                         foreach (var see in sees) {
223                                 var cref = see.Attribute ("cref");
224                                 if (cref == null) {
225                                         continue;
226                                 }
227                                 EcmaUrlParser parser = new EcmaUrlParser ();
228                                 EcmaDesc reference;
229                                 if (!parser.TryParse (cref.Value, out reference)) {
230                                         continue;
231                                 }
232                                 if ((new EcmaDesc.Kind[] {
233                                         EcmaDesc.Kind.Constructor,
234                                         EcmaDesc.Kind.Method
235                                 }).Any (k => k == reference.DescKind)) {
236                                         string ns = reference.Namespace;
237                                         string type = reference.TypeName;
238                                         string memberName = reference.MemberName;
239                                         if (reference.MemberArguments != null) {
240                                                 XDocument refDoc = FindReferenceDoc (path, doc, ns, type);
241                                                 if (refDoc == null) {
242                                                         continue;
243                                                 }
244                                                 // look in the refDoc for the memberName, and match on parameters and # of type parameters
245                                                 var overloads = refDoc.XPathSelectElements ("//Member[@MemberName='" + memberName + "']").ToArray ();
246                                                 // Do some initial filtering to find members that could potentially match (based on parameter and typeparam counts)
247                                                 var members = overloads.Where (e => reference.MemberArgumentsCount == e.XPathSelectElements ("Parameters/Parameter[not(@apistyle) or @apistyle='classic']").Count () && reference.GenericMemberArgumentsCount == e.XPathSelectElements ("TypeParameters/TypeParameter[not(@apistyle) or @apistyle='classic']").Count ()).Select (m => new {
248                                                         Node = m,
249                                                         AllParameters = m.XPathSelectElements ("Parameters/Parameter").ToArray (),
250                                                         Parameters = m.XPathSelectElements ("Parameters/Parameter[not(@apistyle) or @apistyle='classic']").ToArray (),
251                                                         NewParameters = m.XPathSelectElements ("Parameters/Parameter[@apistyle='unified']").ToArray ()
252                                                 }).ToArray ();
253                                                 // now find the member that matches on types
254                                                 var member = members.FirstOrDefault (m => reference.MemberArguments.All (r => m.Parameters.Any (mp => mp.Attribute ("Type").Value.Contains (r.TypeName))));
255                                                 if (member == null || member.NewParameters.Length == 0)
256                                                         continue;
257                                                 foreach (var arg in reference.MemberArguments) {
258                                                         // find the "classic" parameter
259                                                         var oldParam = member.Parameters.First (p => p.Attribute ("Type").Value.Contains (arg.TypeName));
260                                                         var newParam = member.NewParameters.FirstOrDefault (p => oldParam.Attribute ("Name").Value == p.Attribute ("Name").Value);
261                                                         if (newParam != null) {
262                                                                 // this means there was a change made, and we should try to convert this cref
263                                                                 arg.TypeName = NativeTypeManager.ConvertToNativeType (arg.TypeName);
264                                                         }
265                                                 }
266                                                 var rewrittenReference = reference.ToEcmaCref ();
267                                                 Console.WriteLine ("From {0} to {1}", cref.Value, rewrittenReference);
268                                                 cref.Value = rewrittenReference;
269                                         }
270                                 }
271                         }
272                 }
273
274                 XDocument FindReferenceDoc (string currentPath, XDocument currentDoc, string ns, string type)
275                 {
276                         if (currentPath.Length <= 1) {
277                                 return null;
278                         }
279                         // build up the supposed path to the doc
280                         string dir = Path.GetDirectoryName (currentPath);
281                         if (dir.Equals (currentPath)) {
282                                 return null;
283                         }
284
285                         string supposedPath = Path.Combine (dir, ns, type + ".xml");
286
287                         // if it's the current path, return currentDoc
288                         if (supposedPath == currentPath) {
289                                 return currentDoc;
290                         }
291
292                         if (!File.Exists (supposedPath)) {
293                                 // hmm, file not there, look one directory up
294                                 return FindReferenceDoc (dir, currentDoc, ns, type);
295                         }
296
297                         // otherwise, load the XDoc and return
298                         return XDocument.Load (supposedPath);
299                 }
300
301                 void DropNSFromDocument (XDocument doc)
302                 {
303                         var attributes = doc.Descendants ().SelectMany (n => n.Attributes ());
304                         var textNodes = doc.DescendantNodes().OfType<XText> ().ToArray();
305
306                         DropNS (attributes, textNodes);
307                 }
308
309                 void DropNS(IEnumerable<XAttribute> attributes, IEnumerable<XText> textNodes) 
310                 {
311                         if (!shouldDropNamespace) {
312                                 return;
313                         }
314
315                         string nsString = string.Format ("{0}.", droppedNamespace);
316                         foreach (var attr in attributes) {
317                                 if (attr.Value.Contains (nsString)) {
318                                         attr.Value = attr.Value.Replace (nsString, string.Empty);
319                                 }
320                         }
321
322                         foreach (var textNode in textNodes) {
323                                 if (textNode.Value.Contains (nsString)) {
324                                         textNode.Value = textNode.Value.Replace (nsString, string.Empty);
325                                 }
326                         }
327                 }
328                         
329
330                 /// <param name="nsName">This is the type's name in the processed XML content. 
331                 /// If dropping the namespace, we'll need to append it so that it's found in the source.</param>
332                 /// <param name="typeName">Type name.</param>
333                 public string GetTypeXmlPath(string basePath, string nsName, string typeName) 
334                 {
335                         string nsNameToUse = nsName;
336                         if (shouldDropNamespace) {
337                                 nsNameToUse = string.Format ("{0}.{1}", droppedNamespace, nsName);
338
339                                 var droppedPath = BuildTypeXmlPath (basePath, typeName, nsNameToUse);
340                                 var origPath = BuildTypeXmlPath (basePath, typeName, nsName);
341
342                                 if (!File.Exists (droppedPath)) {
343                                         if (File.Exists (origPath)) {
344                                                 return origPath;
345                                         }
346                                 }
347
348                                 return droppedPath;
349                         } else {
350
351                                 var finalPath = BuildTypeXmlPath (basePath, typeName, nsNameToUse);
352
353                                 return finalPath;
354                         }
355                 }
356
357                 static string BuildTypeXmlPath (string basePath, string typeName, string nsNameToUse)
358                 {
359                         string finalPath = Path.Combine (basePath, nsNameToUse, Path.ChangeExtension (typeName, ".xml"));
360                         return finalPath;
361                 }
362
363                 static string BuildNamespaceXmlPath (string basePath, string ns)
364                 {
365                         var nsFileName = Path.Combine (basePath, String.Format ("ns-{0}.xml", ns));
366                         return nsFileName;
367                 }
368
369                 /// <returns>The namespace for path, with the dropped namespace so it can be used to pick the right file if we're dropping it.</returns>
370                 /// <param name="ns">This namespace will already have "dropped" the namespace.</param>
371                 public string GetNamespaceXmlPath(string basePath, string ns) 
372                 {
373                         string nsNameToUse = ns;
374                         if (shouldDropNamespace) {
375                                 nsNameToUse = string.Format ("{0}.{1}", droppedNamespace, ns);
376
377                                 var droppedPath = BuildNamespaceXmlPath (basePath, nsNameToUse);
378                                 var origPath = BuildNamespaceXmlPath (basePath, ns);
379
380                                 if (!File.Exists (droppedPath) && File.Exists(origPath)) {
381                                         return origPath;
382                                 }
383
384                                 return droppedPath;
385                         } else {
386                                 var path = BuildNamespaceXmlPath (basePath, ns); 
387                                 return path;
388                         }
389                 }
390
391                 public XDocument GetTypeDocument(string path) 
392                 {
393                         var doc = XDocument.Load (path);
394                         DropApiStyle (doc, path);
395                         DropNSFromDocument (doc);
396
397                         return doc;
398                 }
399
400                 public XElement ExtractNamespaceSummary (string path)
401                 {
402                         using (var reader = GetIndexReader (path)) {
403                                 reader.ReadToFollowing ("Namespace");
404                                 var name = reader.GetAttribute ("Name");
405                                 var summary = reader.ReadToFollowing ("summary") ? XElement.Load (reader.ReadSubtree ()) : new XElement ("summary");
406                                 var remarks = reader.ReadToFollowing ("remarks") ? XElement.Load (reader.ReadSubtree ()) : new XElement ("remarks");
407
408                                 return new XElement ("namespace",
409                                         new XAttribute ("ns", name ?? string.Empty),
410                                         summary,
411                                         remarks);
412                         }
413                 }
414         }
415 }