// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php
using System;
using System.Linq;
using Nova.Rendering;
namespace Nova.CodeDOM
{
/// <summary>
/// Represents user documentation of code, and is also the common base class of <see cref="DocText"/>,
/// <see cref="DocCodeRefBase"/>, <see cref="DocNameBase"/>, <see cref="DocB"/>, <see cref="DocC"/>, <see cref="DocCode"/>,
/// <see cref="DocCDATA"/>, <see cref="DocExample"/>, <see cref="DocI"/>, <see cref="DocInclude"/>, <see cref="DocPara"/>,
/// <see cref="DocRemarks"/>, <see cref="DocSummary"/>, <see cref="DocTag"/>, <see cref="DocValue"/>, <see cref="DocList"/>,
/// <see cref="DocListHeader"/>, <see cref="DocListItem"/>, <see cref="DocListDescription"/>, <see cref="DocListTerm"/>.
/// </summary>
/// <remarks>
/// This is the common base class of all documentation comment classes, but it can also be instantiated as
/// a container of other documentation comment objects in cases where more than one is attached to the same
/// code object. For example, a DocSummary object can be attached to a code object by itself, or a DocComment
/// can be attached which in turn can contain instances of DocSummary, DocParam, DocReturns, etc.
/// C# uses an XML format for documentation comments, but Nova parses them and stores them as nested collections
/// of code objects to make their manipulation and display easier. References to code objects (DocParam, DocSee,
/// etc) are stored as SymbolicRefs, and code embedded inside comments (DocCode, DocC) are stored as nested sub-
/// trees of actual code objects, allowing for navigation and refactoring.
/// </remarks>
public class DocComment : CommentBase
{
#region /* FIELDS */
/// <summary>
/// The content can be a simple string or a ChildList of DocComment objects, or in some cases it can
/// also be a sub-tree of embedded code objects.
/// </summary>
protected object _content;
#endregion
#region /* CONSTRUCTORS */
/// <summary>
/// Create a <see cref="DocComment"/>.
/// </summary>
public DocComment()
{ }
/// <summary>
/// Create a <see cref="DocComment"/> with the specified text content.
/// </summary>
public DocComment(string text)
{
_content = (text != null ? text.Replace("\r\n", "\n") : null); // Normalize newlines
}
/// <summary>
/// Create a <see cref="DocComment"/> with the specified child <see cref="DocComment"/> content.
/// </summary>
public DocComment(DocComment docComment)
{
_content = new ChildList<DocComment>(this) { docComment };
}
/// <summary>
/// Create a <see cref="DocComment"/> with the specified children <see cref="DocComment"/>s as content.
/// </summary>
public DocComment(params DocComment[] docComments)
{
Add(docComments);
}
/// <summary>
/// Create a <see cref="DocComment"/> with the specified child <see cref="CodeObject"/> content.
/// </summary>
protected DocComment(CodeObject codeObject)
{
Content = codeObject;
}
#endregion
#region /* PROPERTIES */
/// <summary>
/// The content of the documentation comment - can be a simple string, a ChildList of DocComment objects, or
/// a sub-tree of embedded code objects.
/// </summary>
public object Content
{
get { return _content; }
set { SetField(ref _content, value, true); }
}
/// <summary>
/// The XML tag name for the documentation comment.
/// </summary>
public virtual string TagName
{
get { return null; }
}
/// <summary>
/// True if the documentation comment is missing a start tag.
/// </summary>
public bool MissingStartTag
{
get { return _annotationFlags.HasFlag(AnnotationFlags.NoStartTag); }
}
/// <summary>
/// True if the documentation comment is missing an end tag.
/// </summary>
public bool MissingEndTag
{
get { return _annotationFlags.HasFlag(AnnotationFlags.NoEndTag); }
}
#endregion
#region /* STATIC METHODS */
/// <summary>
/// Implicit conversion of a <c>string</c> to a <see cref="DocComment"/> (actually, a <see cref="DocText"/>).
/// </summary>
/// <remarks>This allows strings to be passed directly to any method expecting a <see cref="DocComment"/> type
/// without having to call <c>new DocText(text)</c>.</remarks>
/// <param name="text">The <c>string</c> to be converted.</param>
/// <returns>A generated <see cref="DocText"/> wrapping the specified <c>string</c>.</returns>
public static implicit operator DocComment(string text)
{
return new DocText(text);
}
#endregion
#region /* METHODS */
/// <summary>
/// Add the specified text to the documentation comment.
/// </summary>
public virtual void Add(string text)
{
if (text != null)
{
if (_content == null)
_content = text;
else if (_content is string)
_content += text;
else if (_content is ChildList<DocComment>)
{
ChildList<DocComment> children = (ChildList<DocComment>)_content;
if (children.Count == 0)
_content = text;
else if (children.Last is DocText)
children.Last.Add(text);
else
children.Add(new DocText(text));
}
else
throw new Exception("Can't add to a DocComment that contains code objects - add to the contained BlockDecl instead.");
}
}
/// <summary>
/// Add the specified child <see cref="DocComment"/> to the documentation comment.
/// </summary>
public virtual void Add(DocComment docComment)
{
if (docComment != null)
{
if (_content == null)
_content = new ChildList<DocComment>(this);
else if (_content is string)
{
string existing = (string)_content;
_content = new ChildList<DocComment>(this);
if (existing.Length > 0) // Don't use NotEmpty(), because we want to preserve whitespace
((ChildList<DocComment>)_content).Add(new DocText(existing));
}
if (docComment.GetType() == typeof(DocComment))
{
// If we're adding a base container, merge the two containers instead
object content = docComment.Content;
if (content is string)
((ChildList<DocComment>)_content).Add(new DocText((string)content));
else if (_content is ChildList<DocComment>)
{
((ChildList<DocComment>)_content).AddRange((ChildList<DocComment>)content);
NormalizeContent();
}
}
else if (_content is ChildList<DocComment>)
{
((ChildList<DocComment>)_content).Add(docComment);
NormalizeContent();
}
else
throw new Exception("Can't add to a DocComment that contains code objects - add to the contained BlockDecl instead.");
}
}
/// <summary>
/// Add the specified <see cref="DocComment"/>s to the documentation comment.
/// </summary>
public void Add(params DocComment[] docComments)
{
foreach (DocComment docComment in docComments)
Add(docComment);
}
/// <summary>
/// Normalize content.
/// </summary>
public void NormalizeContent()
{
if (_content is ChildList<DocComment>)
{
ChildList<DocComment> children = (ChildList<DocComment>)_content;
// Replace an empty collection with null
if (children.Count == 0)
_content = null;
else
{
for (int i = children.Count - 1; i > 0; --i)
{
// Combine adjacent DocText objects into a single object
if (children[i] is DocText && children[i - 1] is DocText)
{
children[i - 1].Add(children[i].Text);
children.RemoveAt(i);
}
}
if (children.Count == 1)
{
CodeObject child = children[0];
// Replace a single DocText with a string
if (child is DocText)
_content = ((DocText)child).Text;
else if (child.NewLines > 0)
{
// Remove any newlines on the first child if they weren't explicitly set
if (!child.IsNewLinesSet && child.NewLines > 0)
{
// Move the newlines to the parent if it hasn't been explicitly set
if (!IsNewLinesSet)
SetNewLines(child.NewLines);
child.SetNewLines(0);
}
}
}
}
}
}
/// <summary>
/// Returns the <see cref="DocSummary"/> documentation comment, or null if none exists.
/// </summary>
public override DocSummary GetDocSummary()
{
if (_content is ChildList<DocComment>)
return Enumerable.FirstOrDefault(Enumerable.OfType<DocSummary>(((ChildList<DocComment>)_content)));
return null;
}
/// <summary>
/// Get the root documentation comment object.
/// </summary>
public DocComment GetRootDocComment()
{
DocComment parent = this;
while (parent.Parent is DocComment)
parent = (DocComment)parent.Parent;
return parent;
}
/// <summary>
/// Deep-clone the code object.
/// </summary>
public override CodeObject Clone()
{
DocComment clone = (DocComment)base.Clone();
if (_content is ChildList<DocComment>)
clone._content = ChildListHelpers.Clone((ChildList<DocComment>)_content, clone);
else
clone.CloneField(ref clone._content, _content);
return clone;
}
#endregion
#region /* FORMATTING */
/// <summary>
/// Determines if the code object only requires a single line for display.
/// </summary>
public override bool IsSingleLine
{
get
{
if (base.IsSingleLine)
{
if (_content == null)
return true;
if (_content is string)
return (((string)_content).IndexOf('\n') < 0);
if (_content is ChildList<DocComment>)
return !((ChildList<DocComment>)_content)[0].IsFirstOnLine && ((ChildList<DocComment>)_content).IsSingleLine;
if (_content is CodeObject)
return !((CodeObject)_content).IsFirstOnLine && ((CodeObject)_content).IsSingleLine;
}
return false;
}
set
{
base.IsSingleLine = value;
if (_content is string)
{
if (value)
_content = ((string)_content).Trim().Replace("\n", "; ");
}
else if (_content is ChildList<DocComment>)
{
ChildList<DocComment> childList = (ChildList<DocComment>)_content;
if (value && childList.Count > 0)
childList[0].IsFirstOnLine = false;
childList.IsSingleLine = value;
}
else if (_content is CodeObject)
{
if (value)
((CodeObject)_content).IsFirstOnLine = false;
((CodeObject)_content).IsSingleLine = value;
}
}
}
#endregion
#region /* RENDERING */
protected virtual void AsTextStart(CodeWriter writer, RenderFlags flags)
{
if (!flags.HasFlag(RenderFlags.Description) || MissingEndTag)
{
string tagName = TagName;
if (tagName != null)
writer.Write("<" + tagName + (_content == null && !MissingEndTag ? "/>" : ">"));
}
}
protected internal string GetContentForDisplay(RenderFlags flags)
{
// If NoTagNewLines is set, trim any leading AND/OR trailing whitespace from the content (any newlines
// determine if the content starts and/or ends on the same line as the start/end tag, and NoTagNewLines
// means we don't want to render them because we're not rendering the tags).
string content = (string)_content;
if (flags.HasFlag(RenderFlags.NoTagNewLines))
content = content.Trim();
return content;
}
protected internal string GetContentForDisplay(DocText docText, bool isFirst, bool isLast, RenderFlags flags)
{
// If NoTagNewLines is set, trim any leading whitespace from the first child if it's a DocText, and trim
// any trailing whitespace from the last child if it's a DocText.
string text = docText.Text;
if (flags.HasFlag(RenderFlags.NoTagNewLines))
{
if (isFirst)
text = text.TrimStart();
else if (isLast)
text = text.TrimEnd();
}
return text;
}
protected virtual void AsTextContent(CodeWriter writer, RenderFlags flags)
{
writer.EscapeUnicode = false;
if (_content is string)
DocText.AsTextText(writer, GetContentForDisplay(flags), flags);
else if (_content is ChildList<DocComment>)
{
ChildList<DocComment> content = (ChildList<DocComment>)_content;
for (int i = 0; i < content.Count; ++i)
{
DocComment docComment = content[i];
if (docComment is DocText)
DocText.AsTextText(writer, GetContentForDisplay((DocText)docComment, i == 0, i == content.Count - 1, flags), flags);
else
docComment.AsText(writer, flags);
}
}
else if (_content is CodeObject)
{
// Turn on translation of '<', '&', and '>' for content
writer.InDocCommentContent = true;
((CodeObject)_content).AsText(writer, flags);
writer.InDocCommentContent = false;
}
writer.EscapeUnicode = true;
}
protected virtual void AsTextEnd(CodeWriter writer, RenderFlags flags)
{
if (!MissingEndTag && (_content != null || MissingStartTag) && !flags.HasFlag(RenderFlags.Description))
{
string tagName = TagName;
if (tagName != null)
writer.Write("</" + tagName + ">");
}
}
public override void AsText(CodeWriter writer, RenderFlags flags)
{
bool isPrefix = flags.HasFlag(RenderFlags.IsPrefix);
int newLines = NewLines;
bool isTopLevelDocComment = !flags.HasFlag(RenderFlags.InDocComment);
if (isTopLevelDocComment)
{
if (!isPrefix && newLines > 0 && !flags.HasFlag(RenderFlags.SuppressNewLine))
writer.WriteLines(newLines);
AsTextDocNewLines(writer, 0);
}
else if (!isPrefix && newLines > 0)
AsTextDocNewLines(writer, newLines);
RenderFlags passFlags = (flags & RenderFlags.PassMask) | RenderFlags.InDocComment;
UpdateLineCol(writer, flags);
AsTextStart(writer, passFlags);
AsTextContent(writer, passFlags);
AsTextEnd(writer, passFlags);
if (isTopLevelDocComment && isPrefix)
{
// If this object is rendered as a child prefix object of another, then any whitespace is
// rendered here *after* the object instead of before it.
// A documentation comment must always be followed by a newline if it's a prefix.
writer.WriteLines(newLines < 1 ? 1 : newLines);
}
}
protected static void AsTextDocNewLines(CodeWriter writer, int count)
{
// Render one or more newlines (0 means a prefix w/o a newline)
do
{
if (count > 0)
writer.WriteLine();
writer.Write("/// ");
--count;
}
while (count > 0);
}
#endregion
}
}