using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Reflection;
using System.Xml;
using System.IO;
using AWSPowerShellGenerator.ServiceConfig;
using AWSPowerShellGenerator.Utils;
using System.Collections.Concurrent;
namespace AWSPowerShellGenerator.Generators
{
public class SimplePropertyInfo
{
const string ConstantClassBaseTypeName = "Amazon.Runtime.ConstantClass";
#region Properties
public enum PropertyCollectionType
{
NoCollection = 0,
IsGenericList,
IsGenericDictionary,
IsGenericListOfGenericList,
IsGenericListOfGenericListOfGenericList,
IsGenericListOfGenericListOfGenericListOfGenericList,
IsGenericListOfGenericDictionary
}
public PropertyInfo BaseProperty { get; private set; }
public Type DeclaringType { get; private set; }
public Type PropertyType { get; private set; }
public String DeprecationMessage { get; private set; }
public bool IsDeprecated { get { return DeprecationMessage != null; } }
public bool IsRequired { get; private set; }
public long? MinValue { get; private set; }
public long? MaxValue { get; private set; }
///
///
/// Returns true if we should check at runtime that the associated parameter has been bound
/// and is in the cmdlet's BoundParameters collection.
///
///
/// The execution context member for the parameter will be set to a nullable type to allow us
/// to employ use-if-present/ignore-if-absent semantics and avoid passing unintentional default
/// values to request class members (ie 'false' for a bool that was never specified by the user,
/// which may have a different meaning to 'not specified').
///
///
public bool UseParameterValueOnlyIfBound { get; internal set; }
public string PropertyTypeName { get; set; }
public Type[] GenericCollectionTypes { get; internal set; }
public PropertyCollectionType CollectionType { get; internal set; }
public SimplePropertyInfo Parent { get; private set; }
public SimplePropertyInfo RootParent
{
get
{
if (Parent == null)
return this;
return Parent.RootParent;
}
}
public List Children { get; private set; }
///
/// Any customization applied to the parameter, either from a
/// config file entry or during generation as a result of
/// automatic renaming. In the configuration file, parameters
/// can be customized at the service operation level or globally
/// for a service.
///
public Param Customization { get; set; }
///
/// The original property member name
///
public string Name { get; private set; }
///
/// The full (potentially flattened) name of the property after
/// reflection of the request hierarchy has completed. This
/// uniquely identifies the property no matter the hierarchy.
/// Parent property names are separated by '_' characters.
///
public string AnalyzedName { get { return GetFullName(Name); } }
///
/// The name of the parameter to expose to the user.
///
public string CmdletParameterName
{
get
{
if (Customization != null && !string.IsNullOrEmpty(Customization.NewName))
return Customization.NewName;
return AnalyzedName;
}
}
// set when parsing properties on cmdlets, so we can extract positional,
// pipeline-by-value and parameter set data for help purposes
public ParameterAttribute[] PsParameterAttribute { get; private set; }
// set when parsing properties on cmdlets, so that we can report aliases
// in the web help
public AliasAttribute PsAliasAttribute { get; private set; }
// for help generation, this is retrieved from the Amazon.PowerShell.Common.AWSRequiredParameter
// Null means the parameter is never required. An empty set means the parameter is always required.
public HashSet IsRequiredForParameterSets { get; private set; }
///
/// Safely returns positional data for a cmdlet property. Parameters that
/// are not positional are given a fake positional value that orders them
/// after the real positionals; these parameters can only be specified with
/// the parameter name.
///
public int ParameterPosition
{
get
{
if (PsParameterAttribute != null)
{
foreach (var p in PsParameterAttribute)
{
if (p.Position >= 0)
return p.Position;
}
}
return int.MaxValue;
}
}
public bool CanPipelineByValue
{
get
{
if (PsParameterAttribute == null)
{
foreach (var p in PsParameterAttribute)
{
if (p.ValueFromPipeline)
return true;
}
}
return false;
}
}
///
/// Wether the property is required. If the parent property is a class member, the parent
/// mult also be a required property.
///
public bool IsRecursivelyRequired
{
get
{
return IsRequired && (Parent?.IsRecursivelyRequired ?? true);
}
}
///
/// If the parameter type is a switch/bool type, we only shorten and prefer to leave any
/// pluralization in the termination fragment as-is, for better readability
///
public bool IsValidForSingularization
{
get
{
// we don't currently use SwitchParameter types in the generated cmdlets, only nullable bools
// but test in case this changes. I've noticed some variability in casing so do insensitive
// check, plus we can see 'bool' and 'boolean' so match on prefix.
var typesNotToSingularize = new[] {"bool", "switch"};
return !typesNotToSingularize.Any(t => PropertyTypeName.StartsWith(t, StringComparison.OrdinalIgnoreCase));
}
}
///
/// If true the parameter type derives from the SDK's ConstantClass
/// enumeration type and an argument completer should be generated
/// for/referenced by the parameter.
///
public bool IsConstrainedToSet
{
get; private set;
}
public bool IsReadWrite { get; private set; }
public XmlDocument DocumentationSource { get; private set; }
///
/// True if the type of the parameter/property is derived from
/// a System.IO.MemoryStream type.
///
public bool IsMemoryStreamType { get; private set; }
///
/// True if the type of the parameter/property is derived from
/// a System.IO.Stream type.
///
public bool IsStreamType { get; private set; }
///
/// True if the type of the parameter/property is Amazon.Runtime.Document
///
public bool IsDocumentType { get; private set; }
public string DefaultValue { get; internal set; }
private static ConcurrentDictionary, string> PowershellDocumentationCache = new ConcurrentDictionary, string>();
public string PowershellDocumentation
{
get
{
var key = new Tuple(DeclaringType, CmdletParameterName);
var xml = PowershellDocumentationCache.GetOrAdd(key, (k) =>
{
var documentation = MemberDocumentation; //FlattenedDocumentation;
return DocumentationUtils.FormatXMLForPowershell(documentation);
});
return xml;
}
}
private static ConcurrentDictionary, string> PowershellWebDocumentationCache = new ConcurrentDictionary, string>();
public string PowershellWebDocumentation
{
get
{
var key = new Tuple(DeclaringType, CmdletParameterName);
var xml = PowershellWebDocumentationCache.GetOrAdd(key, (k) =>
{
var documentation = MemberDocumentation; //FlattenedDocumentation;
return DocumentationUtils.FormatXMLForPowershell(documentation, true);
});
return xml;
}
}
// Extracts just the member documentation for a parameter, ignoring the parent hierarchy
// issues that using FlattenedDocumentation yields. In addition we strip the unnecessary
// 'Gets and sets the...' opening sentence if present. If as a result of these changes
// the member yields no documentation, we log it and put in a holding comment whilst we
// chase up the service team (so the SDKs and other tools benefit too).
public string MemberDocumentation
{
get
{
using (var writer = new StringWriter())
{
var propertyDocumentation = DocumentationUtils.GetPropertyDocumentation(DeclaringType, Name, DocumentationSource);
// would like to make this a collection of regex's one day, to handle multiple cuts/replacements
var sentenceStart = propertyDocumentation.IndexOf("Gets and sets ", StringComparison.Ordinal);
if (sentenceStart != -1)
{
// roll forward to the start of the next sentence, if any
var sentenceEnd = propertyDocumentation.IndexOf('.', sentenceStart + 1);
if (sentenceEnd != -1)
{
sentenceEnd++;
while (sentenceEnd < propertyDocumentation.Length)
{
if (!Char.IsWhiteSpace(propertyDocumentation[sentenceEnd]))
break;
sentenceEnd++;
}
}
// snip out the sentence, as this leaves any surrounding tags intact (which
// is safer)
if (sentenceEnd == -1 || sentenceEnd > propertyDocumentation.Length)
sentenceEnd = propertyDocumentation.Length;
var sentenceLength = sentenceEnd - sentenceStart;
propertyDocumentation = propertyDocumentation.Remove(sentenceStart, sentenceLength);
}
// do some cleaning up of excess newlines we sometimes see
if (!string.IsNullOrEmpty(propertyDocumentation))
{
propertyDocumentation = propertyDocumentation.TrimStart('\r', '\n');
propertyDocumentation = propertyDocumentation.Replace("" + Environment.NewLine, "");
propertyDocumentation = propertyDocumentation.Replace(Environment.NewLine + "", "");
propertyDocumentation = propertyDocumentation.Replace("", "");
propertyDocumentation = propertyDocumentation.TrimEnd('\r', '\n');
}
if (propertyDocumentation.Length == 0)
{
propertyDocumentation = "The service has not provided documentation for this parameter; please refer to the service's API reference documentation for the latest available information.";
// Steve: turning this message off now I have the initial set of shape
// members that have no useful documentation. Will re-enable and make a build
// error once the doc team members I've contacted finish updating their models.
//Console.WriteLine("Warning: Missing parameter documentation: - sdk: {0} (parameter {1})", this.GetFullName(this.BaseProperty), this.CmdletParameterName);
}
// have some sdk docs that don't start with para tags, but use them internally and
// other mixes -- unless we have recognized start and end tags, we assume we have a
// mixed snippet and enclose everything.
writer.WriteLine("");
writer.WriteLine(propertyDocumentation);
writer.WriteLine("");
var finalDoc = writer.ToString();
return finalDoc.Trim();
}
}
}
///
/// Extracts the field values for a property that is derived from the SDK's ConstantClass
/// enumeration type.
///
/// The ConstantClass-derived type to be inspected
/// Collection of strings representing the valid values
public static IEnumerable GetConstantClassMembers(Type propertyType)
{
if (!propertyType.BaseType.FullName.Equals(ConstantClassBaseTypeName, StringComparison.Ordinal))
throw new ArgumentException(string.Format("GetConstantClassMembers: base type of {0} was {1}, expected {2}",
propertyType.FullName,
propertyType.BaseType.FullName,
ConstantClassBaseTypeName));
// order the set to help user at command prompt; ignore case since PowerShell is case insensitive and
// SDK member styling varies
var memberSet = new SortedSet(StringComparer.OrdinalIgnoreCase);
var fields = propertyType.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.GetField);
foreach (var f in fields)
{
var v = f.GetValue(null).ToString();
memberSet.Add(v);
}
return memberSet;
}
#endregion
#region Constructor
///
/// This ctor is used by the help generator. At this point we don't care about collection type/contents or
/// emit limiter status as the type data has already been determined.
///
///
///
///
///
public SimplePropertyInfo(PropertyInfo propertyInfo, SimplePropertyInfo parent, string propertyTypeName, XmlDocument documentationSource)
: this(propertyInfo, parent, propertyTypeName, documentationSource, PropertyCollectionType.NoCollection, null)
{
}
///
/// This ctor should be used by anything that emits code. Emit limiters are always emitted
/// as int types regardless of the wire type and as such a check will be made for the
/// parameter being bound before use. The check for all other parameters is done based on
/// whether they could be a nullable value type.
///
///
///
///
///
///
///
public SimplePropertyInfo(PropertyInfo propertyInfo,
SimplePropertyInfo parent,
string propertyTypeName,
XmlDocument documentationSource,
PropertyCollectionType collectionType,
Type[] genericCollectionTypes)
{
BaseProperty = propertyInfo;
Name = propertyInfo.Name;
PropertyType = propertyInfo.PropertyType;
PropertyTypeName = propertyTypeName;
DeclaringType = propertyInfo.DeclaringType;
DeprecationMessage = propertyInfo.GetCustomAttributes(typeof(ObsoleteAttribute), false).Cast().FirstOrDefault()?.Message;
dynamic awsPropertyAttribute = propertyInfo.GetCustomAttributes().Where(attribute => attribute.GetType().FullName == "Amazon.Runtime.Internal.AWSPropertyAttribute").SingleOrDefault();
if (awsPropertyAttribute != null)
{
IsRequired = awsPropertyAttribute?.Required;
MinValue = awsPropertyAttribute.IsMinSet ? awsPropertyAttribute.Min : null;
MaxValue = awsPropertyAttribute.IsMaxSet ? awsPropertyAttribute.Max : null;
}
CollectionType = collectionType;
GenericCollectionTypes = genericCollectionTypes;
Parent = parent;
Children = new List();
IsReadWrite = propertyInfo.CanRead && propertyInfo.CanWrite;
DocumentationSource = documentationSource;
IsMemoryStreamType = typeof(System.IO.MemoryStream).IsAssignableFrom(PropertyType);
IsStreamType = typeof(System.IO.Stream).IsAssignableFrom(PropertyType);
IsDocumentType = PropertyType.FullName == "Amazon.Runtime.Documents.Document";
UseParameterValueOnlyIfBound = IsNullableValueType(propertyInfo.PropertyType);
// if analysing properties on a cmdlet for help purposes, extract
// the Parameter, AWSRequiredParameter and Alias attributes info
PsParameterAttribute
= propertyInfo.GetCustomAttributes(typeof(ParameterAttribute), false) as ParameterAttribute[];
PsAliasAttribute
= propertyInfo.GetCustomAttributes(typeof(AliasAttribute), false).SingleOrDefault() as AliasAttribute;
IsRequiredForParameterSets = GetRequiredHelpDescription(propertyInfo);
IsConstrainedToSet = PropertyType.BaseType != null && PropertyType.BaseType.FullName.Equals(ConstantClassBaseTypeName, StringComparison.Ordinal);
}
#endregion
#region Private members
private static HashSet GetRequiredHelpDescription(PropertyInfo propertyInfo)
{
dynamic awsRequiredParameter = propertyInfo.GetCustomAttributes().Where(attribute => attribute.GetType().FullName == "Amazon.PowerShell.Common.AWSRequiredParameterAttribute").SingleOrDefault();
if (awsRequiredParameter == null)
{
return null;
}
string[] parameterSets = awsRequiredParameter.ParameterSets;
if (parameterSets == null || parameterSets.Length == 0)
{
return new HashSet();
}
return new HashSet(parameterSets);
}
private static bool IsNullableValueType(Type type)
{
if (type == typeof(PSObject) || type.FullName == "Amazon.Runtime.Documents.Document")
return false;
if (!type.IsValueType)
return false;
if (type.IsGenericType)
return type.GetGenericTypeDefinition().IsAssignableFrom(typeof (Nullable<>));
return true;
}
private string GetFullName(PropertyInfo propertyInfo)
{
string parentHierarchy = RootParent.DeclaringType.FullName;
var parent = Parent;
while (parent != null)
{
parentHierarchy = parentHierarchy + "." + parent.Name;
parent = parent.Parent;
}
string propertyFullName = parentHierarchy + "." + Name;
return propertyFullName;
}
private string GetFullName(string name)
{
string fullName;
if (Parent != null)
fullName = Parent.AnalyzedName + "_" + name;
else
fullName = name;
return fullName;
}
#endregion
public override string ToString()
{
return this.AnalyzedName;
}
}
public static class DocumentationUtils
{
// these two strings get substituted when we have iteration parameters on a cmdlet but no doc
private const string MissingNextTokenHelpText = "Indicates where to start fetching the next page of results when paginating manually.";
private const string MissingMaxRecordsHelpText = "The maximum number of records to emit.";
///
/// Contains substitute text for cmdlet parameters which yield in no documentation once
/// the SDK doc (if any) has been cleaned of redundant information ("Gets and sets...").
/// The intent is to have this be an empty collection eventually.... The dictionary is keyed
/// off of the cmdlet parameter's property name, so entries can be re-used across multiple cmdlets.
/// This collection is probed if a member-specific entry cannot be found in the
/// SubstituteSDKTypeMemberDocumentation dictionary.
///
static readonly Dictionary SubstituteParameterDocumentation = new Dictionary(StringComparer.Ordinal)
{
// iteration parameters
{ "NextToken", MissingNextTokenHelpText },
{ "Marker", MissingNextTokenHelpText },
{ "PageToken", MissingNextTokenHelpText },
{ "MaxRecords", MissingMaxRecordsHelpText },
{ "MaxItems", MissingMaxRecordsHelpText },
{ "MaxKeys", MissingMaxRecordsHelpText },
{ "MaxJobs", MissingMaxRecordsHelpText },
{ "Limit", MissingMaxRecordsHelpText },
{ "MaxResults", MissingMaxRecordsHelpText },
};
#region Cmdlet processing
private const string SDKLeadingSpaces = " ";
private const string SDKTypeFirstLineStart = "Container for the parameters to the";
private const string SDKTypeFirstLineEnd = "operation.";
private static string CleanseSDKTypeDocumentation(string documentation)
{
bool isFirst = true;
var modified = DocumentationUtils.ProcessLines(documentation, l =>
{
string t = l;
if (t.StartsWith(SDKLeadingSpaces, StringComparison.Ordinal))
{
t = t.Substring(SDKLeadingSpaces.Length);
}
if (isFirst)
{
isFirst = false;
if (t.StartsWith(SDKTypeFirstLineStart, StringComparison.Ordinal) &&
t.EndsWith(SDKTypeFirstLineEnd, StringComparison.Ordinal))
{
return null;
}
}
return t;
});
string xml = RemoveOuterParaTag(modified);
return xml;
}
private static string RemoveOuterParaTag(string text)
{
//Because most of the times text is not valid xml, this method wastes a lot
//of build time by failing XmlDocument.LoadXml and catching the exception.
//Let's at least return quickly if text doesn't start with a '<'.
//TODO replace this whole method with a regular expression.
if (!text.TrimStart().StartsWith('<'))
{
return text;
}
var doc = new XmlDocument();
string newXml = text;
try
{
//Adding an outer element because sometimes there are more elements than one in the xml block
//and it would fail the parsing
doc.LoadXml("" + text + "");
var rootElement = doc.DocumentElement;
if (rootElement.ChildNodes.Count == 1 && (rootElement.ChildNodes[0] as XmlElement)?.Name == "para")
{
newXml = rootElement["para"].InnerXml.Trim();
}
}
catch (Exception e)
{
}
return newXml;
}
#endregion
#region PowerShell processing
//private static HashSet XMLNodesToIgnore = new HashSet(StringComparer.OrdinalIgnoreCase) { "b", "i", "c" };
private static HashSet XMLNodesToCopyAsIs = new HashSet(StringComparer.OrdinalIgnoreCase)
{
"list", "see", "istruncated", "copy", "a", "br", "b", "i", "c", "p", "emphasis", "important", "code", "member", "title", "caution"
};
private static HashSet XMLNodesToNewline = new HashSet(StringComparer.OrdinalIgnoreCase)
{
"para", "note", "ul", "remarks", "ol"
};
private static HashSet XMLNodesToRemove = new HashSet(StringComparer.OrdinalIgnoreCase)
{
"enumvalues"
};
// temp replacement docs for ElastiCache's use of a table element in the
// docs for NewAvailabilityZones
private const string newAvailabilityZonesTableReplacement =
@"
Scenarios Pending Operation New Request Results
--------- ----------------- ----------- ------------------------------------
Scenario-1 Delete Delete The new delete, pending or immediate,
replaces the pending delete.
Scenario-2 Delete Create The new create, pending or immediate,
replaces the pending delete.
Scenario-3 Create Delete The new delete, pending or immediate,
replaces the pending create.
Scenario-4 Create Create The new create is added to the pending
create. Important: If the new create
request is 'Apply Immediately - Yes',
all creates are performed immediately.
If the new create request is
'Apply Immediately - No', all creates
are pending.
";
public static string FormatXMLForPowershell(string xml, bool forWebUse = false)
{
var sb = new StringBuilder();
using (var reader = new XmlTextReader(xml, XmlNodeType.Element, null))
{
while (reader.Read())
{
var type = reader.NodeType;
var name = reader.Name.ToLowerInvariant();
var value = reader.Value;
if (type == XmlNodeType.Element)
{
switch (name)
{
case "ul":
case "ol":
if (!forWebUse)
sb.AppendLine();
else
sb.AppendFormat("<{0}>", name);
break;
case "li":
if (!forWebUse)
{
sb.AppendLine();
sb.Append(" -");
}
else
sb.AppendFormat("<{0}>", name);
break;
// very temp hack for ElastiCache in 2.3.5.0 release -- this is the only known set of table elements,
// we want them in web help but not maml docs
case "table":
if (!forWebUse)
{
sb.Append(newAvailabilityZonesTableReplacement);
do
{
reader.Read();
} while (!reader.Name.Equals("table", StringComparison.OrdinalIgnoreCase));
}
else
sb.AppendFormat("<{0}>", name);
break;
case "th":
case "tr":
case "td":
case "div":
if (forWebUse)
sb.AppendFormat("<{0}>", name);
break;
case "dl":
case "dt":
case "dd":
if (forWebUse)
sb.AppendFormat("<{0}>", name);
break;
case "strong":
if (forWebUse)
sb.AppendFormat("<{0}>", name);
break;
case "pre":
if (forWebUse)
sb.AppendFormat("<{0}>", name);
break;
case "link":
case "filename":
case "replaceable":
case "seealso":
break;
default:
if (!HandleElement(sb, reader, name, forWebUse))
{
throw new InvalidOperationException("Unsupported node of type " + name + ". Full XML: " + xml);
}
break;
}
}
else if (type == XmlNodeType.EndElement)
{
switch (name)
{
case "li":
if (!forWebUse)
sb.AppendLine();
else
sb.AppendFormat("{0}>", name);
break;
case "ul":
case "ol":
if (!forWebUse)
{
sb.AppendLine();
sb.AppendLine();
}
else
sb.AppendFormat("{0}>", name);
break;
// very temp hack for ElastiCache in 2.3.5.0 release -- this is the only known set of table elements,
// we want them in web help but not maml docs
case "table":
if (forWebUse)
sb.AppendFormat("{0}>", name);
break;
case "th":
case "tr":
case "td":
case "div":
if (forWebUse)
sb.AppendFormat("{0}>", name);
break;
case "dl":
case "dt":
case "dd":
if (forWebUse)
sb.AppendFormat("{0}>", name);
break;
case "strong":
if (forWebUse)
sb.AppendFormat("{0}>", name);
break;
case "pre":
if (forWebUse)
sb.AppendFormat("{0}>", name);
break;
case "link":
case "filename":
case "replaceable":
case "seealso":
break;
default:
if (!HandleEndElement(sb, name, forWebUse))
{
throw new InvalidOperationException("Unsupported node of type " + name +
". Full XML: " + xml);
}
break;
}
}
else if (type == XmlNodeType.Text)
{
sb.Append(System.Net.WebUtility.HtmlEncode(value));
}
}
string composed = sb.ToString();
string final = DocumentationUtils.ProcessLines(composed, l => l, compressConsequitiveNonemptyLines: true, compressConsequitiveEmptyLines: true, skipEmptyLines: true);
return final;
}
}
private static bool HandleElement(StringBuilder sb, XmlTextReader reader, string name, bool forWebUse)
{
if (XMLNodesToCopyAsIs.Contains(name) || XMLNodesToRemove.Contains(name))
{
using (var subReader = reader.ReadSubtree())
{
var doc = new XmlDocument();
doc.Load(subReader);
var el = doc.DocumentElement;
if (!XMLNodesToRemove.Contains(name))
{
sb.Append(el.OuterXml);
}
}
}
//else if (XMLNodesToIgnore.Contains(name))
//{
// sb.AppendFormat("<{0}>", name);
//}
else if (XMLNodesToNewline.Contains(name))
{
if (!forWebUse)
sb.AppendLine();
}
else
{
return false;
}
return true;
}
private static bool HandleEndElement(StringBuilder sb, string name, bool forWebUse)
{
//if (XMLNodesToIgnore.Contains(name))
//{
// sb.AppendFormat("{0}>", name);
//}
if (XMLNodesToNewline.Contains(name))
{
if (!forWebUse)
sb.AppendLine();
}
else
{
return false;
}
return true;
}
#endregion
#region Documentation methods
public static string CommentDocumentation(string documentation)
{
using (var writer = new StringWriter())
{
string commentedDocs = ProcessLines(documentation, l => "/// " + l);
writer.WriteLine("/// ");
writer.WriteLine(commentedDocs);
writer.Write("/// ");
string formattedDocumentation = writer.ToString();
return formattedDocumentation;
}
}
public static string GetTypeDocumentation(Type type, XmlDocument documentationSource)
{
string raw = GetRawTypeDocumentation(type, documentationSource);
string clean = CleanseSDKTypeDocumentation(raw);
return clean;
}
public static string GetMethodDocumentation(Type declaringType, string methodName, XmlDocument documentationSource)
{
string raw = GetRawMethodDocumentation(declaringType, methodName, documentationSource);
string clean = CleanseSDKTypeDocumentation(raw);
return clean;
}
public static string GetPropertyDocumentation(Type declaringType, string propertyName, XmlDocument documentationSource)
{
string raw = GetRawPropertyDocumentation(declaringType, propertyName, documentationSource);
string clean = CleanseSDKTypeDocumentation(raw);
return clean;
}
public static string ProcessLines(string text, Func action, bool compressConsequitiveNonemptyLines = false, bool compressConsequitiveEmptyLines = false, bool skipEmptyLines = false)
{
var garbageChars = new char[]
{
'\u2028'
};
var builder = new StringBuilder();
IEnumerable lines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
// remove leading and trailing empty lines (Reverse lines, SkipWhile empty, Reverse again)
lines = lines.SkipWhile(s => string.IsNullOrEmpty(s.Trim())).Reverse().SkipWhile(s => string.IsNullOrEmpty(s.Trim())).Reverse();
if (compressConsequitiveNonemptyLines)
lines = CompressConsequitiveNonemptyLines(lines);
if (compressConsequitiveEmptyLines)
lines = CompressConsequitiveEmptyLines(lines);
lines = lines.ToList();
foreach (var line in lines)
{
var newLine = action(line);
if (newLine != null && !(skipEmptyLines && newLine.IsEmpty())) // skip only null lines: empty lines stay
{
if (builder.Length > 0)
builder.AppendLine();
// strip out any garbage characters that occur from time to time
foreach (var c in garbageChars)
{
newLine = newLine.Replace(c, ' ');
}
builder.Append(newLine);
}
}
string formattedLines = builder.ToString();
return formattedLines;
}
#endregion
#region Private methods
private static string GetRawTypeDocumentation(Type type, XmlDocument documentationSource)
{
string xpath = string.Format("doc/members/member[@name='T:{0}']", type.FullName.Replace('+', '.'));
var docNode = documentationSource.SelectSingleNode(xpath);
if (docNode == null)
{
//throw new InvalidOperationException(string.Format(
// "Unable to find documentation for type {0}, expected at xpath {1}",
// type.FullName, xpath));
Console.WriteLine("NO SDK DOCUMENTATION PRESENT FOR TYPE {0}, expected at xpath {1}", type.FullName, xpath);
// emit just the name into help; the shouty warning looks bad
return type.FullName;
}
var summary = docNode.SelectSingleNode("summary");
if (summary == null)
{
throw new InvalidOperationException(string.Format(
"Unable to find summary node for type {0}, expected at xpath {1}/summary",
type.FullName, xpath));
}
string xml = summary.InnerXml;
return xml;
}
private static string GetRawPropertyDocumentation(Type declaringType, string propertyName, XmlDocument documentationSource)
{
string propertyFullName = declaringType.FullName.Replace('+', '.') + "." + propertyName;
string xpath = string.Format("doc/members/member[@name='P:{0}']", propertyFullName);
var docNode = documentationSource.SelectSingleNode(xpath);
if (docNode == null)
{
// emit just the name into help unless its a pagination property we
// recognise; the shouty warning looks bad
if (SubstituteParameterDocumentation.ContainsKey(propertyName))
return SubstituteParameterDocumentation[propertyName];
return propertyFullName;
}
var summary = docNode.SelectSingleNode("summary");
if (summary == null)
throw new InvalidOperationException(string.Format(
"Unable to find documentation for property {0} of type {1}, expected at xpath {2}/summary",
propertyName, declaringType.FullName, xpath));
string xml = summary.InnerXml;
return xml;
}
private static string GetRawMethodDocumentation(Type declaringType, string methodName, XmlDocument documentationSource)
{
string methodFullName = declaringType.FullName.Replace('+', '.') + "." + methodName;
string xpath = string.Format("doc/members/member[starts-with(@name, 'M:{0}Async(')]", methodFullName);
var docNode = documentationSource.SelectSingleNode(xpath);
if (docNode == null)
{
// emit just the name into help; the shouty warning looks bad
return methodFullName;
}
var summary = docNode.SelectSingleNode("summary");
if (summary == null)
throw new InvalidOperationException(string.Format(
"Unable to find documentation for property {0} of type {1}, expected at xpath {2}/summary",
methodName, declaringType.FullName, xpath));
string xml = summary.InnerXml;
return xml;
}
private static IEnumerable CompressConsequitiveEmptyLines(IEnumerable lines)
{
int lastLineLength = 0;
foreach (var line in lines)
{
int lineLength = line == null ? 0 : line.Trim().Length;
if (lineLength > 0 || lastLineLength > 0)
{
if (lineLength == 0)
yield return string.Empty;
else
yield return line;
}
lastLineLength = lineLength;
}
}
private static IEnumerable CompressConsequitiveNonemptyLines(IEnumerable lines)
{
StringBuilder builder = new StringBuilder();
foreach (var line in lines)
{
if (line.IsEmpty())
{
if (builder.Length > 0)
{
yield return builder.ToString();
builder.Clear();
}
yield return line;
}
else
{
builder.AppendFormat("{0} ", line);
}
}
if (builder.Length > 0)
yield return builder.ToString();
}
private static bool IsEmpty(this string self)
{
if (string.IsNullOrEmpty(self))
return true;
if (string.IsNullOrEmpty(self.Trim()))
return true;
return false;
}
#endregion
}
}