Editor/HotCodeEditor/Navigation/DocComment.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
/// <summary>
/// The readable parts of an XML doc comment (<c>/// <summary></c> etc).
/// </summary>
public record DocComment( string Summary, IReadOnlyDictionary<string, string> Parameters, string Returns )
{
public static readonly DocComment Empty = new( "", new Dictionary<string, string>(), "" );
/// <summary>
/// Parses what Roslyn's GetDocumentationCommentXml returns. Never throws; bad XML gives <see cref="Empty"/>.
/// </summary>
public static DocComment Parse( string xml )
{
if ( string.IsNullOrWhiteSpace( xml ) ) return Empty;
XElement root;
try
{
// Source comments come wrapped in <member>; metadata ones sometimes aren't wrapped at all
root = XElement.Parse( $"<root>{xml}</root>", LoadOptions.PreserveWhitespace );
}
catch ( XmlException )
{
return Empty;
}
var summary = root.Descendants( "summary" ).FirstOrDefault();
var returns = root.Descendants( "returns" ).FirstOrDefault();
var parameters = new Dictionary<string, string>();
foreach ( var param in root.Descendants( "param" ) )
{
var name = (string)param.Attribute( "name" );
if ( !string.IsNullOrEmpty( name ) ) parameters[name] = TextOf( param );
}
return new DocComment( TextOf( summary ), parameters, TextOf( returns ) );
}
/// <summary>
/// Element text with <see cref="T:Foo.Bar"/> shown as "Bar", <paramref name="x"/> as "x",
/// and whitespace collapsed (doc comments are full of indentation).
/// </summary>
private static string TextOf( XElement element )
{
if ( element is null ) return "";
var sb = new StringBuilder();
Append( element, sb );
return Regex.Replace( sb.ToString(), @"\s+", " " ).Trim();
}
private static void Append( XElement element, StringBuilder sb )
{
foreach ( var node in element.Nodes() )
{
switch ( node )
{
case XText text:
sb.Append( text.Value );
break;
case XElement child when child.Name == "see" || child.Name == "seealso":
var target = (string)child.Attribute( "cref" ) ?? (string)child.Attribute( "langword" ) ?? (string)child.Attribute( "href" ) ?? "";
sb.Append( child.IsEmpty ? ShortName( target ) : child.Value );
break;
case XElement child when child.Name == "paramref" || child.Name == "typeparamref":
sb.Append( (string)child.Attribute( "name" ) );
break;
case XElement child when child.Name == "para" || child.Name == "br":
sb.Append( ' ' );
Append( child, sb );
sb.Append( ' ' );
break;
case XElement child:
Append( child, sb );
break;
}
}
}
/// <summary>
/// "M:Sandbox.GameObject.Destroy" -> "Destroy", "T:System.String" -> "String".
/// </summary>
private static string ShortName( string cref )
{
if ( cref.Length > 2 && cref[1] == ':' ) cref = cref[2..];
var paren = cref.IndexOf( '(' );
if ( paren >= 0 ) cref = cref[..paren];
var dot = cref.LastIndexOf( '.' );
return dot >= 0 ? cref[(dot + 1)..] : cref;
}
}