* Monodoc/provider.cs: Revert; Don't check for Tree.Nodes == null, as it
[mono.git] / mcs / tools / monodoc / Monodoc / provider.cs
1 //
2 // Provider: shared code and interfaces for providers
3 //
4 // Author:
5 //   Miguel de Icaza (miguel@ximian.com)
6 //
7 // (C) 2002, Ximian, Inc.
8 //
9 // TODO:
10 //   Each node should have a provider link
11 //
12 //   Should encode numbers using a runlength encoding to save space
13 //
14 namespace Monodoc {
15 using System;
16 using System.IO;
17 using System.Text;
18 using System.Text.RegularExpressions;
19 using System.Collections;
20 using System.Diagnostics;
21 using System.Configuration;
22 using System.Text.RegularExpressions;
23 using System.Xml;
24 using System.Xml.XPath;
25 using ICSharpCode.SharpZipLib.Zip;
26
27 using Monodoc.Lucene.Net.Index;
28 using Monodoc.Lucene.Net.Analysis.Standard;
29 /// <summary>
30 ///    This tree is populated by the documentation providers, or populated
31 ///    from a binary encoding of the tree.  The format of the tree is designed
32 ///    to minimize the need to load it in full.
33 /// </summary>
34 public class Tree : Node {
35
36 #region Loading the tree from a file
37
38         /// <summary>
39         ///   Our HelpSource container
40         /// </summary>
41         public readonly HelpSource HelpSource;
42         
43         internal FileStream InputStream;
44         internal BinaryReader InputReader;
45
46         /// <summary>
47         ///   Load from file constructor
48         /// </summary>
49         public Tree (HelpSource hs, string filename) : base (null, null)
50         {
51                 Encoding utf8 = new UTF8Encoding (false, true);
52
53                 if (!File.Exists (filename)){
54                         throw new FileNotFoundException ();
55                 }
56                 
57                 InputStream = File.OpenRead (filename);
58                 InputReader = new BinaryReader (InputStream, utf8);
59                 byte [] sig = InputReader.ReadBytes (4);
60                 
61                 if (!GoodSig (sig))
62                         throw new Exception ("Invalid file format");
63                 
64                 InputStream.Position = 4;
65                 position = InputReader.ReadInt32 ();
66
67                 LoadNode ();
68                 HelpSource = hs;
69         }
70
71         /// <summary>
72         ///    Tree creation and merged tree constructor
73         /// </summary>
74         public Tree (HelpSource hs, string caption, string url) : base (caption, url)
75         {
76                 HelpSource = hs;
77         }
78
79         public Tree (HelpSource hs, Node parent, string caption, string element) : base (parent, caption, element)
80         {
81                 HelpSource = hs;
82         }
83
84 #endregion
85
86         /// <summary>
87         ///    Saves the tree into the specified file using the help file format.
88         /// </summary>
89         public void Save (string file)
90         {
91                 Encoding utf8 = new UTF8Encoding (false, true);
92                 using (FileStream output = File.OpenWrite (file)){
93                         // Skip over the pointer to the first node.
94                         output.Position = 8;
95                         
96                         using (BinaryWriter writer = new BinaryWriter (output, utf8)){
97                                 // Recursively dump
98                                 Dump (output, writer);
99
100                                 output.Position = 0;
101                                 writer.Write (new byte [] { (byte) 'M', (byte) 'o', (byte) 'H', (byte) 'P' });
102                                 writer.Write (position);
103                         }
104                 }
105         }
106
107         static bool GoodSig (byte [] sig)
108         {
109                 if (sig.Length != 4)
110                         return false;
111                 if (sig [0] != (byte) 'M' ||
112                     sig [1] != (byte) 'o' ||
113                     sig [2] != (byte) 'H' ||
114                     sig [3] != (byte) 'P')
115                         return false;
116                 return true;
117         }
118
119 }
120
121 public class Node : IComparable {
122         string caption, element;
123         public bool Documented;
124         public readonly Tree tree;
125         Node parent;
126         protected ArrayList nodes;
127         protected internal int position;
128
129         static ArrayList empty = ArrayList.ReadOnly(new ArrayList(0));
130
131         /// <summary>
132         ///    Creates a node, called by the Tree.
133         /// </summary>
134         public Node (string caption, string element)
135         {
136                 this.tree = (Tree) this;
137                 this.caption = caption;
138                 this.element = element;
139                 parent = null;
140         }
141
142         public Node (Node parent, string caption, string element)
143         {
144                 this.parent = parent;
145                 this.tree = parent.tree;
146                 this.caption = caption;
147                 this.element = element;
148         }
149         
150         /// <summary>
151         ///    Creates a node from an on-disk representation
152         /// </summary>
153         Node (Node parent, int address)
154         {
155                 this.parent = parent;
156                 position = address;
157                 this.tree = parent.tree;
158                 if (address > 0)
159                         LoadNode ();
160         }
161
162         public void AddNode (Node n)
163         {
164                 Nodes.Add (n);
165                 n.parent = this;
166                 n.Documented = true;
167         }
168
169         public void DelNode (Node n)
170         {
171                 Nodes.Remove (n);
172         }
173
174         public ArrayList Nodes {
175                 get {
176                         if (position < 0)
177                                 LoadNode ();
178                         return nodes != null ? nodes : empty;
179                 }
180         }
181
182         public string Element {
183                 get {
184                         if (position < 0)
185                                 LoadNode ();
186                         return element;
187                 }
188
189                 set {
190                         element = value;
191                 }
192         }
193
194         public string Caption {
195                 get {
196                         if (position < 0)
197                                 LoadNode ();
198                         return caption;
199                 }
200         }
201         
202         public Node Parent {
203                 get {
204                         return parent;
205                 }
206         }
207                 
208         public void LoadNode ()
209         {
210                 if (position < 0)
211                         position = -position;
212
213                 tree.InputStream.Position = position;
214                 BinaryReader reader = tree.InputReader;
215                 int count = DecodeInt (reader);
216                 element = reader.ReadString ();
217                 caption = reader.ReadString ();
218                 if (count == 0)
219                         return;
220                 
221                 nodes = new ArrayList (count);
222                 for (int i = 0; i < count; i++){
223                         int child_address = DecodeInt (reader);
224                                                               
225                         Node t = new Node (this, -child_address);
226                         nodes.Add (t);
227                 }
228         }
229         
230         /// <summary>
231         ///   Creates a new node, in the locator entry point, and with
232         ///   a user visible caption of @caption
233         /// </summary>
234         public Node CreateNode (string c_caption, string c_element)
235         {
236                 if (nodes == null)
237                         nodes = new ArrayList ();
238
239                 Node t = new Node (this, c_caption, c_element);
240                 nodes.Add (t);
241                 return t;
242         }
243
244         /// <summary>
245         ///   Looks up or creates a new node, in the locator entry point, and with
246         ///   a user visible caption of @caption.  This is different from
247         ///   CreateNode in that it will look up an existing node for the given @locator.
248         /// </summary>
249         public Node LookupNode (string c_caption, string c_element)
250         {
251                 if (nodes == null)
252                         return CreateNode (c_caption, c_element);
253
254                 foreach (Node n in nodes){
255                         if (n.element == c_element)
256                                 return n;
257                 }
258                 return CreateNode (c_caption, c_element);
259         }
260
261         public void EnsureNodes ()
262         {
263                 if (nodes == null)
264                         nodes = new ArrayList ();
265         }
266         
267         public bool IsLeaf {
268                 get {
269                         return nodes == null;
270                 }
271         }
272
273         void EncodeInt (BinaryWriter writer, int value)
274         {
275                 do {
276                         int high = (value >> 7) & 0x01ffffff;
277                         byte b = (byte)(value & 0x7f);
278
279                         if (high != 0) {
280                                 b = (byte)(b | 0x80);
281                         }
282                         
283                         writer.Write(b);
284                         value = high;
285                 } while(value != 0);
286         }
287
288         int DecodeInt (BinaryReader reader)
289         {
290                 int ret = 0;
291                 int shift = 0;
292                 byte b;
293                 
294                         do {
295                                 b = reader.ReadByte();
296
297                                 ret = ret | ((b & 0x7f) << shift);
298                                 shift += 7;
299                         } while ((b & 0x80) == 0x80);
300                         
301                         return ret;
302         }
303
304         internal void Dump (FileStream output, BinaryWriter writer)
305         {
306                 if (nodes != null){
307                         foreach (Node child in nodes){
308                                 child.Dump (output, writer);
309                         }
310                 }
311                 position = (int) output.Position;
312                 EncodeInt (writer, nodes == null ? 0 : (int) nodes.Count);
313                 writer.Write (element);
314                 writer.Write (caption);
315
316                 if (nodes != null){
317                         foreach (Node child in nodes){
318                                 EncodeInt (writer, child.position);
319                         }
320                 }
321         }
322
323         static int indent;
324
325         static void Indent ()
326         {
327                 for (int i = 0; i < indent; i++)
328                         Console.Write ("   ");
329         }
330         
331         public static void PrintTree (Node node)
332         {
333                 Indent ();
334                 Console.WriteLine ("{0},{1}", node.Element, node.Caption);
335                 if (node.Nodes == null)
336                         return;
337
338                 indent++;
339                 foreach (Node n in node.nodes)
340                         PrintTree (n);
341                 indent--;
342         }
343
344         public void Sort ()
345         {
346                 if (nodes != null)
347                         nodes.Sort ();
348         }
349
350         public string URL {
351                 get {
352                         if (position < 0)
353                                 LoadNode ();
354
355                         if (element.IndexOf (":") >= 0)
356                                 return element;
357
358                         if (parent != null){
359                                 string url = parent.URL;
360
361                                 if (url.EndsWith ("/"))
362                                         return url + element;
363                                 else
364                                         return parent.URL + "/" + element;
365                         } else
366                                 return element;
367                 }
368         }
369
370         int IComparable.CompareTo (object obj)
371         {
372                 Node other = obj as Node;
373                 if (other == null)
374                         return -1;
375
376                 if (position < 0)
377                         LoadNode ();
378                 if (other.position < 0)
379                         other.LoadNode ();
380
381                 Regex digits = new Regex (@"([\d]+)|([^\d]+)");
382                 MatchEvaluator eval = delegate (Match m) {
383                         return (m.Value.Length > 0 && char.IsDigit (m.Value [0])) 
384                                 ? m.Value.PadLeft (System.Math.Max (caption.Length, other.caption.Length)) 
385                                 : m.Value;
386                 };
387                 return digits.Replace (caption, eval).CompareTo (digits.Replace (other.caption, eval));
388         }
389 }
390
391 //
392 // The HelpSource class keeps track of the archived data, and its
393 // tree
394 //
395 public class HelpSource {
396         static int id;
397         public static bool use_css = false;
398         public static string css_code;
399         public static string CssCode {
400                 get {
401                         if (css_code != null)
402                                 return css_code;
403
404                         System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly ();
405                         Stream str_css = assembly.GetManifestResourceStream ("base.css");
406                         StringBuilder sb = new StringBuilder ((new StreamReader (str_css)).ReadToEnd());
407                         sb.Replace ("@@FONT_FAMILY@@", SettingsHandler.Settings.preferred_font_family);
408                         sb.Replace ("@@FONT_SIZE@@", SettingsHandler.Settings.preferred_font_size.ToString());
409                         css_code = sb.ToString ();
410                         return css_code;
411                 }
412                 set { css_code = value; }
413         }
414
415         public virtual string InlineCss {
416                 get { return CssCode; }
417         }
418
419         public virtual string InlineJavaScript {
420                 get { return null; }
421         }
422
423         public static bool FullHtml = true;
424
425         //
426         // The unique ID for this HelpSource.
427         //
428         int source_id;
429         DateTime zipFileWriteTime;
430         string name;
431         TraceLevel trace_level = TraceLevel.Warning;
432
433         public HelpSource (string base_filename, bool create)
434         {
435                 this.name = Path.GetFileName (base_filename);
436                 tree_filename = base_filename + ".tree";
437                 zip_filename = base_filename + ".zip";
438
439                 if (create)
440                         SetupForOutput ();
441                 else 
442                         Tree = new Tree (this, tree_filename);
443
444                 source_id = id++;
445                 try {
446                         FileInfo fi = new FileInfo (zip_filename);
447                         zipFileWriteTime = fi.LastWriteTime;
448                 } catch {
449                         zipFileWriteTime = DateTime.Now;
450                 }
451         }
452         
453         public HelpSource() {
454                 Tree = new Tree (this, "Blah", "Blah");
455                 source_id = id++;
456         }
457
458         public DateTime ZipFileWriteTime {
459                 get {
460                         return zipFileWriteTime;
461                 }
462         }
463         
464         public int SourceID {
465                 get {
466                         return source_id;
467                 }
468         }
469         
470         public string Name {
471                 get {
472                         return name;
473                 }
474         }
475
476         public TraceLevel TraceLevel {
477                 get { return trace_level; }
478                 set { trace_level = value; }
479         }
480         
481         ZipFile zip_file;
482         
483         /// <summary>
484         ///   Returns a stream from the packaged help source archive
485         /// </summary>
486         public Stream GetHelpStream (string id)
487         {
488                 if (zip_file == null)
489                         zip_file = new ZipFile (zip_filename);
490
491                 ZipEntry entry = zip_file.GetEntry (id);
492                 if (entry != null)
493                         return zip_file.GetInputStream (entry);
494                 return null;
495         }
496         
497         public string GetRealPath (string file)
498         {
499                 if (zip_file == null)
500                         zip_file = new ZipFile (zip_filename);
501
502                 ZipEntry entry = zip_file.GetEntry (file);
503                 if (entry != null && entry.ExtraData != null)
504                         return ConvertToString (entry.ExtraData);
505                 return null;
506         }
507         
508         public XmlReader GetHelpXml (string id)
509         {
510                 if (zip_file == null)
511                         zip_file = new ZipFile (zip_filename);
512
513                 ZipEntry entry = zip_file.GetEntry (id);
514                 if (entry != null) {
515                         Stream s = zip_file.GetInputStream (entry);
516                         string url = "monodoc:///" + SourceID + "@" + System.Web.HttpUtility.UrlEncode (id) + "@";
517                         return new XmlTextReader (url, s);
518                 }
519                 return null;
520         }
521         
522         public XmlDocument GetHelpXmlWithChanges (string id)
523         {
524                 if (zip_file == null)
525                         zip_file = new ZipFile (zip_filename);
526
527                 ZipEntry entry = zip_file.GetEntry (id);
528                 if (entry != null) {
529                         Stream s = zip_file.GetInputStream (entry);
530                         string url = "monodoc:///" + SourceID + "@" + System.Web.HttpUtility.UrlEncode (id) + "@";
531                         XmlReader r = new XmlTextReader (url, s);
532                         XmlDocument ret = new XmlDocument ();
533                         ret.Load (r);
534                         
535                         if (entry.ExtraData != null)
536                                 EditingUtils.AccountForChanges (ret, Name, ConvertToString (entry.ExtraData));
537                         
538                         return ret;
539                 }
540                 return null;    
541         }
542         
543         /// <summary>
544         ///   Get a nice, unique expression for any XPath node that you get.
545         ///   This function is used by editing to get the expression to put
546         ///   on to the file. The idea is to create an expression that is resistant
547         ///   to changes in the structure of the XML.
548         /// </summary>
549         public virtual string GetNodeXPath (XPathNavigator n)
550         {
551                 return EditingUtils.GetXPath (n.Clone ());
552         }
553         
554         public string GetEditUri (XPathNavigator n)
555         {
556                 return EditingUtils.FormatEditUri (n.BaseURI, GetNodeXPath (n));
557         }
558         
559         static string ConvertToString (byte[] data)
560         {
561                 return Encoding.UTF8.GetString(data);
562         }
563         
564         static byte[] ConvertToArray (string str)
565         {
566                 return Encoding.UTF8.GetBytes(str);
567         }
568
569         /// <summary>
570         ///   The tree that is being populated
571         /// </summary>
572         public Tree Tree;
573         public RootTree RootTree;
574
575         // Base filename used by this HelpSource.
576         string tree_filename, zip_filename;
577
578         // Used for ziping. 
579         const int buffer_size = 65536;
580         ZipOutputStream zip_output;
581         byte [] buffer;
582         
583         HelpSource (string base_filename)
584         {
585         }
586                 
587         void SetupForOutput ()
588         {
589                 Tree = new Tree (this, "", "");
590
591                 FileStream stream = File.Create (zip_filename);
592                 
593                 zip_output = new ZipOutputStream (stream);
594                 zip_output.SetLevel (9);
595
596                 buffer = new byte [buffer_size];
597         }               
598
599         /// <summary>
600         ///   Saves the tree and the archive
601         /// </summary>
602         public void Save ()
603         {
604                 Tree.Save (tree_filename);
605                 zip_output.Finish ();
606                 zip_output.Close ();
607         }
608
609         int code;
610
611         string GetNewCode ()
612         {
613                 return String.Format ("{0}", code++);
614         }
615
616         /// <summary>
617         ///   Providers call this to store a file they will need, and the return value
618         ///   is the name that was assigned to it
619         /// </summary>
620         public string PackFile (string file)
621         {
622                 string entry_name = GetNewCode ();
623                 return PackFile (file, entry_name);
624         }
625
626         public string PackFile (string file, string entry_name)
627         {
628                 using (FileStream input = File.OpenRead (file)) {
629                         PackStream (input, entry_name, file);
630                 }
631
632                 return entry_name;
633         }
634         
635         public void PackStream (Stream s, string entry_name)
636         {
637                 PackStream (s, entry_name, null);
638         }
639         
640         void PackStream (Stream s, string entry_name, string realPath)
641         {
642                 ZipEntry entry = new ZipEntry (entry_name);
643                                 
644                 if (realPath != null)
645                         entry.ExtraData = ConvertToArray (realPath);
646                 
647                 zip_output.PutNextEntry (entry);
648                 int n;
649                         
650                 while ((n = s.Read (buffer, 0, buffer_size)) > 0){
651                         zip_output.Write (buffer, 0, n);
652                 }       
653         }
654         
655         public void PackXml (string fname, XmlDocument doc, string real_path)
656         { 
657                 ZipEntry entry = new ZipEntry (fname); 
658                 if (real_path != null) 
659                         entry.ExtraData = ConvertToArray(real_path);
660
661                 zip_output.PutNextEntry (entry);
662                 XmlTextWriter xmlWriter = new XmlTextWriter (zip_output, Encoding.UTF8);
663                 doc.WriteContentTo (xmlWriter);
664                 xmlWriter.Flush ();
665         }
666         
667         public virtual void RenderPreviewDocs (XmlNode newNode, XmlWriter writer)
668         {
669                 throw new NotImplementedException ();
670         }
671         
672         public virtual string GetText (string url, out Node n)
673         {
674                 n = null;
675                 return null;
676         }
677
678         public virtual Stream GetImage (string url)
679         {
680                 return null;
681         }
682         
683         //
684         // Default method implementation does not satisfy the request
685         //
686         public virtual string RenderTypeLookup (string prefix, string ns, string type, string member, out Node n)
687         {
688                 n = null;
689                 return null;
690         }
691
692         public virtual string RenderNamespaceLookup (string nsurl, out Node n)
693         {
694                 n = null;
695                 return null;
696         }
697
698         //
699         // Populates the index.
700         //
701         public virtual void PopulateIndex (IndexMaker index_maker)
702         {
703         }
704         
705         //
706         // Build an html document
707         //
708         public static string BuildHtml (string css, string html_code)
709         {
710                 return BuildHtml (css, null, html_code);
711         }
712
713         internal static string BuildHtml (string css, string js, string html_code) {
714                 if (!FullHtml) {
715                         return html_code;
716                 }
717                 StringWriter output = new StringWriter ();
718                 output.Write ("<html><head>");
719                 output.Write ("<style type=\"text/css\">");
720                 output.Write (CssCode);
721                 output.Write (css);
722                 output.Write ("</style>");
723
724                 System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly ();
725                 Stream str_js = assembly.GetManifestResourceStream ("helper.js");
726                 StringBuilder sb = new StringBuilder ((new StreamReader (str_js)).ReadToEnd());
727                 output.Write ("<script type=\"text/JavaScript\">\n");
728                 output.Write (sb.ToString ());
729                 output.Write ("</script>\n");
730                 
731                 if (js != null) {
732                         output.Write ("<script type=\"text/JavaScript\">\n");
733                         output.Write (js);
734                         output.Write ("\n</script>");
735                 }
736
737                 output.Write ("</head><body>");
738                 output.Write (html_code);
739                 output.Write ("</body></html>");
740                 return output.ToString ();
741         }
742
743         //
744         // Create different Documents for adding to Lucene search index
745         // The default action is do nothing. Subclasses should add the docs
746         // 
747         public virtual void PopulateSearchableIndex (IndexWriter writer) {
748                 return;
749         }
750
751         public void Message (TraceLevel level, string format, params object[] args)
752         {
753                 if ((int) level <= (int) trace_level)
754                         Console.WriteLine (format, args);
755         }
756
757         public void Error (string format, params object[] args)
758         {
759                 Console.Error.WriteLine (format, args);
760         }
761 }
762
763 public abstract class Provider {
764         //
765         // This code is used to "tag" all the different sources
766         //
767         static short serial;
768
769         public int code;
770         
771         public Provider ()
772         {
773                 code = serial++;
774         }
775
776         public abstract void PopulateTree (Tree tree);
777
778         //
779         // Called at shutdown time after the tree has been populated to perform
780         // any fixups or final tasks.
781         //
782         public abstract void CloseTree (HelpSource hs, Tree tree);
783 }
784
785 public class RootTree : Tree {
786         string basedir;
787         
788         public static ArrayList UncompiledHelpSources = new ArrayList();
789         
790         public const int MonodocVersion = 1;
791         
792         public static RootTree LoadTree ()
793         {
794                 string basedir;
795                 string myPath = System.Reflection.Assembly.GetExecutingAssembly ().Location;
796                 string cfgFile = myPath + ".config";
797                 if (!File.Exists (cfgFile)) {
798                         basedir = ".";
799                         return LoadTree (basedir);
800                 }
801                 
802                 XmlDocument d = new XmlDocument ();
803                 d.Load (cfgFile);
804                 basedir = d.SelectSingleNode ("config/path").Attributes ["docsPath"].Value;
805                 
806                 return LoadTree (basedir);
807         }
808         
809         //
810         // Loads the tree layout
811         //
812         public static RootTree LoadTree (string basedir)
813         {
814                 XmlDocument doc = new XmlDocument ();
815
816                 RootTree root = new RootTree ();
817                 root.basedir = basedir;
818                 
819                 //
820                 // Load the layout
821                 //
822                 string layout = Path.Combine (basedir, "monodoc.xml");
823                 doc.Load (layout);
824                 XmlNodeList nodes = doc.SelectNodes ("/node/node");
825
826                 root.name_to_node ["root"] = root;
827                 root.name_to_node ["libraries"] = root;
828                 root.Populate (root, nodes);
829
830                 Node third_party = root.LookupEntryPoint ("various");
831                 if (third_party == null) {
832                         Console.Error.WriteLine ("No 'various' doc node! Check monodoc.xml!");
833                         third_party = root;
834                 }
835
836                 //
837                 // Load the sources
838                 //
839                 string sources_dir = Path.Combine (basedir, "sources");
840                 
841                 string [] files = Directory.GetFiles (sources_dir);
842                 foreach (string file in files){
843                         if (!file.EndsWith (".source"))
844                                 continue;
845
846                         doc = new XmlDocument ();
847                         try {
848                                 doc.Load (file);
849                         } catch {
850                                 Console.Error.WriteLine ("Error: Could not load source file {0}", file);
851                                 continue;
852                         }
853
854                         XmlNodeList extra_nodes = doc.SelectNodes ("/monodoc/node");
855                         if (extra_nodes.Count > 0)
856                                 root.Populate (third_party, extra_nodes);
857
858                         XmlNodeList sources = doc.SelectNodes ("/monodoc/source");
859                         if (sources == null){
860                                 Console.Error.WriteLine ("Error: No <source> section found in the {0} file", file);
861                                 continue;
862                         }
863                         foreach (XmlNode source in sources){
864                                 XmlAttribute a = source.Attributes ["provider"];
865                                 if (a == null){
866                                         Console.Error.WriteLine ("Error: no provider in <source>");
867                                         continue;
868                                 }
869                                 string provider = a.InnerText;
870                                 a = source.Attributes ["basefile"];
871                                 if (a == null){
872                                         Console.Error.WriteLine ("Error: no basefile in <source>");
873                                         continue;
874                                 }
875                                 string basefile = a.InnerText;
876                                 a = source.Attributes ["path"];
877                                 if (a == null){
878                                         Console.Error.WriteLine ("Error: no path in <source>");
879                                         continue;
880                                 }
881                                 string path = a.InnerText;
882
883                                 string basefilepath = Path.Combine (sources_dir, basefile);
884                                 HelpSource hs = GetHelpSource (provider, basefilepath);
885                                 if (hs == null)
886                                         continue;
887                                 hs.RootTree = root;
888                                 root.help_sources.Add (hs);
889                                 root.name_to_hs [path] = hs;
890
891                                 Node parent = root.LookupEntryPoint (path);
892                                 if (parent == null){
893                                         Console.Error.WriteLine ("node `{0}' is not defined on the documentation map", path);
894                                         parent = third_party;
895                                 }
896
897                                 foreach (Node n in hs.Tree.Nodes){
898                                         parent.AddNode (n);
899                                 }
900                                 parent.Sort ();
901                         }
902                 }
903                 
904                 foreach (string path in UncompiledHelpSources) {
905                         EcmaUncompiledHelpSource hs = new EcmaUncompiledHelpSource(path);
906                         root.help_sources.Add (hs);
907                         string epath = "extra-help-source-" + hs.Name;
908                         Node hsn = root.CreateNode (hs.Name, "root:/" + epath);
909                         root.name_to_hs [epath] = hs;
910                         hsn.EnsureNodes ();
911                         foreach (Node n in hs.Tree.Nodes){
912                                 hsn.AddNode (n);
913                         }
914                 }
915                 
916                 // Clean the tree
917                 PurgeNode(root);
918
919                 root.Sort ();
920
921                 return root;
922         }
923         
924         // Delete nodes which does not have documentaiton (source)
925         static bool PurgeNode(Node node)
926         {
927                 bool purge = false;
928                 
929                 if (!node.Documented)
930                 {
931                         ArrayList del_child = new ArrayList();
932                         //Delete node unless any child has documentation
933                         bool purged_child = false;
934                         foreach (Node child in node.Nodes)
935                         {
936                                 purged_child = PurgeNode(child);
937                                 if (purged_child) 
938                                 {
939                                         del_child.Add(child);
940                                 }
941                         }
942                                 
943                         // delete the node if all its children are to be deleted
944                         purge = (node.Nodes.Count == del_child.Count); 
945                                 
946                         // delete children
947                         foreach (Node child in del_child)
948                         {
949                                 node.DelNode(child);
950                         }
951                 }
952                 
953                 return purge;
954         }
955                                         
956         
957         static HelpSource GetHelpSource (string provider, string basefilepath)
958         {
959                 try {
960                         switch (provider){
961                         case "ecma":
962                                 return new EcmaHelpSource (basefilepath, false);
963                         case "ecma-uncompiled":
964                                 return new EcmaUncompiledHelpSource (basefilepath);
965                         case "monohb":
966                                 return new MonoHBHelpSource(basefilepath, false);
967                         case "xhtml":
968                                 return new XhtmlHelpSource (basefilepath, false);
969                         case "man":
970                                 return new ManHelpSource (basefilepath, false);
971                         case "simple":
972                                 return new SimpleHelpSource (basefilepath, false);
973                         case "error":
974                                 return new ErrorHelpSource (basefilepath, false);
975                         case "ecmaspec":
976                                 return new EcmaSpecHelpSource (basefilepath, false);
977                         case "addins":
978                                 return new AddinsHelpSource (basefilepath, false);
979                         default:
980                                 Console.Error.WriteLine ("Error: Unknown provider specified: {0}", provider);
981                                 break;
982                         }
983                         return null;
984                 }
985                 catch (FileNotFoundException) {
986                         Console.Error.WriteLine ("Error: did not find one of the files in sources/"+basefilepath);
987                         return null;
988                 }
989         }
990                 
991
992         //
993         // Maintains the name to node mapping
994         //
995         Hashtable name_to_node = new Hashtable ();
996         Hashtable name_to_hs = new Hashtable ();
997         
998         void Populate (Node parent, XmlNodeList xml_node_list)
999         {
1000                 foreach (XmlNode xml_node in xml_node_list){
1001                         XmlAttribute e = xml_node.Attributes ["parent"];
1002                         if (e != null && name_to_node.ContainsKey (e.InnerText)) {
1003                                 Node p = (Node) name_to_node [e.InnerText];
1004                                 xml_node.Attributes.Remove (e);
1005                                 Populate (p, xml_node.SelectNodes ("."));
1006                                 continue;
1007                         }
1008                         e = xml_node.Attributes ["label"];
1009                         if (e == null){
1010                                 Console.Error.WriteLine ("`label' attribute missing in <node>");
1011                                 continue;
1012                         }
1013                         string label = e.InnerText;
1014                         e = xml_node.Attributes ["name"];
1015                         if (e == null){
1016                                 Console.Error.WriteLine ("`name' attribute missing in <node>");
1017                                 continue;
1018                         }
1019                         string name = e.InnerText;
1020
1021                         Node n = parent.LookupNode (label, "root:/" + name);
1022                         n.EnsureNodes ();
1023                         name_to_node [name] = n;
1024                         XmlNodeList children = xml_node.SelectNodes ("./node");
1025                         if (children != null)
1026                                 Populate (n, children);
1027                 }
1028         }
1029
1030         public Node LookupEntryPoint (string name)
1031         {
1032                 return (Node) name_to_node [name];
1033         }
1034         
1035         ArrayList help_sources;
1036         DateTime lastHelpSourceTime;
1037         
1038         RootTree () : base (null, "Mono Documentation", "root:")
1039         {
1040                 nodes = new ArrayList ();
1041                 help_sources = new ArrayList ();
1042                 lastHelpSourceTime = DateTime.MinValue;
1043         }
1044
1045         public DateTime LastHelpSourceTime {
1046                 get {
1047                         return lastHelpSourceTime;
1048                 }
1049         }
1050         
1051         public static bool GetNamespaceAndType (string url, out string ns, out string type)
1052         {
1053                 int nsidx = -1;
1054                 int numLt = 0;
1055                 for (int i = 0; i < url.Length; ++i) {
1056                         char c = url [i];
1057                         switch (c) {
1058                         case '<':
1059                         case '{':
1060                                 ++numLt;
1061                                 break;
1062                         case '>':
1063                         case '}':
1064                                 --numLt;
1065                                 break;
1066                         case '.':
1067                                 if (numLt == 0)
1068                                         nsidx = i;
1069                                 break;
1070                         }
1071                 }
1072
1073                 if (nsidx == -1) {
1074                         Console.Error.WriteLine ("Did not find dot in: " + url);
1075                         ns = null;
1076                         type = null;
1077                         return false;
1078                 }
1079                 ns = url.Substring (0, nsidx);
1080                 type = url.Substring (nsidx + 1);
1081                 
1082                 //Console.Error.WriteLine ("GetNameSpaceAndType (ns={0}, type={1}", ns, type);
1083                 return true;
1084         }
1085
1086         public XmlDocument GetHelpXml (string url)
1087         {
1088                 string rest = url.Substring (2);
1089                 string ns, type;
1090
1091                 if (!GetNamespaceAndType (rest, out ns, out type))
1092                         return null;
1093
1094                 foreach (HelpSource hs in help_sources) {
1095                         EcmaHelpSource ehs = hs as EcmaHelpSource;
1096                         if (ehs == null)
1097                                 continue;
1098                         string id = ehs.GetIdFromUrl ("T:", ns, type);
1099                         if (id == null)
1100                                 continue;
1101                         XmlDocument doc = hs.GetHelpXmlWithChanges (id);
1102                         if (doc != null)
1103                                 return doc;
1104                 }
1105                 return null;
1106         }
1107         
1108         public string TypeLookup (string url, out Node match_node)
1109         {
1110                 string rest = Regex.Replace (url, @"^T:\s*", "");
1111                 string ns, type;
1112
1113                 if (!GetNamespaceAndType (rest, out ns, out type)){
1114                         match_node = null;
1115                         return null;
1116                 }
1117                 
1118                 foreach (HelpSource hs in help_sources){
1119                         string s = hs.RenderTypeLookup ("T:", ns, type, null, out match_node);
1120                         
1121                         if (s != null) {
1122                                 lastHelpSourceTime = hs.ZipFileWriteTime;
1123                                 return s;
1124                         }
1125                 }
1126                 match_node = null;
1127                 return null;
1128         }
1129
1130         public string MemberLookup (string prefix, string url, out Node match_node)
1131         {
1132                 string rest = Regex.Replace (url, @"^.:\s*", "");
1133                 
1134                 // Dots in the arg list (for methods) confuse this.
1135                 // Chop off the arg list for now and put it back later.
1136                 string arglist = "";
1137                 int argliststart = rest.IndexOf("(");
1138                 if (argliststart >= 0) {
1139                         arglist = rest.Substring(argliststart);
1140                         rest = rest.Substring(0, argliststart);
1141                 }
1142
1143                 string ns_type, member;
1144         
1145                 if (prefix != "C:") {
1146                         int member_idx = rest.LastIndexOf (".");
1147         
1148                         // The dot in .ctor (if it's a M: link) would confuse this.
1149                         if (rest.EndsWith("..ctor")) member_idx--;
1150         
1151                         ns_type = rest.Substring (0, member_idx);
1152                         member = rest.Substring (member_idx + 1);
1153                 } else {
1154                         // C: links don't have the .ctor member part as it would in a M: link
1155                         // Even though externally C: links are different from M: links,
1156                         // C: links get transformed into M:-style links (with .ctor) here.
1157                         ns_type = rest;
1158                         member = ".ctor";
1159                 }
1160  
1161
1162                 //Console.WriteLine ("NS_TYPE: {0}  MEMBER: {1}", ns_type, member);
1163
1164                 string ns, type;
1165                 if (!GetNamespaceAndType (ns_type, out ns, out type)){
1166                         match_node = null;
1167                         return null;
1168                 }
1169                 
1170                 foreach (HelpSource hs in help_sources){
1171                         string s = hs.RenderTypeLookup (prefix, ns, type, member + arglist, out match_node);
1172                         
1173                         if (s != null) {
1174                                 lastHelpSourceTime = hs.ZipFileWriteTime;
1175                                 return s;
1176                         }
1177                 }
1178                 match_node = null;
1179                 return null;
1180         }
1181
1182         public Stream GetImage (string url)
1183         {
1184                 if (url.StartsWith ("source-id:")){
1185                         string rest = url.Substring (10);
1186                         int p = rest.IndexOf (":");
1187                         string str_idx = rest.Substring (0, p);
1188                         int idx = 0;
1189
1190                         try {
1191                                 idx = Int32.Parse (str_idx);
1192                         } catch {
1193                                 Console.Error.WriteLine ("Failed to parse source-id url: {0} `{1}'", url, str_idx);
1194                                 return null;
1195                         }
1196
1197                         HelpSource hs = GetHelpSourceFromId (idx);
1198                         lastHelpSourceTime = hs.ZipFileWriteTime;
1199                         return hs.GetImage (rest.Substring (p + 1));
1200                 } else {
1201                         System.Reflection.Assembly assembly = System.Reflection.Assembly.GetAssembly (typeof(RootTree));                        
1202                         return assembly.GetManifestResourceStream (url);
1203                 }
1204                 lastHelpSourceTime = DateTime.MinValue;
1205                 return null;
1206         }
1207         
1208         public HelpSource GetHelpSourceFromId (int id)
1209         {
1210                 return (HelpSource) help_sources [id];
1211         }
1212         
1213         string home_cache;
1214         /// <summary>
1215         ///    Allows every HelpSource to try to provide the content for this
1216         ///    URL.
1217         /// </summary>
1218         public string RenderUrl (string url, out Node match_node)
1219         {
1220                 lastHelpSourceTime = DateTime.MinValue;
1221                 if (url == "root:") {
1222                         match_node = this;
1223
1224                         // look whether there are contribs
1225                         GlobalChangeset chgs = EditingUtils.changes;
1226                         StringBuilder con = new StringBuilder ();
1227                         
1228                         //add links to the contrib
1229                         int oldContrib = 0, contribs = 0;
1230                         con.Append ("<ul>");
1231                         foreach (DocSetChangeset dscs in chgs.DocSetChangesets) 
1232                                 foreach (FileChangeset fcs in dscs.FileChangesets) 
1233                                         foreach (Change c in fcs.Changes) {
1234                                                 if (c.NodeUrl == null) {
1235                                                         if (c.Serial == SettingsHandler.Settings.SerialNumber)
1236                                                                 oldContrib++;
1237                                                 } else if (c.Serial == SettingsHandler.Settings.SerialNumber) {
1238                                                         contribs++;
1239                                                         con.Append (String.Format ("<li><a href=\"{0}\">{0}</a></li>", c.NodeUrl));
1240                                                 }
1241                                         }
1242                         
1243                         string contrib = (oldContrib + contribs) == 1?"There is {0} contribution":"There are {0} contributions";
1244                         con.Insert (0, String.Format (contrib, oldContrib + contribs) + " pending upload <i>(Contributing--&gt; Upload)</i>", 1);
1245                         con.Append ("</ul>");
1246                         if (oldContrib == 1)
1247                                 con.Append ("<i>You have 1 contribution that is not listed below that will be sent the next time you upload contributions. Only contributions made from now on will be listed.</i>");
1248                         else if (oldContrib > 1)
1249                                 con.Append ("<i>You have " + oldContrib + "contributions that are not listed below and will be sent the next time you upload contributions. Only contributions made from now on will be listed.</i>");
1250
1251                         //start the rendering
1252                         if (!HelpSource.use_css) {
1253                                 StringBuilder sb = new StringBuilder ("<table bgcolor=\"#b0c4de\" width=\"100%\" cellpadding=\"5\"><tr><td><h3>Mono Documentation Library</h3></td></tr></table>");
1254                         
1255                                 foreach (Node n in Nodes)
1256                                         sb.AppendFormat ("<a href='{0}'>{1}</a><br/>", n.Element, n.Caption);
1257                         
1258                                 //contributions
1259                                 sb.Append ("<br><table bgcolor=\"#fff3f3\" width=\"100%\" cellpadding=\"5\"><tr><td>");
1260                                 sb.Append ("<h5>Contributions</h5><br>");
1261                                 if ((oldContrib + contribs) == 0) {
1262                                         sb.Append ("<p><b>You have not made any contributions yet.</b></p>");
1263                                         sb.Append ("<p>The Documentation of the libraries is not complete and your contributions would be greatly appreciated. The procedure is easy, browse to the part of the documentation you want to contribute to and click on the <font color=\"blue\">[Edit]</font> link to start writing the documentation.</p>");
1264                                         sb.Append ("<p>When you are happy with your changes, use the Contributing--&gt; Upload Contributions menu to send your contributions to our server.</p></div>");
1265                                 } else {
1266                                         sb.Append (con.ToString ());
1267                                 }
1268                                 sb.Append ("</td></tr></table>");
1269                                 return sb.ToString ();  
1270                         } else {
1271                                 if (home_cache == null) {
1272                                         System.Reflection.Assembly assembly = System.Reflection.Assembly.GetAssembly (typeof (HelpSource));
1273                                         Stream hp_stream = assembly.GetManifestResourceStream ("home.html");
1274                                         home_cache = (new StreamReader (hp_stream)).ReadToEnd ();
1275                                 }
1276                                 StringBuilder sb = new StringBuilder (home_cache);
1277                                 // adjust fonts
1278                                 sb.Replace ("@@FONT_FAMILY@@", SettingsHandler.Settings.preferred_font_family);
1279                                 sb.Replace ("@@FONT_SIZE@@", SettingsHandler.Settings.preferred_font_size.ToString());
1280                                 //contributions
1281                                 if ((oldContrib + contribs) == 0) {
1282                                         sb.Replace ("@@CONTRIB_DISP@@", "display: none;");
1283                                 } else {
1284                                         sb.Replace ("@@NO_CONTRIB_DISP@@", "display: none;");
1285                                         sb.Replace ("@@CONTRIBS@@", con.ToString ());
1286                                 }
1287                                         
1288                                 // load the url of nodes
1289                                 String add_str;
1290                                 StringBuilder urls = new StringBuilder ();
1291                                 foreach (Node n in Nodes) {
1292                                         add_str = String.Format ("<li><a href=\"{0}\">{1}</a></li>", n.Element, n.Caption);
1293                                         urls.Append (add_str);
1294                                 }
1295                                 sb.Replace ("@@API_DOCS@@", urls.ToString ());
1296                                                 
1297                                 return sb.ToString ();
1298                         }
1299                 } 
1300                 
1301                 if (url.StartsWith ("root:")) {
1302                         match_node = ((Node)name_to_node [url.Substring (6)]);
1303                         HelpSource hs = ((HelpSource)name_to_hs [url.Substring (6)]);
1304                         if (hs == null) 
1305                         {
1306                                 return GenerateNodeIndex(match_node);
1307                         }
1308                                 
1309                         Node dummy;
1310                         lastHelpSourceTime = hs.ZipFileWriteTime;
1311                         return hs.GetText ("root:", out dummy);
1312                 }
1313         
1314                 
1315                 if (url.StartsWith ("source-id:")){
1316                         string rest = url.Substring (10);
1317                         int p = rest.IndexOf (":");
1318                         string str_idx = rest.Substring (0, p);
1319                         int idx = 0;
1320
1321                         try {
1322                                 idx = Int32.Parse (str_idx);
1323                         } catch {
1324                                 Console.Error.WriteLine ("Failed to parse source-id url: {0} `{1}'", url, str_idx);
1325                                 match_node = null;
1326                                 return null;
1327                         }
1328                         HelpSource hs = (HelpSource) help_sources [idx];
1329                         // Console.WriteLine ("Attempting to get docs from: " + rest.Substring (p + 1));
1330                         lastHelpSourceTime = hs.ZipFileWriteTime;
1331                         return hs.GetText (rest.Substring (p + 1), out match_node);
1332                 }
1333
1334                 if (url.Length < 2){
1335                         match_node = null;
1336                         return null;
1337                 }
1338                 
1339                 string prefix = url.Substring (0, 2);
1340                 
1341                 switch (prefix.ToUpper ()){
1342                 case "N:":
1343                         foreach (HelpSource hs in help_sources){
1344                                 string s = hs.RenderNamespaceLookup (url, out match_node);
1345                                 if (s != null) {
1346                                         lastHelpSourceTime = hs.ZipFileWriteTime;
1347                                         return s;
1348                                 }
1349                         }
1350                         match_node = null;
1351                         return null;
1352
1353                 case "T:":
1354                         return TypeLookup (url, out match_node);
1355
1356                 case "M:":
1357                 case "F:":
1358                 case "P:":
1359                 case "E:":
1360                 case "C:":
1361                 case "O:":
1362                         return MemberLookup (prefix, url, out match_node);
1363                 
1364                 default:
1365                         foreach (HelpSource hs in help_sources){
1366                                 string s = hs.GetText (url, out match_node);
1367                                 
1368                                 if (s != null) {
1369                                         lastHelpSourceTime = hs.ZipFileWriteTime;
1370                                         return s;
1371                                 }
1372                         }
1373                         match_node = null;
1374                         return null;
1375                 }
1376         }
1377         
1378         public string GenerateNodeIndex (Node node)
1379         {
1380                 StringBuilder buf = new StringBuilder();
1381                 buf.AppendFormat("<H3>{0}</H3>", node.Caption);
1382                 buf.Append("<ul>");
1383                 foreach (Node child in node.Nodes)
1384                 {
1385                         buf.AppendFormat("<li><a href=\"{0}\">{1}</a>", child.URL, child.Caption);
1386                 }
1387                 buf.Append("</ul>");
1388                 return buf.ToString();
1389         }
1390         
1391         public IndexReader GetIndex ()
1392         {
1393                 //try to load from basedir
1394                 string index_file = Path.Combine (basedir, "monodoc.index");
1395                 if (File.Exists (index_file))
1396                         return IndexReader.Load (index_file);
1397                 //then, try to load from config dir
1398                 index_file = Path.Combine (SettingsHandler.Path, "monodoc.index");
1399                 return IndexReader.Load (index_file);
1400                 
1401         }
1402
1403         public static void MakeIndex ()
1404         {
1405                 RootTree root = LoadTree ();
1406                 if (root == null)
1407                         return;
1408
1409                 IndexMaker index_maker = new IndexMaker ();
1410                 
1411                 foreach (HelpSource hs in root.help_sources){
1412                         hs.PopulateIndex (index_maker);
1413                 }
1414
1415                 // if the user has no write permissions use config dir
1416                 string path = Path.Combine (root.basedir, "monodoc.index");
1417                 try {
1418                         index_maker.Save (path);
1419                 } catch (System.UnauthorizedAccessException) {
1420                         path = Path.Combine (SettingsHandler.Path, "monodoc.index");
1421                         try {
1422                                 index_maker.Save (path);
1423                         } catch (System.UnauthorizedAccessException) {
1424                                 Console.WriteLine ("Unable to write index file in {0}", Path.Combine (SettingsHandler.Path, "monodoc.index")); 
1425                                 return;
1426                         }
1427                 }
1428
1429                 if (IsUnix){
1430                         // No octal in C#, how lame is that
1431                         chmod (path, 0x1a4);
1432                 }
1433                 Console.WriteLine ("Documentation index updated");
1434         }
1435
1436         static bool IsUnix {
1437                 get {
1438                         int p = (int) Environment.OSVersion.Platform;
1439                         return ((p == 4) || (p == 128) || (p == 6));
1440                 }
1441         }
1442
1443         // Search Index
1444         public SearchableIndex GetSearchIndex ()
1445         {
1446                 //try to load from basedir
1447                 string index_file = Path.Combine (basedir, "search_index");
1448                 if (Directory.Exists (index_file))
1449                         return SearchableIndex.Load (index_file);
1450                 //then, try to load from config dir
1451                 index_file = Path.Combine (SettingsHandler.Path, "search_index");
1452                 return SearchableIndex.Load (index_file);
1453         }
1454
1455         public static void MakeSearchIndex ()
1456         {
1457                 // Loads the RootTree
1458                 Console.WriteLine ("Loading the monodoc tree...");
1459                 RootTree root = LoadTree ();
1460                 if (root == null)
1461                         return;
1462
1463                 string dir = Path.Combine (root.basedir, "search_index");
1464                 IndexWriter writer;
1465                 //try to create the dir to store the index
1466                 try {
1467                         if (!Directory.Exists (dir)) 
1468                                 Directory.CreateDirectory (dir);
1469
1470                         writer = new IndexWriter(Lucene.Net.Store.FSDirectory.GetDirectory(dir, true), new StandardAnalyzer(), true);
1471                 } catch (UnauthorizedAccessException) {
1472                         //try in the .config directory
1473                         try {
1474                                 dir = Path.Combine (SettingsHandler.Path, "search_index");
1475                                 if (!Directory.Exists (dir)) 
1476                                         Directory.CreateDirectory (dir);
1477
1478                                 writer = new IndexWriter(Lucene.Net.Store.FSDirectory.GetDirectory(dir, true), new StandardAnalyzer(), true);
1479                         } catch (UnauthorizedAccessException) {
1480                                 Console.WriteLine ("You don't have permissions to write on " + dir);
1481                                 return;
1482                         }
1483                 }
1484
1485                 //Collect all the documents
1486                 Console.WriteLine ("Collecting and adding documents...");
1487                 foreach (HelpSource hs in root.HelpSources) 
1488                         hs.PopulateSearchableIndex (writer);
1489         
1490                 //Optimize and close
1491                 Console.WriteLine ("Closing...");
1492                 writer.Optimize();
1493                 writer.Close();
1494         }
1495
1496
1497         public ICollection HelpSources { get { return new ArrayList(help_sources); } }
1498
1499         [System.Runtime.InteropServices.DllImport ("libc")]
1500         static extern int chmod (string filename, int mode);
1501 }
1502 }