//
// Provider: shared code and interfaces for providers
//
// Author:
// Miguel de Icaza (miguel@ximian.com)
//
// (C) 2002, Ximian, Inc.
// Copyright 2003-2011 Novell
// Copyright 2011 Xamarin Inc
//
// TODO:
// Each node should have a provider link
//
// Should encode numbers using a runlength encoding to save space
//
namespace Monodoc {
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Diagnostics;
using System.Configuration;
using System.Reflection;
using System.Xml;
using System.Xml.XPath;
using ICSharpCode.SharpZipLib.Zip;
using Mono.Lucene.Net.Index;
using Mono.Lucene.Net.Analysis.Standard;
using Mono.Documentation;
///
/// This tree is populated by the documentation providers, or populated
/// from a binary encoding of the tree. The format of the tree is designed
/// to minimize the need to load it in full.
///
public class Tree : Node {
#region Loading the tree from a file
///
/// Our HelpSource container
///
public readonly HelpSource HelpSource;
internal FileStream InputStream;
internal BinaryReader InputReader;
///
/// Load from file constructor
///
public Tree (HelpSource hs, string filename) : base (null, null)
{
Encoding utf8 = new UTF8Encoding (false, true);
if (!File.Exists (filename)){
throw new FileNotFoundException ();
}
InputStream = File.OpenRead (filename);
InputReader = new BinaryReader (InputStream, utf8);
byte [] sig = InputReader.ReadBytes (4);
if (!GoodSig (sig))
throw new Exception ("Invalid file format");
InputStream.Position = 4;
position = InputReader.ReadInt32 ();
LoadNode ();
HelpSource = hs;
}
///
/// Tree creation and merged tree constructor
///
public Tree (HelpSource hs, string caption, string url) : base (caption, url)
{
HelpSource = hs;
}
public Tree (HelpSource hs, Node parent, string caption, string element) : base (parent, caption, element)
{
HelpSource = hs;
}
#endregion
///
/// Saves the tree into the specified file using the help file format.
///
public void Save (string file)
{
Encoding utf8 = new UTF8Encoding (false, true);
using (FileStream output = File.OpenWrite (file)){
// Skip over the pointer to the first node.
output.Position = 8;
using (BinaryWriter writer = new BinaryWriter (output, utf8)){
// Recursively dump
Dump (output, writer);
output.Position = 0;
writer.Write (new byte [] { (byte) 'M', (byte) 'o', (byte) 'H', (byte) 'P' });
writer.Write (position);
}
}
}
static bool GoodSig (byte [] sig)
{
if (sig.Length != 4)
return false;
if (sig [0] != (byte) 'M' ||
sig [1] != (byte) 'o' ||
sig [2] != (byte) 'H' ||
sig [3] != (byte) 'P')
return false;
return true;
}
}
public class Node : IComparable {
string caption, element;
public bool Documented;
public readonly Tree tree;
Node parent;
protected ArrayList nodes;
protected internal int position;
string compare_key;
static ArrayList empty = ArrayList.ReadOnly(new ArrayList(0));
///
/// Creates a node, called by the Tree.
///
public Node (string caption, string element)
{
this.tree = (Tree) this;
this.caption = caption;
this.element = element;
parent = null;
}
public Node (Node parent, string caption, string element)
{
this.parent = parent;
this.tree = parent.tree;
this.caption = caption;
this.element = element;
}
///
/// Creates a node from an on-disk representation
///
Node (Node parent, int address)
{
this.parent = parent;
position = address;
this.tree = parent.tree;
if (address > 0)
LoadNode ();
}
public void AddNode (Node n)
{
Nodes.Add (n);
n.parent = this;
n.Documented = true;
}
public void DelNode (Node n)
{
Nodes.Remove (n);
}
public ArrayList Nodes {
get {
if (position < 0)
LoadNode ();
return nodes != null ? nodes : empty;
}
}
public string Element {
get {
if (position < 0)
LoadNode ();
return element;
}
set {
element = value;
}
}
public string Caption {
get {
if (position < 0)
LoadNode ();
return caption;
}
}
public Node Parent {
get {
return parent;
}
}
public void LoadNode ()
{
if (position < 0)
position = -position;
tree.InputStream.Position = position;
BinaryReader reader = tree.InputReader;
int count = DecodeInt (reader);
element = reader.ReadString ();
caption = reader.ReadString ();
if (count == 0)
return;
nodes = new ArrayList (count);
for (int i = 0; i < count; i++){
int child_address = DecodeInt (reader);
Node t = new Node (this, -child_address);
nodes.Add (t);
}
}
///
/// Creates a new node, in the locator entry point, and with
/// a user visible caption of @caption
///
public Node CreateNode (string c_caption, string c_element)
{
if (nodes == null)
nodes = new ArrayList ();
Node t = new Node (this, c_caption, c_element);
nodes.Add (t);
return t;
}
///
/// Looks up or creates a new node, in the locator entry point, and with
/// a user visible caption of @caption. This is different from
/// CreateNode in that it will look up an existing node for the given @locator.
///
public Node LookupNode (string c_caption, string c_element)
{
if (nodes == null)
return CreateNode (c_caption, c_element);
foreach (Node n in nodes){
if (n.element == c_element)
return n;
}
return CreateNode (c_caption, c_element);
}
public void EnsureNodes ()
{
if (nodes == null)
nodes = new ArrayList ();
}
public bool IsLeaf {
get {
return nodes == null;
}
}
void EncodeInt (BinaryWriter writer, int value)
{
do {
int high = (value >> 7) & 0x01ffffff;
byte b = (byte)(value & 0x7f);
if (high != 0) {
b = (byte)(b | 0x80);
}
writer.Write(b);
value = high;
} while(value != 0);
}
int DecodeInt (BinaryReader reader)
{
int ret = 0;
int shift = 0;
byte b;
do {
b = reader.ReadByte();
ret = ret | ((b & 0x7f) << shift);
shift += 7;
} while ((b & 0x80) == 0x80);
return ret;
}
internal void Dump (FileStream output, BinaryWriter writer)
{
if (nodes != null){
foreach (Node child in nodes){
child.Dump (output, writer);
}
}
position = (int) output.Position;
EncodeInt (writer, nodes == null ? 0 : (int) nodes.Count);
writer.Write (element);
writer.Write (caption);
if (nodes != null){
foreach (Node child in nodes){
EncodeInt (writer, child.position);
}
}
}
static int indent;
static void Indent ()
{
for (int i = 0; i < indent; i++)
Console.Write (" ");
}
public static void PrintTree (Node node)
{
Indent ();
Console.WriteLine ("{0},{1}\t[PublicUrl: {2}]", node.Element, node.Caption, node.PublicUrl);
if (node.Nodes.Count == 0)
return;
indent++;
foreach (Node n in node.Nodes)
PrintTree (n);
indent--;
}
public void Sort ()
{
if (nodes != null)
nodes.Sort ();
}
[Obsolete("Use PublicUrl")]
public string URL {
get {
if (position < 0)
LoadNode ();
if (element.IndexOf (":") >= 0)
return element;
if (parent != null){
string url = parent.URL;
if (url.EndsWith ("/"))
return url + element;
else
return parent.URL + "/" + element;
} else
return element;
}
}
public string PublicUrl {
get {
return tree.HelpSource != null
? tree.HelpSource.GetPublicUrl (URL)
: URL;
}
}
int IComparable.CompareTo (object obj)
{
Node other = obj as Node;
if (other == null)
return -1;
if (position < 0)
LoadNode ();
if (other.position < 0)
other.LoadNode ();
if (compare_key == null || other.compare_key == null) {
Regex digits = new Regex (@"([\d]+)|([^\d]+)");
MatchEvaluator eval = delegate (Match m) {
return (m.Value.Length > 0 && char.IsDigit (m.Value [0]))
? m.Value.PadLeft (System.Math.Max (caption.Length, other.caption.Length))
: m.Value;
};
if (compare_key == null)
compare_key = digits.Replace (caption, eval);
if (other.compare_key == null)
other.compare_key = digits.Replace (other.caption, eval);
}
return compare_key.CompareTo (other.compare_key);
}
}
//
// The HelpSource class keeps track of the archived data, and its
// tree
//
public class HelpSource {
static int id;
public static bool use_css = false;
public static string css_code;
public static string CssCode {
get {
if (css_code != null)
return css_code;
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly ();
Stream str_css = assembly.GetManifestResourceStream ("base.css");
StringBuilder sb = new StringBuilder ((new StreamReader (str_css)).ReadToEnd());
sb.Replace ("@@FONT_FAMILY@@", SettingsHandler.Settings.preferred_font_family);
sb.Replace ("@@FONT_SIZE@@", SettingsHandler.Settings.preferred_font_size.ToString());
css_code = sb.ToString ();
return css_code;
}
set { css_code = value; }
}
public virtual string InlineCss {
get { return CssCode; }
}
public virtual string InlineJavaScript {
get { return null; }
}
public static bool FullHtml = true;
// should only be enabled by ASP.NET webdoc
public static bool UseWebdocCache;
//
// The unique ID for this HelpSource.
//
int source_id;
DateTime zipFileWriteTime;
string name;
string basepath;
TraceLevel trace_level = TraceLevel.Warning;
protected bool nozip;
protected string base_dir;
public HelpSource (string base_filename, bool create)
{
this.name = Path.GetFileName (base_filename);
this.basepath = Path.GetDirectoryName (base_filename);
tree_filename = base_filename + ".tree";
zip_filename = base_filename + ".zip";
base_dir = XmlDocUtils.GetCacheDirectory (base_filename);
if (UseWebdocCache && !create && Directory.Exists (base_dir)) {
nozip = true;
}
if (create)
SetupForOutput ();
else
Tree = new Tree (this, tree_filename);
source_id = id++;
try {
FileInfo fi = new FileInfo (zip_filename);
zipFileWriteTime = fi.LastWriteTime;
} catch {
zipFileWriteTime = DateTime.Now;
}
}
public HelpSource() {
Tree = new Tree (this, "Blah", "Blah");
source_id = id++;
}
public DateTime ZipFileWriteTime {
get {
return zipFileWriteTime;
}
}
public int SourceID {
get {
return source_id;
}
}
public string Name {
get {
return name;
}
}
/* This gives the full path of the source/ directory */
public string BaseFilePath {
get {
return basepath;
}
}
public TraceLevel TraceLevel {
get { return trace_level; }
set { trace_level = value; }
}
public string BaseDir {
get {
return base_dir;
}
}
ZipFile zip_file;
///
/// Returns a stream from the packaged help source archive
///
public virtual Stream GetHelpStream (string id)
{
if (nozip) {
string path = XmlDocUtils.GetCachedFileName (base_dir, id);
if (File.Exists (path))
return File.OpenRead (path);
return null;
}
if (zip_file == null)
zip_file = new ZipFile (zip_filename);
ZipEntry entry = zip_file.GetEntry (id);
if (entry != null)
return zip_file.GetInputStream (entry);
return null;
}
public string GetRealPath (string file)
{
if (zip_file == null)
zip_file = new ZipFile (zip_filename);
ZipEntry entry = zip_file.GetEntry (file);
if (entry != null && entry.ExtraData != null)
return ConvertToString (entry.ExtraData);
return null;
}
public XmlReader GetHelpXml (string id)
{
if (nozip) {
Stream s = File.OpenRead (XmlDocUtils.GetCachedFileName (base_dir, id));
string url = "monodoc:///" + SourceID + "@" + Uri.EscapeUriString (id) + "@";
return new XmlTextReader (url, s);
}
if (zip_file == null)
zip_file = new ZipFile (zip_filename);
ZipEntry entry = zip_file.GetEntry (id);
if (entry != null) {
Stream s = zip_file.GetInputStream (entry);
string url = "monodoc:///" + SourceID + "@" + Uri.EscapeUriString (id) + "@";
return new XmlTextReader (url, s);
}
return null;
}
public virtual XmlDocument GetHelpXmlWithChanges (string id)
{
if (nozip) {
Stream s = File.OpenRead (XmlDocUtils.GetCachedFileName (base_dir, id));
string url = "monodoc:///" + SourceID + "@" + Uri.EscapeUriString (id) + "@";
XmlReader r = new XmlTextReader (url, s);
XmlDocument ret = new XmlDocument ();
ret.Load (r);
return ret;
}
if (zip_file == null)
zip_file = new ZipFile (zip_filename);
ZipEntry entry = zip_file.GetEntry (id);
if (entry != null) {
Stream s = zip_file.GetInputStream (entry);
string url = "monodoc:///" + SourceID + "@" + Uri.EscapeUriString (id) + "@";
XmlReader r = new XmlTextReader (url, s);
XmlDocument ret = new XmlDocument ();
ret.Load (r);
if (entry.ExtraData != null)
EditingUtils.AccountForChanges (ret, Name, ConvertToString (entry.ExtraData));
return ret;
}
return null;
}
///
/// Get a nice, unique expression for any XPath node that you get.
/// This function is used by editing to get the expression to put
/// on to the file. The idea is to create an expression that is resistant
/// to changes in the structure of the XML.
///
public virtual string GetNodeXPath (XPathNavigator n)
{
return EditingUtils.GetXPath (n.Clone ());
}
public string GetEditUri (XPathNavigator n)
{
return EditingUtils.FormatEditUri (n.BaseURI, GetNodeXPath (n));
}
static string ConvertToString (byte[] data)
{
return Encoding.UTF8.GetString(data);
}
static byte[] ConvertToArray (string str)
{
return Encoding.UTF8.GetBytes(str);
}
///
/// The tree that is being populated
///
public Tree Tree;
public RootTree RootTree;
// Base filename used by this HelpSource.
string tree_filename, zip_filename;
// Used for ziping.
const int buffer_size = 65536;
ZipOutputStream zip_output;
byte [] buffer;
HelpSource (string base_filename)
{
}
void SetupForOutput ()
{
Tree = new Tree (this, "", "");
FileStream stream = File.Create (zip_filename);
zip_output = new ZipOutputStream (stream);
zip_output.SetLevel (9);
buffer = new byte [buffer_size];
}
///
/// Saves the tree and the archive
///
public void Save ()
{
Tree.Save (tree_filename);
zip_output.Finish ();
zip_output.Close ();
}
int code;
string GetNewCode ()
{
return String.Format ("{0}", code++);
}
///
/// Providers call this to store a file they will need, and the return value
/// is the name that was assigned to it
///
public string PackFile (string file)
{
string entry_name = GetNewCode ();
return PackFile (file, entry_name);
}
public string PackFile (string file, string entry_name)
{
using (FileStream input = File.OpenRead (file)) {
PackStream (input, entry_name, file);
}
return entry_name;
}
public void PackStream (Stream s, string entry_name)
{
PackStream (s, entry_name, null);
}
void PackStream (Stream s, string entry_name, string realPath)
{
ZipEntry entry = new ZipEntry (entry_name);
if (realPath != null)
entry.ExtraData = ConvertToArray (realPath);
zip_output.PutNextEntry (entry);
int n;
while ((n = s.Read (buffer, 0, buffer_size)) > 0){
zip_output.Write (buffer, 0, n);
}
}
public void PackXml (string fname, XmlDocument doc, string real_path)
{
ZipEntry entry = new ZipEntry (fname);
if (real_path != null)
entry.ExtraData = ConvertToArray(real_path);
zip_output.PutNextEntry (entry);
XmlTextWriter xmlWriter = new XmlTextWriter (zip_output, Encoding.UTF8);
doc.WriteContentTo (xmlWriter);
xmlWriter.Flush ();
}
public virtual void RenderPreviewDocs (XmlNode newNode, XmlWriter writer)
{
throw new NotImplementedException ();
}
public virtual string GetPublicUrl (string id)
{
return id;
}
public virtual string GetText (string url, out Node n)
{
n = null;
return null;
}
protected string GetCachedText (string url)
{
if (!nozip)
return null;
string file = XmlDocUtils.GetCachedFileName (base_dir, url);
if (!File.Exists (file))
return null;
return File.OpenText (file).ReadToEnd ();
}
public virtual Stream GetImage (string url)
{
return null;
}
//
// Default method implementation does not satisfy the request
//
public virtual string RenderTypeLookup (string prefix, string ns, string type, string member, out Node n)
{
n = null;
return null;
}
public virtual string RenderNamespaceLookup (string nsurl, out Node n)
{
n = null;
return null;
}
//
// Populates the index.
//
public virtual void PopulateIndex (IndexMaker index_maker)
{
}
//
// Build an html document
//
public static string BuildHtml (string css, string html_code)
{
return BuildHtml (css, null, html_code);
}
internal static string BuildHtml (string css, string js, string html_code) {
if (!FullHtml) {
return html_code;
}
StringWriter output = new StringWriter ();
output.Write ("
");
output.Write ("");
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly ();
Stream str_js = assembly.GetManifestResourceStream ("helper.js");
StringBuilder sb = new StringBuilder ((new StreamReader (str_js)).ReadToEnd());
output.Write ("\n");
if (js != null) {
output.Write ("");
}
output.Write ("");
output.Write (html_code);
output.Write ("");
return output.ToString ();
}
//
// Create different Documents for adding to Lucene search index
// The default action is do nothing. Subclasses should add the docs
//
public virtual void PopulateSearchableIndex (IndexWriter writer) {
return;
}
public void Message (TraceLevel level, string format, params object[] args)
{
if ((int) level <= (int) trace_level)
Console.WriteLine (format, args);
}
public void Error (string format, params object[] args)
{
Console.Error.WriteLine (format, args);
}
}
public abstract class Provider {
//
// This code is used to "tag" all the different sources
//
static short serial;
public int code;
public Provider ()
{
code = serial++;
}
public abstract void PopulateTree (Tree tree);
//
// Called at shutdown time after the tree has been populated to perform
// any fixups or final tasks.
//
public abstract void CloseTree (HelpSource hs, Tree tree);
}
public class RootTree : Tree {
string basedir;
public static ArrayList UncompiledHelpSources = new ArrayList();
public const int MonodocVersion = 1;
public static RootTree LoadTree ()
{
return LoadTree (null);
}
const string MacMonoDocDir = "/Library/Frameworks/Mono.framework/Versions/Current/lib/monodoc";
//
// Loads the tree layout
//
public static RootTree LoadTree (string basedir)
{
if (basedir == null) {
string myPath = System.Reflection.Assembly.GetExecutingAssembly ().Location;
string cfgFile = myPath + ".config";
if (!File.Exists (cfgFile)) {
basedir = ".";
}
else {
XmlDocument d = new XmlDocument ();
d.Load (cfgFile);
basedir = d.SelectSingleNode ("config/path").Attributes ["docsPath"].Value;
}
// Temporary workaround for developers distributing a monodoc.dll themselves on Mac
if (Directory.Exists (MacMonoDocDir)){
Console.WriteLine ("MacDir exists");
if (!File.Exists (Path.Combine (basedir, "monodoc.xml"))){
basedir = MacMonoDocDir;
}
}
}
//
// Load the layout
//
XmlDocument doc = new XmlDocument ();
string layout = Path.Combine (basedir, "monodoc.xml");
doc.Load (layout);
string osxExternalDir = "/Library/Frameworks/Mono.framework/External/monodoc";
string[] osxExternalSources = Directory.Exists (osxExternalDir)
? Directory.GetFiles (osxExternalDir, "*.source")
: new string[0];
return LoadTree (basedir, doc,
Directory.GetFiles (Path.Combine (basedir, "sources"), "*.source")
.Concat (osxExternalSources));
}
// Compatibility shim w/ Mono 2.6
public static RootTree LoadTree (string indexDir, XmlDocument docTree, IEnumerable sourceFiles)
{
return LoadTree (indexDir, docTree, sourceFiles.Cast());
}
public static RootTree LoadTree (string indexDir, XmlDocument docTree, IEnumerable sourceFiles)
{
if (docTree == null) {
docTree = new XmlDocument ();
using (var defTree = typeof(RootTree).Assembly.GetManifestResourceStream ("monodoc.xml"))
docTree.Load (defTree);
}
sourceFiles = sourceFiles ?? new string [0];
//
// Load the layout
//
RootTree root = new RootTree ();
root.basedir = indexDir;
XmlNodeList nodes = docTree.SelectNodes ("/node/node");
root.name_to_node ["root"] = root;
root.name_to_node ["libraries"] = root;
root.Populate (root, nodes);
Node third_party = root.LookupEntryPoint ("various");
if (third_party == null) {
Console.Error.WriteLine ("No 'various' doc node! Check monodoc.xml!");
third_party = root;
}
//
// Load the sources
//
foreach (var sourceFile in sourceFiles)
root.AddSourceFile (sourceFile);
foreach (string path in UncompiledHelpSources) {
EcmaUncompiledHelpSource hs = new EcmaUncompiledHelpSource(path);
hs.RootTree = root;
root.help_sources.Add (hs);
string epath = "extra-help-source-" + hs.Name;
Node hsn = root.CreateNode (hs.Name, "root:/" + epath);
root.name_to_hs [epath] = hs;
hsn.EnsureNodes ();
foreach (Node n in hs.Tree.Nodes){
hsn.AddNode (n);
}
}
// Clean the tree
PurgeNode(root);
root.Sort ();
return root;
}
public void AddSource (string sources_dir)
{
string [] files = Directory.GetFiles (sources_dir);
foreach (string file in files){
if (!file.EndsWith (".source"))
continue;
AddSourceFile (file);
}
}
Dictionary loadedSourceFiles = new Dictionary ();
public void AddSourceFile (string sourceFile)
{
if (loadedSourceFiles.ContainsKey (sourceFile))
return;
Node third_party = LookupEntryPoint ("various") ?? this;
XmlDocument doc = new XmlDocument ();
try {
doc.Load (sourceFile);
}
catch {
Console.Error.WriteLine ("Error: Could not load source file {0}", sourceFile);
return;
}
XmlNodeList extra_nodes = doc.SelectNodes ("/monodoc/node");
if (extra_nodes.Count > 0)
Populate (third_party, extra_nodes);
XmlNodeList sources = doc.SelectNodes ("/monodoc/source");
if (sources == null){
Console.Error.WriteLine ("Error: No