2 // The ecmaspec provider is for ECMA specifications
5 // John Luke (jluke@cfl.rr.com)
6 // Ben Maurer (bmaurer@users.sourceforge.net)
9 // mono assembler.exe --ecmaspec DIRECTORY --out name
17 using System.Xml.Linq;
18 using System.Collections.Generic;
20 using Lucene.Net.Index;
21 using Lucene.Net.Documents;
26 namespace MonkeyDoc.Providers
28 public enum EcmaNodeType {
33 Meta, // A node that's here to serve as a header for other node
36 public class EcmaProvider : Provider
38 HashSet<string> directories = new HashSet<string> ();
40 public EcmaProvider ()
44 public EcmaProvider (string baseDir)
46 AddDirectory (baseDir);
49 public void AddDirectory (string directory)
51 if (string.IsNullOrEmpty (directory))
52 throw new ArgumentNullException ("directory");
54 directories.Add (directory);
57 public override void PopulateTree (Tree tree)
59 var root = tree.RootNode;
60 var storage = tree.HelpSource.Storage;
62 var nsSummaries = new Dictionary<string, XElement> ();
64 foreach (var asm in directories) {
65 var indexFilePath = Path.Combine (asm, "index.xml");
66 if (!File.Exists (indexFilePath)) {
67 Console.Error.WriteLine ("Warning: couldn't process directory `{0}' as it has no index.xml file", asm);
70 using (var reader = XmlReader.Create (File.OpenRead (indexFilePath))) {
71 reader.ReadToFollowing ("Types");
72 var types = XElement.Load (reader.ReadSubtree ());
74 foreach (var ns in types.Elements ("Namespace")) {
75 var nsName = (string)ns.Attribute ("Name");
76 nsName = !string.IsNullOrEmpty (nsName) ? nsName : "global";
77 var nsNode = root.GetOrCreateNode (nsName, "N:" + nsName);
80 if (!nsSummaries.TryGetValue (nsName, out nsElements))
81 nsSummaries[nsName] = nsElements = new XElement ("elements",
82 new XElement ("summary"),
83 new XElement ("remarks"));
85 foreach (var type in ns.Elements ("Type")) {
86 // Add the XML file corresponding to the type to our storage
88 var typeFilePath = Path.Combine (asm, nsName, Path.ChangeExtension (type.Attribute ("Name").Value, ".xml"));
89 if (!File.Exists (typeFilePath)) {
90 Console.Error.WriteLine ("Warning: couldn't process type file `{0}' as it doesn't exist", typeFilePath);
93 using (var file = File.OpenRead (typeFilePath))
94 storage.Store (id.ToString (), file);
95 nsElements.Add (ExtractClassSummary (typeFilePath));
96 var typeDocument = XDocument.Load (typeFilePath);
98 var typeCaption = ((string)(type.Attribute ("DisplayName") ?? type.Attribute ("Name"))).Replace ('+', '.');
99 var url = "ecma:" + id + '#' + typeCaption + '/';
100 typeCaption += " " + (string)type.Attribute ("Kind");
101 var typeNode = nsNode.CreateNode (typeCaption, url);
103 // Add meta "Members" node
104 typeNode.CreateNode ("Members", "*");
105 var membersNode = typeDocument.Root.Element ("Members");
106 if (membersNode == null || !membersNode.Elements ().Any ())
108 var members = membersNode
110 .ToLookup (m => m.Attribute ("MemberName").Value.StartsWith ("op_") ? "Operator" : m.Element ("MemberType").Value);
112 foreach (var memberType in members) {
113 // We pluralize the member type to get the caption and take the first letter as URL
114 var node = typeNode.CreateNode (PluralizeMemberType (memberType.Key), memberType.Key[0].ToString ());
117 var isCtors = memberType.Key[0] == 'C';
119 // We do not escape much member name here
120 foreach (var memberGroup in memberType.GroupBy (m => MakeMemberCaption (m, isCtors))) {
121 if (memberGroup.Count () > 1) {
123 var overloadCaption = MakeMemberCaption (memberGroup.First (), false);
124 var overloadNode = node.CreateNode (overloadCaption, overloadCaption);
125 foreach (var member in memberGroup)
126 overloadNode.CreateNode (MakeMemberCaption (member, true), (memberIndex++).ToString ());
127 overloadNode.Sort ();
129 // We treat constructor differently by showing their argument list in all cases
130 node.CreateNode (MakeMemberCaption (memberGroup.First (), isCtors), (memberIndex++).ToString ());
143 foreach (var summary in nsSummaries)
144 storage.Store ("xml.summary." + summary.Key, summary.Value.ToString ());
146 var masterSummary = new XElement ("elements",
148 .SelectMany (d => Directory.EnumerateFiles (d, "ns-*.xml"))
149 .Select (ExtractNamespaceSummary));
150 storage.Store ("mastersummary.xml", masterSummary.ToString ());
153 string PluralizeMemberType (string memberType)
155 switch (memberType) {
159 return memberType + "s";
163 string MakeMemberCaption (XElement member, bool withArguments)
165 var caption = (string)member.Attribute ("MemberName");
166 // Use type name instead of .ctor for cosmetic sake
167 if (caption == ".ctor") {
168 caption = (string)member.Ancestors ("Type").First ().Attribute ("Name");
169 // If this is an inner type ctor, strip the parent type reference
170 var plusIndex = caption.LastIndexOf ('+');
172 caption = caption.Substring (plusIndex + 1);
174 if (caption.StartsWith ("op_")) {
176 caption = MakeOperatorSignature (member, out sig);
177 caption = withArguments ? sig : caption;
181 var args = member.Element ("Parameters");
183 if (args != null && args.Elements ("Parameter").Any ()) {
184 caption += args.Elements ("Parameter")
185 .Select (p => (string)p.Attribute ("Type"))
186 .Aggregate ((p1, p2) => p1 + "," + p2);
194 XElement ExtractClassSummary (string typeFilePath)
196 using (var reader = XmlReader.Create (typeFilePath)) {
197 reader.ReadToFollowing ("Type");
198 var name = reader.GetAttribute ("Name");
199 var fullName = reader.GetAttribute ("FullName");
200 reader.ReadToFollowing ("AssemblyName");
201 var assemblyName = reader.ReadElementString ();
202 reader.ReadToFollowing ("summary");
203 var summary = reader.ReadInnerXml ();
204 reader.ReadToFollowing ("remarks");
205 var remarks = reader.ReadInnerXml ();
207 return new XElement ("class",
208 new XAttribute ("name", name ?? string.Empty),
209 new XAttribute ("fullname", fullName ?? string.Empty),
210 new XAttribute ("assembly", assemblyName ?? string.Empty),
211 new XElement ("summary", new XCData (summary)),
212 new XElement ("remarks", new XCData (remarks)));
216 XElement ExtractNamespaceSummary (string nsFile)
218 using (var reader = XmlReader.Create (nsFile)) {
219 reader.ReadToFollowing ("Namespace");
220 var name = reader.GetAttribute ("Name");
221 reader.ReadToFollowing ("summary");
222 var summary = reader.ReadInnerXml ();
223 reader.ReadToFollowing ("remarks");
224 var remarks = reader.ReadInnerXml ();
226 return new XElement ("namespace",
227 new XAttribute ("ns", name ?? string.Empty),
228 new XElement ("summary", new XCData (summary)),
229 new XElement ("remarks", new XCData (remarks)));
233 public override void CloseTree (HelpSource hs, Tree tree)
236 AddExtensionMethods (hs);
239 void AddEcmaXml (HelpSource hs)
241 var xmls = directories
242 .SelectMany (Directory.EnumerateDirectories) // Assemblies
243 .SelectMany (Directory.EnumerateDirectories) // Namespaces
244 .SelectMany (Directory.EnumerateFiles)
245 .Where (f => f.EndsWith (".xml")); // Type XML files
248 foreach (var xml in xmls)
249 using (var file = File.OpenRead (xml))
250 hs.Storage.Store ((resID++).ToString (), file);
253 void AddImages (HelpSource hs)
255 var imgs = directories
256 .SelectMany (Directory.EnumerateDirectories)
257 .Select (d => Path.Combine (d, "_images"))
258 .Where (Directory.Exists)
259 .SelectMany (Directory.EnumerateFiles);
261 foreach (var img in imgs)
262 using (var file = File.OpenRead (img))
263 hs.Storage.Store (Path.GetFileName (img), file);
266 void AddExtensionMethods (HelpSource hs)
268 var extensionMethods = directories
269 .SelectMany (Directory.EnumerateDirectories)
270 .Select (d => Path.Combine (d, "index.xml"))
273 using (var file = File.OpenRead (f)) {
274 var reader = XmlReader.Create (file);
275 reader.ReadToFollowing ("ExtensionMethods");
276 return reader.ReadInnerXml ();
279 .DefaultIfEmpty (string.Empty);
281 hs.Storage.Store ("ExtensionMethods.xml",
282 "<ExtensionMethods>" + extensionMethods.Aggregate (string.Concat) + "</ExtensionMethods>");
285 IEnumerable<string> GetEcmaXmls ()
288 .SelectMany (Directory.EnumerateDirectories) // Assemblies
289 .SelectMany (Directory.EnumerateDirectories) // Namespaces
290 .SelectMany (Directory.EnumerateFiles)
291 .Where (f => f.EndsWith (".xml")); // Type XML files
294 string MakeOperatorSignature (XElement member, out string memberSignature)
296 string name = (string)member.Attribute ("MemberName");
297 var nicename = name.Substring(3);
298 memberSignature = null;
301 // unary operators: no overloading possible [ECMA-335 §10.3.1]
302 case "op_UnaryPlus": // static R operator+ (T)
303 case "op_UnaryNegation": // static R operator- (T)
304 case "op_LogicalNot": // static R operator! (T)
305 case "op_OnesComplement": // static R operator~ (T)
306 case "op_Increment": // static R operator++ (T)
307 case "op_Decrement": // static R operator-- (T)
308 case "op_True": // static bool operator true (T)
309 case "op_False": // static bool operator false (T)
310 case "op_AddressOf": // static R operator& (T)
311 case "op_PointerDereference": // static R operator* (T)
312 memberSignature = nicename;
314 // conversion operators: overloading based on parameter and return type [ECMA-335 §10.3.3]
315 case "op_Implicit": // static implicit operator R (T)
316 case "op_Explicit": // static explicit operator R (T)
317 nicename = name.EndsWith ("Implicit") ? "ImplicitConversion" : "ExplicitConversion";
318 string arg = (string)member.Element ("Parameters").Element ("Parameter").Attribute ("Type");
319 string ret = (string)member.Element ("ReturnValue").Element ("ReturnType");
320 memberSignature = arg + " to " + ret;
322 // binary operators: overloading is possible [ECMA-335 §10.3.2]
326 + string.Join (",", member.Element ("Parameters").Elements ("Parameter").Select (p => (string)p.Attribute ("Type")))
335 public class EcmaHelpSource : HelpSource
337 const string EcmaPrefix = "ecma:";
338 EcmaUrlParser parser = new EcmaUrlParser ();
339 LRUCache<string, Node> cache = new LRUCache<string, Node> (4);
341 public EcmaHelpSource (string base_file, bool create) : base (base_file, create)
345 protected override string UriPrefix {
351 public override bool CanHandleUrl (string url)
353 if (url.Length > 2 && url[1] == ':') {
366 return base.CanHandleUrl (url);
369 // Clean the extra paramers in the id
370 public override Stream GetHelpStream (string id)
372 var idParts = id.Split ('?');
373 return base.GetHelpStream (idParts[0]);
376 public override Stream GetCachedHelpStream (string id)
378 var idParts = id.Split ('?');
379 return base.GetCachedHelpStream (idParts[0]);
382 public override DocumentType GetDocumentTypeForId (string id, out Dictionary<string, string> extraParams)
385 int interMark = id.LastIndexOf ('?');
387 extraParams = id.Substring (interMark)
390 var eqIdx = nvp.IndexOf ('=');
391 return new { Key = nvp.Substring (0, eqIdx < 0 ? nvp.Length : eqIdx), Value = nvp.Substring (eqIdx + 1) };
393 .ToDictionary (kvp => kvp.Key, kvp => kvp.Value );
394 return DocumentType.EcmaXml;
397 public override string GetPublicUrl (Node node)
399 string url = string.Empty;
400 var type = GetNodeType (node);
401 //Console.WriteLine ("GetPublicUrl {0} : {1} [{2}]", node.Element, node.Caption, type.ToString ());
403 case EcmaNodeType.Namespace:
404 return node.Element; // A namespace node has already a well formated internal url
405 case EcmaNodeType.Type:
406 return MakeTypeNodeUrl (node);
407 case EcmaNodeType.Meta:
408 return MakeTypeNodeUrl (GetNodeTypeParent (node)) + GenerateMetaSuffix (node);
409 case EcmaNodeType.Member:
410 var typeChar = GetNodeMemberTypeChar (node);
411 var parentNode = GetNodeTypeParent (node);
412 var typeNode = MakeTypeNodeUrl (parentNode).Substring (2);
413 return typeChar + ":" + typeNode + MakeMemberNodeUrl (typeChar, node);
419 string MakeTypeNodeUrl (Node node)
421 // A Type node has a Element property of the form: 'ecma:{number}#{typename}/'
422 var hashIndex = node.Element.IndexOf ('#');
423 var typeName = node.Element.Substring (hashIndex + 1, node.Element.Length - hashIndex - 2);
424 return "T:" + node.Parent.Caption + '.' + typeName.Replace ('.', '+');
427 string MakeMemberNodeUrl (char typeChar, Node node)
429 // We clean inner type ctor name which may contain the outer type name
430 var caption = node.Caption;
432 // Sanitize constructor caption of inner types
433 if (typeChar == 'C') {
435 for (int i = 0; i < caption.Length && caption[i] != '('; i++)
436 lastDot = caption[i] == '.' ? i : lastDot;
437 return lastDot == -1 ? '.' + caption : caption.Substring (lastDot);
440 /* We handle type conversion operator by checking if the name contains " to "
441 * (as in 'foo to bar') and we generate a corresponding conversion signature
443 if (typeChar == 'O' && caption.IndexOf (" to ") != -1) {
444 var parts = caption.Split (' ');
445 return "." + node.Parent.Caption + "(" + parts[0] + ", " + parts[2] + ")";
448 /* The goal here is to treat method which are explicit interface definition
449 * such as 'void IDisposable.Dispose ()' for which the caption is a dot
450 * expression thus colliding with the ecma parser.
451 * If the first non-alpha character in the caption is a dot then we have an
452 * explicit member implementation (we assume the interface has namespace)
454 var firstNonAlpha = caption.FirstOrDefault (c => !char.IsLetterOrDigit (c));
455 if (firstNonAlpha == '.')
456 return "$" + caption;
458 return "." + caption;
461 EcmaNodeType GetNodeType (Node node)
463 // We guess the node type by checking the depth level it's at in the tree
464 int level = GetNodeLevel (node);
467 return EcmaNodeType.Namespace;
469 return EcmaNodeType.Type;
471 return EcmaNodeType.Meta;
472 case 3: // Here it's either a member or, in case of overload, a meta
473 return node.IsLeaf ? EcmaNodeType.Member : EcmaNodeType.Meta;
474 case 4: // At this level, everything is necessarily a member
475 return EcmaNodeType.Member;
477 return EcmaNodeType.Invalid;
481 int GetNodeLevel (Node node)
484 for (; !node.Element.StartsWith ("root:/", StringComparison.OrdinalIgnoreCase); i++) {
485 //Console.WriteLine ("\tLevel {0} : {1} {2}", i, node.Element, node.Caption);
491 char GetNodeMemberTypeChar (Node node)
493 int level = GetNodeLevel (node);
494 // We try to reach the member group node depending on node nested level
497 return node.Element[0];
499 return node.Parent.Element[0];
501 return node.Parent.Parent.Element[0];
503 throw new ArgumentException ("node", "Couldn't determine member type of node `" + node.Caption + "'");
507 Node GetNodeTypeParent (Node node)
509 // Type nodes are always at level 2 so we just need to get there
510 while (node != null && node.Parent != null && !node.Parent.Parent.Element.StartsWith ("root:/", StringComparison.OrdinalIgnoreCase))
515 string GenerateMetaSuffix (Node node)
517 string suffix = string.Empty;
518 // A meta node has always a type element to begin with
519 while (GetNodeType (node) != EcmaNodeType.Type) {
520 suffix = '/' + node.Element + suffix;
526 public override string GetInternalIdForUrl (string url, out Node node)
528 var id = string.Empty;
531 if (!url.StartsWith (EcmaPrefix, StringComparison.OrdinalIgnoreCase)) {
532 node = MatchNode (url);
535 id = node.GetInternalUrl ();
538 if (id.StartsWith (UriPrefix, StringComparison.OrdinalIgnoreCase))
539 id = id.Substring (UriPrefix.Length);
540 else if (id.StartsWith ("N:", StringComparison.OrdinalIgnoreCase))
541 id = "xml.summary." + id.Substring ("N:".Length);
543 var hashIndex = id.IndexOf ('#');
544 var hash = string.Empty;
545 if (hashIndex != -1) {
546 hash = id.Substring (hashIndex + 1);
547 id = id.Substring (0, hashIndex);
550 return id + GetArgs (hash, node);
553 public override Node MatchNode (string url)
556 if ((node = cache.Get (url)) == null) {
557 node = InternalMatchNode (url);
559 cache.Put (url, node);
564 public Node InternalMatchNode (string url)
568 if (!parser.TryParse (url, out desc))
572 Node currentNode = Tree.RootNode;
573 Node searchNode = new Node () { Caption = desc.Namespace };
574 int index = currentNode.Nodes.BinarySearch (searchNode, EcmaGenericNodeComparer.Instance);
576 result = currentNode.Nodes[index];
577 if (desc.DescKind == EcmaDesc.Kind.Namespace || index < 0)
581 currentNode = result;
583 searchNode.Caption = desc.ToCompleteTypeName ();
584 index = currentNode.Nodes.BinarySearch (searchNode, EcmaTypeNodeComparer.Instance);
586 result = currentNode.Nodes[index];
587 if ((desc.DescKind == EcmaDesc.Kind.Type && !desc.IsEtc) || index < 0)
591 currentNode = result;
593 var caption = desc.IsEtc ? EtcKindToCaption (desc.Etc) : MemberKindToCaption (desc.DescKind);
594 currentNode = FindNodeForCaption (currentNode.Nodes, caption);
595 if (currentNode == null
596 || (desc.IsEtc && desc.DescKind == EcmaDesc.Kind.Type && string.IsNullOrEmpty (desc.EtcFilter)))
601 var format = desc.DescKind == EcmaDesc.Kind.Constructor ? EcmaDesc.Format.WithArgs : EcmaDesc.Format.WithoutArgs;
602 searchNode.Caption = desc.ToCompleteMemberName (format);
603 index = currentNode.Nodes.BinarySearch (searchNode, EcmaGenericNodeComparer.Instance);
606 result = currentNode.Nodes[index];
607 if (result.Nodes.Count == 0 || desc.IsEtc)
611 currentNode = result;
612 searchNode.Caption = desc.ToCompleteMemberName (EcmaDesc.Format.WithArgs);
613 index = currentNode.Nodes.BinarySearch (searchNode, EcmaGenericNodeComparer.Instance);
616 result = result.Nodes[index];
621 // This comparer returns the answer straight from caption comparison
622 class EcmaGenericNodeComparer : IComparer<Node>
624 public static readonly EcmaGenericNodeComparer Instance = new EcmaGenericNodeComparer ();
626 public int Compare (Node n1, Node n2)
628 return string.Compare (n1.Caption, n2.Caption, StringComparison.Ordinal);
632 // This comparer take into account the space in the caption
633 class EcmaTypeNodeComparer : IComparer<Node>
635 public static readonly EcmaTypeNodeComparer Instance = new EcmaTypeNodeComparer ();
637 public int Compare (Node n1, Node n2)
639 int length1 = CaptionLength (n1.Caption);
640 int length2 = CaptionLength (n2.Caption);
642 return string.Compare (n1.Caption, 0, n2.Caption, 0, Math.Max (length1, length2), StringComparison.Ordinal);
645 int CaptionLength (string caption)
647 var length = caption.LastIndexOf (' ');
648 return length == -1 ? caption.Length : length;
652 string EtcKindToCaption (char etc)
660 return "Constructors";
674 string MemberKindToCaption (EcmaDesc.Kind kind)
677 case EcmaDesc.Kind.Method:
679 case EcmaDesc.Kind.Property:
681 case EcmaDesc.Kind.Constructor:
682 return "Constructors";
683 case EcmaDesc.Kind.Field:
685 case EcmaDesc.Kind.Event:
687 case EcmaDesc.Kind.Operator:
694 Node FindNodeForCaption (List<Node> nodes, string caption)
696 foreach (var node in nodes)
697 if (node.Caption.Equals (caption, StringComparison.OrdinalIgnoreCase))
702 string GetArgs (string hash, Node node)
704 var args = new Dictionary<string, string> ();
706 args["source-id"] = SourceID.ToString ();
709 var nodeType = GetNodeType (node);
711 case EcmaNodeType.Namespace:
712 args["show"] = "namespace";
713 args["namespace"] = node.Element.Substring ("N:".Length);
715 case EcmaNodeType.Type:
716 args["show"] = "typeoverview";
718 case EcmaNodeType.Member:
719 case EcmaNodeType.Meta:
720 switch (GetNodeMemberTypeChar (node)){
722 args["membertype"] = "Constructor";
725 args["membertype"] = "Method";
728 args["membertype"] = "Property";
731 args["membertype"] = "Field";
734 args["membertype"] = "Event";
737 args["membertype"] = "Operator";
740 args["membertype"] = "ExtensionMethod";
743 args["membertype"] = "All";
747 if (nodeType == EcmaNodeType.Meta) {
748 args["show"] = "members";
749 args["index"] = "all";
751 args["show"] = "member";
752 args["index"] = node.Element;
758 if (!string.IsNullOrEmpty (hash))
761 return "?" + string.Join ("&", args.Select (kvp => kvp.Key == kvp.Value ? kvp.Key : kvp.Key + '=' + kvp.Value));