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 Monodoc.Providers
28 public class EcmaProvider : Provider
30 HashSet<string> directories = new HashSet<string> ();
32 public EcmaProvider ()
36 public EcmaProvider (string baseDir)
38 AddDirectory (baseDir);
41 public void AddDirectory (string directory)
43 if (string.IsNullOrEmpty (directory))
44 throw new ArgumentNullException ("directory");
46 directories.Add (directory);
49 public override void PopulateTree (Tree tree)
51 var storage = tree.HelpSource.Storage;
52 var nsSummaries = new Dictionary<string, XElement> ();
55 foreach (var asm in directories) {
56 var indexFilePath = Path.Combine (asm, "index.xml");
57 if (!File.Exists (indexFilePath)) {
58 Console.Error.WriteLine ("Warning: couldn't process directory `{0}' as it has no index.xml file", asm);
62 EcmaDoc.PopulateTreeFromIndexFile (indexFilePath, EcmaHelpSource.EcmaPrefix, tree, storage, nsSummaries, _ => resID++.ToString ());
65 foreach (var summary in nsSummaries)
66 storage.Store ("xml.summary." + summary.Key, summary.Value.ToString ());
68 var masterSummary = new XElement ("elements",
70 .SelectMany (d => Directory.EnumerateFiles (d, "ns-*.xml"))
71 .Select (ExtractNamespaceSummary));
72 storage.Store ("mastersummary.xml", masterSummary.ToString ());
75 XElement ExtractNamespaceSummary (string nsFile)
77 using (var reader = XmlReader.Create (nsFile)) {
78 reader.ReadToFollowing ("Namespace");
79 var name = reader.GetAttribute ("Name");
80 var summary = reader.ReadToFollowing ("summary") ? XElement.Load (reader.ReadSubtree ()) : new XElement ("summary");
81 var remarks = reader.ReadToFollowing ("remarks") ? XElement.Load (reader.ReadSubtree ()) : new XElement ("remarks");
83 return new XElement ("namespace",
84 new XAttribute ("ns", name ?? string.Empty),
90 public override void CloseTree (HelpSource hs, Tree tree)
93 AddExtensionMethods (hs);
96 void AddEcmaXml (HelpSource hs)
98 var xmls = directories
99 .SelectMany (Directory.EnumerateDirectories) // Assemblies
100 .SelectMany (Directory.EnumerateDirectories) // Namespaces
101 .SelectMany (Directory.EnumerateFiles)
102 .Where (f => f.EndsWith (".xml")); // Type XML files
105 foreach (var xml in xmls)
106 using (var file = File.OpenRead (xml))
107 hs.Storage.Store ((resID++).ToString (), file);
110 void AddImages (HelpSource hs)
112 var imgs = directories
113 .SelectMany (Directory.EnumerateDirectories)
114 .Select (d => Path.Combine (d, "_images"))
115 .Where (Directory.Exists)
116 .SelectMany (Directory.EnumerateFiles);
118 foreach (var img in imgs)
119 using (var file = File.OpenRead (img))
120 hs.Storage.Store (Path.GetFileName (img), file);
123 void AddExtensionMethods (HelpSource hs)
125 var extensionMethods = directories
126 .SelectMany (Directory.EnumerateDirectories)
127 .Select (d => Path.Combine (d, "index.xml"))
130 using (var file = File.OpenRead (f)) {
131 var reader = XmlReader.Create (file);
132 reader.ReadToFollowing ("ExtensionMethods");
133 return reader.ReadInnerXml ();
136 .DefaultIfEmpty (string.Empty);
138 hs.Storage.Store ("ExtensionMethods.xml",
139 "<ExtensionMethods>" + extensionMethods.Aggregate (string.Concat) + "</ExtensionMethods>");
142 IEnumerable<string> GetEcmaXmls ()
145 .SelectMany (Directory.EnumerateDirectories) // Assemblies
146 .SelectMany (Directory.EnumerateDirectories) // Namespaces
147 .SelectMany (Directory.EnumerateFiles)
148 .Where (f => f.EndsWith (".xml")); // Type XML files
152 public class EcmaHelpSource : HelpSource
154 internal const string EcmaPrefix = "ecma:";
155 LRUCache<string, Node> cache = new LRUCache<string, Node> (4);
157 public EcmaHelpSource (string base_file, bool create) : base (base_file, create)
161 protected EcmaHelpSource () : base ()
165 protected override string UriPrefix {
171 public override bool CanHandleUrl (string url)
173 if (url.Length > 2 && url[1] == ':') {
186 return base.CanHandleUrl (url);
189 // Clean the extra paramers in the id
190 public override Stream GetHelpStream (string id)
192 var idParts = id.Split ('?');
193 var name = idParts[0];
195 name = "mastersummary.xml";
196 return base.GetHelpStream (name);
199 public override Stream GetCachedHelpStream (string id)
201 var idParts = id.Split ('?');
202 return base.GetCachedHelpStream (idParts[0]);
205 public override DocumentType GetDocumentTypeForId (string id)
207 return DocumentType.EcmaXml;
210 public override string GetPublicUrl (Node node)
212 string url = string.Empty;
213 var type = EcmaDoc.GetNodeType (node);
214 //Console.WriteLine ("GetPublicUrl {0} : {1} [{2}]", node.Element, node.Caption, type.ToString ());
216 case EcmaNodeType.Namespace:
217 return node.Element; // A namespace node has already a well formated internal url
218 case EcmaNodeType.Type:
219 return MakeTypeNodeUrl (node);
220 case EcmaNodeType.Meta:
221 return MakeTypeNodeUrl (GetNodeTypeParent (node)) + GenerateMetaSuffix (node);
222 case EcmaNodeType.Member:
223 var typeChar = EcmaDoc.GetNodeMemberTypeChar (node);
224 var parentNode = GetNodeTypeParent (node);
225 var typeNode = MakeTypeNodeUrl (parentNode).Substring (2);
226 return typeChar + ":" + typeNode + MakeMemberNodeUrl (typeChar, node);
232 string MakeTypeNodeUrl (Node node)
234 // A Type node has a Element property of the form: 'ecma:{number}#{typename}/'
235 var hashIndex = node.Element.IndexOf ('#');
236 var typeName = node.Element.Substring (hashIndex + 1, node.Element.Length - hashIndex - 2);
237 return "T:" + node.Parent.Caption + '.' + typeName.Replace ('.', '+');
240 string MakeMemberNodeUrl (char typeChar, Node node)
242 // We clean inner type ctor name which may contain the outer type name
243 var caption = node.Caption;
245 // Sanitize constructor caption of inner types
246 if (typeChar == 'C') {
248 for (int i = 0; i < caption.Length && caption[i] != '('; i++)
249 lastDot = caption[i] == '.' ? i : lastDot;
250 return lastDot == -1 ? '.' + caption : caption.Substring (lastDot);
253 /* We handle type conversion operator by checking if the name contains " to "
254 * (as in 'foo to bar') and we generate a corresponding conversion signature
256 if (typeChar == 'O' && caption.IndexOf (" to ") != -1) {
257 var parts = caption.Split (' ');
258 return "." + node.Parent.Caption + "(" + parts[0] + ", " + parts[2] + ")";
261 /* The goal here is to treat method which are explicit interface definition
262 * such as 'void IDisposable.Dispose ()' for which the caption is a dot
263 * expression thus colliding with the ecma parser.
264 * If the first non-alpha character in the caption is a dot then we have an
265 * explicit member implementation (we assume the interface has namespace)
267 var firstNonAlpha = caption.FirstOrDefault (c => !char.IsLetterOrDigit (c));
268 if (firstNonAlpha == '.')
269 return "$" + caption;
271 return "." + caption;
274 Node GetNodeTypeParent (Node node)
276 // Type nodes are always at level 2 so we just need to get there
277 while (node != null && node.Parent != null
278 && !node.Parent.Parent.Element.StartsWith ("root:/", StringComparison.OrdinalIgnoreCase) && node.Parent.Parent.Parent != null)
283 string GenerateMetaSuffix (Node node)
285 string suffix = string.Empty;
286 // A meta node has always a type element to begin with
287 while (EcmaDoc.GetNodeType (node) != EcmaNodeType.Type) {
288 suffix = '/' + node.Element + suffix;
294 public override string GetInternalIdForUrl (string url, out Node node, out Dictionary<string, string> context)
296 var id = string.Empty;
300 if (!url.StartsWith (UriPrefix, StringComparison.OrdinalIgnoreCase)) {
301 node = MatchNode (url);
304 id = node.GetInternalUrl ();
308 id = GetInternalIdForInternalUrl (id, out hash);
309 context = EcmaDoc.GetContextForEcmaNode (hash, SourceID.ToString (), node);
314 public string GetInternalIdForInternalUrl (string internalUrl, out string hash)
316 var id = internalUrl;
317 if (id.StartsWith (UriPrefix, StringComparison.OrdinalIgnoreCase))
318 id = id.Substring (UriPrefix.Length);
319 else if (id.StartsWith ("N:", StringComparison.OrdinalIgnoreCase))
320 id = "xml.summary." + id.Substring ("N:".Length);
322 var hashIndex = id.IndexOf ('#');
324 if (hashIndex != -1) {
325 hash = id.Substring (hashIndex + 1);
326 id = id.Substring (0, hashIndex);
332 public override Node MatchNode (string url)
335 if ((node = cache.Get (url)) == null) {
336 node = EcmaDoc.MatchNodeWithEcmaUrl (url, Tree);
338 cache.Put (url, node);
343 public override void PopulateIndex (IndexMaker index_maker)
345 foreach (Node ns_node in Tree.RootNode.ChildNodes){
346 foreach (Node type_node in ns_node.ChildNodes){
347 string typename = type_node.Caption.Substring (0, type_node.Caption.IndexOf (' '));
348 string full = ns_node.Caption + "." + typename;
350 string doc_tag = GetKindFromCaption (type_node.Caption);
351 string url = type_node.PublicUrl;
354 // Add MonoMac/MonoTouch [Export] attributes, those live only in classes
356 XDocument type_doc = null;
357 ILookup<string, XElement> prematchedMembers = null;
358 bool hasExports = doc_tag == "Class" && (ns_node.Caption.StartsWith ("MonoTouch") || ns_node.Caption.StartsWith ("MonoMac"));
362 var id = GetInternalIdForInternalUrl (type_node.GetInternalUrl (), out hash);
363 type_doc = XDocument.Load (GetHelpStream (id));
364 prematchedMembers = type_doc.Root.Element ("Members").Elements ("Member").ToLookup (n => (string)n.Attribute ("MemberName"), n => n);
365 } catch (Exception e) {
366 Console.WriteLine ("Problem processing {0} for MonoTouch/MonoMac exports\n\n{0}", e);
371 if (doc_tag == "Class" || doc_tag == "Structure" || doc_tag == "Interface"){
372 index_maker.Add (type_node.Caption, typename, url);
373 index_maker.Add (full + " " + doc_tag, full, url);
375 foreach (Node c in type_node.ChildNodes){
378 index_maker.Add (" constructors", typename+"0", url + "/C");
381 index_maker.Add (" fields", typename+"1", url + "/F");
384 index_maker.Add (" events", typename+"2", url + "/E");
387 index_maker.Add (" properties", typename+"3", url + "/P");
390 index_maker.Add (" methods", typename+"4", url + "/M");
393 index_maker.Add (" operators", typename+"5", url + "/O");
399 // Now repeat, but use a different sort key, to make sure we come after
400 // the summary data above, start the counter at 6
402 string keybase = typename + "6.";
404 foreach (Node c in type_node.ChildNodes){
405 var type = c.Caption[0];
407 foreach (Node nc in c.ChildNodes) {
408 string res = nc.Caption;
409 string nurl = nc.PublicUrl;
412 if (hasExports && (type == 'C' || type == 'M' || type == 'P')) {
414 var member = GetMemberFromCaption (type_doc, type == 'C' ? ".ctor" : res, false, prematchedMembers);
415 var exports = member.Descendants ("AttributeName").Where (a => a.Value.Contains ("Foundation.Export"));
416 foreach (var exportNode in exports) {
417 var parts = exportNode.Value.Split ('"');
418 if (parts.Length != 3) {
419 Console.WriteLine ("Export attribute not found or not usable in {0}", exportNode);
421 var export = parts[1];
422 index_maker.Add (export + " selector", export, nurl);
425 } catch (Exception e) {
426 Console.WriteLine ("Problem processing {0}/{1} for MonoTouch/MonoMac exports\n\n{2}", nurl, res, e);
434 index_maker.Add (String.Format ("{0}.{1} field", typename, res),
435 keybase + res, nurl);
436 index_maker.Add (String.Format ("{0} field", res), res, nurl);
439 index_maker.Add (String.Format ("{0}.{1} event", typename, res),
440 keybase + res, nurl);
441 index_maker.Add (String.Format ("{0} event", res), res, nurl);
444 index_maker.Add (String.Format ("{0}.{1} property", typename, res),
445 keybase + res, nurl);
446 index_maker.Add (String.Format ("{0} property", res), res, nurl);
449 index_maker.Add (String.Format ("{0}.{1} method", typename, res),
450 keybase + res, nurl);
451 index_maker.Add (String.Format ("{0} method", res), res, nurl);
454 index_maker.Add (String.Format ("{0}.{1} operator", typename, res),
455 keybase + res, nurl);
460 } else if (doc_tag == "Enumeration"){
462 // Enumerations: add the enumeration values
464 index_maker.Add (type_node.Caption, typename, url);
465 index_maker.Add (full + " " + doc_tag, full, url);
467 // Now, pull the values.
469 var id = GetInternalIdForInternalUrl (type_node.GetInternalUrl (), out hash);
470 var xdoc = XDocument.Load (GetHelpStream (id));
474 var members = xdoc.Root.Element ("Members").Elements ("Members");
478 foreach (var member_node in members){
479 string enum_value = member_node.Attribute ("MemberName").Value;
480 string caption = enum_value + " value";
481 index_maker.Add (caption, caption, url);
483 } else if (doc_tag == "Delegate"){
484 index_maker.Add (type_node.Caption, typename, url);
485 index_maker.Add (full + " " + doc_tag, full, url);
492 public override void PopulateSearchableIndex (IndexWriter writer)
494 StringBuilder text = new StringBuilder ();
495 SearchableDocument searchDoc = new SearchableDocument ();
497 foreach (Node ns_node in Tree.RootNode.ChildNodes) {
498 foreach (Node type_node in ns_node.ChildNodes) {
499 string typename = type_node.Caption.Substring (0, type_node.Caption.IndexOf (' '));
500 string full = ns_node.Caption + "." + typename;
501 string url = type_node.PublicUrl;
502 string doc_tag = GetKindFromCaption (type_node.Caption);
504 var id = GetInternalIdForInternalUrl (type_node.GetInternalUrl (), out hash);
505 var xdoc = XDocument.Load (GetHelpStream (id));
508 if (string.IsNullOrEmpty (doc_tag))
511 // For classes, structures or interfaces add a doc for the overview and
512 // add a doc for every constructor, method, event, ...
513 // doc_tag == "Class" || doc_tag == "Structure" || doc_tag == "Interface"
514 if (doc_tag[0] == 'C' || doc_tag[0] == 'S' || doc_tag[0] == 'I') {
515 // Adds a doc for every overview of every type
516 SearchableDocument doc = searchDoc.Reset ();
517 doc.Title = type_node.Caption;
518 doc.HotText = typename;
520 doc.FullTitle = full;
522 var node_sel = xdoc.Root.Element ("Docs");
524 GetTextFromNode (node_sel, text);
525 doc.Text = text.ToString ();
528 GetExamples (node_sel, text);
529 doc.Examples = text.ToString ();
531 writer.AddDocument (doc.LuceneDoc);
532 var exportParsable = doc_tag[0] == 'C' && (ns_node.Caption.StartsWith ("MonoTouch") || ns_node.Caption.StartsWith ("MonoMac"));
534 //Add docs for contructors, methods, etc.
535 foreach (Node c in type_node.ChildNodes) { // c = Constructors || Fields || Events || Properties || Methods || Operators
536 if (c.Element == "*")
538 const float innerTypeBoost = 0.2f;
540 IEnumerable<Node> ncnodes = c.ChildNodes;
541 // The rationale is that we need to properly handle method overloads
542 // so for those method node which have children, flatten them
543 if (c.Caption == "Methods") {
545 .Where (n => n.ChildNodes == null || n.ChildNodes.Count == 0)
546 .Concat (ncnodes.Where (n => n.ChildNodes.Count > 0).SelectMany (n => n.ChildNodes));
547 } else if (c.Caption == "Operators") {
549 .Where (n => !n.Caption.EndsWith ("Conversion"))
550 .Concat (ncnodes.Where (n => n.Caption.EndsWith ("Conversion")).SelectMany (n => n.ChildNodes));
553 var prematchedMembers = xdoc.Root.Element ("Members").Elements ("Member").ToLookup (n => (string)n.Attribute ("MemberName"), n => n);
555 foreach (Node nc in ncnodes) {
556 XElement docsNode = null;
558 docsNode = GetDocsFromCaption (xdoc, c.Caption[0] == 'C' ? ".ctor" : nc.Caption, c.Caption[0] == 'O', prematchedMembers);
560 if (docsNode == null) {
561 Console.Error.WriteLine ("Problem: {0}", nc.PublicUrl);
565 SearchableDocument doc_nod = searchDoc.Reset ();
566 doc_nod.Title = LargeName (nc) + " " + EcmaDoc.EtcKindToCaption (c.Caption[0]);
567 doc_nod.FullTitle = ns_node.Caption + '.' + typename + "::" + nc.Caption;
568 doc_nod.HotText = string.Empty;
570 /* Disable constructors hottext indexing as it's often "polluting" search queries
571 because it has the same hottext than standard types */
572 if (c.Caption != "Constructors") {
573 //dont add the parameters to the hottext
574 int ppos = nc.Caption.IndexOf ('(');
575 doc_nod.HotText = ppos != -1 ? nc.Caption.Substring (0, ppos) : nc.Caption;
578 var urlnc = nc.PublicUrl;
582 GetTextFromNode (docsNode, text);
583 doc_nod.Text = text.ToString ();
586 GetExamples (docsNode, text);
587 doc_nod.Examples = text.ToString ();
589 Document lucene_doc = doc_nod.LuceneDoc;
590 lucene_doc.Boost = innerTypeBoost;
591 writer.AddDocument (lucene_doc);
593 // Objective-C binding specific parsing of [Export] attributes
594 if (exportParsable) {
596 var exports = docsNode.Parent.Elements ("Attributes").Elements ("Attribute").Elements ("AttributeName")
597 .Select (a => (string)a).Where (txt => txt.Contains ("Foundation.Export"));
599 foreach (var exportNode in exports) {
600 var parts = exportNode.Split ('"');
601 if (parts.Length != 3) {
602 Console.WriteLine ("Export attribute not found or not usable in {0}", exportNode);
606 var export = parts[1];
607 var export_node = searchDoc.Reset ();
608 export_node.Title = export + " Export";
609 export_node.FullTitle = ns_node.Caption + '.' + typename + "::" + export;
610 export_node.Url = urlnc;
611 export_node.HotText = export;
612 export_node.Text = string.Empty;
613 export_node.Examples = string.Empty;
614 lucene_doc = export_node.LuceneDoc;
615 lucene_doc.Boost = innerTypeBoost;
616 writer.AddDocument (lucene_doc);
618 } catch (Exception e){
619 Console.WriteLine ("Problem processing {0} for MonoTouch/MonoMac exports\n\n{0}", e);
624 // doc_tag == "Enumeration"
625 } else if (doc_tag[0] == 'E'){
626 var members = xdoc.Root.Element ("Members").Elements ("Member");
631 foreach (var member_node in members) {
632 string enum_value = (string)member_node.Attribute ("MemberName");
633 text.Append (enum_value);
635 GetTextFromNode (member_node.Element ("Docs"), text);
639 SearchableDocument doc = searchDoc.Reset ();
642 GetExamples (xdoc.Root.Element ("Docs"), text);
643 doc.Examples = text.ToString ();
645 doc.Title = type_node.Caption;
646 doc.HotText = (string)xdoc.Root.Attribute ("Name");
647 doc.FullTitle = full;
649 doc.Text = text.ToString();
650 writer.AddDocument (doc.LuceneDoc);
651 // doc_tag == "Delegate"
652 } else if (doc_tag[0] == 'D'){
653 SearchableDocument doc = searchDoc.Reset ();
654 doc.Title = type_node.Caption;
655 doc.HotText = (string)xdoc.Root.Attribute ("Name");
656 doc.FullTitle = full;
659 var node_sel = xdoc.Root.Element ("Docs");
662 GetTextFromNode (node_sel, text);
663 doc.Text = text.ToString();
666 GetExamples (node_sel, text);
667 doc.Examples = text.ToString();
669 writer.AddDocument (doc.LuceneDoc);
675 string GetKindFromCaption (string s)
677 int p = s.LastIndexOf (' ');
679 return s.Substring (p + 1);
683 // Extract the interesting text from the docs node
684 void GetTextFromNode (XElement n, StringBuilder sb)
686 // Include the text content of the docs
687 sb.AppendLine (n.Value);
688 foreach (var tag in n.Descendants ())
689 //include the url to which points the see tag and the name of the parameter
690 if ((tag.Name.LocalName.Equals ("see", StringComparison.Ordinal) || tag.Name.LocalName.Equals ("paramref", StringComparison.Ordinal))
691 && tag.HasAttributes)
692 sb.AppendLine ((string)tag.Attributes ().First ());
695 // Extract the code nodes from the docs
696 void GetExamples (XElement n, StringBuilder sb)
698 foreach (var code in n.Descendants ("code"))
699 sb.Append ((string)code);
702 // Extract a large name for the Node
703 static string LargeName (Node matched_node)
705 string[] parts = matched_node.GetInternalUrl ().Split('/', '#');
706 if (parts.Length == 3 && parts[2] != String.Empty) //List of Members, properties, events, ...
707 return parts[1] + ": " + matched_node.Caption;
708 else if(parts.Length >= 4) //Showing a concrete Member, property, ...
709 return parts[1] + "." + matched_node.Caption;
711 return matched_node.Caption;
714 XElement GetMemberFromCaption (XDocument xdoc, string caption, bool isOperator, ILookup<string, XElement> prematchedMembers)
718 var doc = xdoc.Root.Element ("Members").Elements ("Member");
721 // The first case are explicit and implicit conversion operators which are grouped specifically
722 if (caption.IndexOf (" to ") != -1) {
723 var convArgs = caption.Split (new[] { " to " }, StringSplitOptions.None);
725 .First (n => (AttrEq (n, "MemberName", "op_Explicit") || AttrEq (n, "MemberName", "op_Implicit"))
726 && ((string)n.Element ("ReturnValue").Element ("ReturnType")).Equals (convArgs[1], StringComparison.Ordinal)
727 && AttrEq (n.Element ("Parameters").Element ("Parameter"), "Type", convArgs[0]));
729 return doc.First (m => AttrEq (m, "MemberName", "op_" + caption));
733 TryParseCaption (caption, out name, out args);
735 if (!string.IsNullOrEmpty (name)) { // Filter member by name
736 var prematched = prematchedMembers[name];
737 doc = prematched.Any () ? prematched : doc.Where (m => AttrEq (m, "MemberName", name));
739 if (args != null && args.Count > 0) // Filter member by its argument list
740 doc = doc.Where (m => m.Element ("Parameters").Elements ("Parameter").Attributes ("Type").Select (a => (string)a).SequenceEqual (args));
745 XElement GetDocsFromCaption (XDocument xdoc, string caption, bool isOperator, ILookup<string, XElement> prematchedMembers)
747 return GetMemberFromCaption (xdoc, caption, isOperator, prematchedMembers).Element ("Docs");
750 // A simple stack-based parser to detect single type definition separated by commas
751 IEnumerable<string> ExtractArguments (string rawArgList)
753 var sb = new System.Text.StringBuilder ();
754 int genericDepth = 0;
757 for (int i = 0; i < rawArgList.Length; i++) {
758 char c = rawArgList[i];
761 if (genericDepth == 0 && arrayDepth == 0) {
762 yield return sb.ToString ();
783 yield return sb.ToString ();
786 void TryParseCaption (string caption, out string name, out IList<string> argList)
790 int parenIdx = caption.IndexOf ('(');
791 // In case of simple name, there is no need for processing
792 if (parenIdx == -1) {
796 name = caption.Substring (0, parenIdx);
797 // Now we gather the argument list if there is any
798 var rawArgList = caption.Substring (parenIdx + 1, caption.Length - parenIdx - 2); // Only take what's inside the parens
799 if (string.IsNullOrEmpty (rawArgList))
802 argList = ExtractArguments (rawArgList).Select (arg => arg.Trim ()).ToList ();
805 bool AttrEq (XElement element, string attributeName, string expectedValue)
807 return ((string)element.Attribute (attributeName)).Equals (expectedValue, StringComparison.Ordinal);