Click here to Skip to main content
15,893,622 members
Articles / Programming Languages / C#

Creating a CodeDOM: Modeling the Semantics of Code (Part 2)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (17 votes)
9 Nov 2012CDDL24 min read 41.2K   756   33  
Creating a CodeDOM for C#
// 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
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)


Written By
Software Developer (Senior)
United States United States
I've been writing software since the late 70's, currently focusing mainly on C#.NET. I also like to travel around the world, and I own a Chocolate Factory (sadly, none of my employees are oompa loompas).

Comments and Discussions