5
\$\begingroup\$

I'm adding rather extensive xml-doc comments to all Rubberduck inspection classes, like this:

namespace Rubberduck.Inspections.Concrete
{
    /// <summary>
    /// Warns about a variable that is assigned, and then re-assigned before the first assignment is read.
    /// </summary>
    /// <why>
    /// The first assignment is likely redundant, since it is being overwritten by the second.
    /// </why>
    /// <examples>
    /// <example>
    /// <text>This inspection means to flag the following examples:</text>
    /// <code>
    /// <![CDATA[
    /// Public Sub DoSomething()
    ///     Dim foo As Long
    ///     foo = 12 ' assignment is redundant
    ///     foo = 34 
    /// End Sub]]>
    /// </code>
    /// </example>
    /// <example>
    /// <text>The following code should not trip this inspection:</text>
    /// <code>
    /// <![CDATA[
    /// Public Sub DoSomething(ByVal foo As Long)
    ///     Dim bar As Long
    ///     bar = 12
    ///     bar = bar + foo ' variable is re-assigned, but the prior assigned value is read at least once first.
    /// End Sub]]>
    /// </code>
    /// </example>
    /// </examples>
    public sealed class AssignmentNotUsedInspection : InspectionBase
    {
        ...

The idea being to get our CI server to build the XML docs for that assembly, and then essentially use the GitHub repository as a CDN to retrieve and parse from the project's website - and that works great: on my local build I generate the /inspections/list page contents off the XML documentation, exactly as intended.

But the /list page doesn't include the <examples>; I want the /inspections/details/{inspection-name} pages to include them, and I want them not only formatted as code, but also syntax-highlighted - similar to how the site's 404 error page formats some dummy VBA code, but better: that dummy code's CSS formatting is all hard-coded, and there's no way I'm going to clutter the xml-doc examples with <span class="keyword"> tags.

Since Rubberduck's VBA parser does a pretty awesome job at parsing VBA code, I figured it shouldn't be too hard to write a class that takes this:

Public Sub DoSomething()
    Dim foo As Long
    foo = 12 ' assignment is redundant
    foo = 34
End Sub

...and outputs this:

<span class="keyword">Public</span> <span class="keyword">Sub</span> DoSomething()
    <span class="keyword">Dim</span> foo <span class="keyword">As</span> <span class="keyword">Long</span>
    foo = 12 <span class="comment">' assignment is redundant
</span>    foo = 34
<span class="keyword">End Sub</span>

Here's the class in question:

public class FormattedCodeBlockBuilder
{
    private static readonly string KeywordClass = "keyword";
    private static readonly string CommentClass = "comment";
    private static readonly string StringLiteralClass = "string-literal";

    public string Format(string code)
    {
        var builder = new StringBuilder(code.Length);
        var tokens = Tokenize(code);

        var parser = new VBAParser(tokens);
        var commentsListener = new CommentIntervalsListener();
        parser.AddParseListener(commentsListener);

        parser.startRule();

        for (var i = 0; i < tokens.Size; i++)
        {
            var token = tokens.Get(i);
            if (commentsListener.IsComment(token, out var commentInterval))
            {
                builder.Append($"<span class=\"{CommentClass}\">{tokens.GetText(commentInterval)}</span>");
                i = commentInterval.b;
            }
            else if (StringLiteralTokens.Contains(token.Type))
            {
                builder.Append($"<span class=\"{StringLiteralClass}\">{token.Text}</span>");
            }
            else if (KeywordTokens.Contains(token.Type))
            {
                builder.Append($"<span class=\"{KeywordClass}\">{token.Text}</span>");
            }
            else
            {
                builder.Append(token.Text);
            }
        }

        return builder.ToString();
    }

    private ITokenStream Tokenize(string code)
    {
        AntlrInputStream input;
        using (var reader = new StringReader(code))
        {
            input = new AntlrInputStream(reader);
        }
        var lexer = new VBALexer(input);
        return new CommonTokenStream(lexer);
    }

    #region token classes
    private static readonly HashSet<int> StringLiteralTokens = new HashSet<int>
    {
        VBAParser.STRINGLITERAL,
        VBAParser.DATELITERAL,
    };
    private static readonly HashSet<int> KeywordTokens = new HashSet<int> { 
        VBAParser.ANY,
        VBAParser.CURRENCY,
        VBAParser.DEBUG,
        VBAParser.DOEVENTS,
        VBAParser.EXIT,
        VBAParser.FIX,
        VBAParser.INPUTB,
        VBAParser.LBOUND,
        VBAParser.LONGLONG,
        VBAParser.LONGPTR,
        VBAParser.OPTION,
        VBAParser.PSET,
        VBAParser.SCALE,
        VBAParser.SGN,
        VBAParser.UBOUND,
        VBAParser.ACCESS,
        VBAParser.ADDRESSOF,
        VBAParser.ALIAS,
        VBAParser.AND,
        VBAParser.ATTRIBUTE,
        VBAParser.APPEND,
        VBAParser.AS,
        VBAParser.BEGINPROPERTY,
        VBAParser.BEGIN,
        VBAParser.BINARY,
        VBAParser.BOOLEAN,
        VBAParser.BYVAL,
        VBAParser.BYREF,
        VBAParser.BYTE,
        VBAParser.CALL,
        VBAParser.CASE,
        VBAParser.CDECL,
        VBAParser.CLASS,
        VBAParser.CLOSE,
        VBAParser.CONST,
        VBAParser.CONST,
        VBAParser.DATABASE,
        VBAParser.DATE,
        VBAParser.DECLARE,
        VBAParser.DEFBOOL,
        VBAParser.DEFBYTE,
        VBAParser.DEFDATE,
        VBAParser.DEFDBL,
        VBAParser.DEFCUR,
        VBAParser.DEFINT,
        VBAParser.DEFLNG,
        VBAParser.DEFLNGLNG,
        VBAParser.DEFLNGPTR,
        VBAParser.DEFOBJ,
        VBAParser.DEFSNG,
        VBAParser.DEFSTR,
        VBAParser.DEFVAR,
        VBAParser.DIM,
        VBAParser.DO,
        VBAParser.DOUBLE,
        VBAParser.EACH,
        VBAParser.ELSE,
        VBAParser.ELSEIF,
        VBAParser.EMPTY,
        VBAParser.END_ENUM,
        VBAParser.END_FUNCTION,
        VBAParser.END_IF,
        VBAParser.ENDPROPERTY,
        VBAParser.END_SELECT,
        VBAParser.END_SUB,
        VBAParser.END_TYPE,
        VBAParser.END_WITH,
        VBAParser.END,
        VBAParser.ENUM,
        VBAParser.EQV,
        VBAParser.ERASE,
        VBAParser.ERROR,
        VBAParser.EVENT,
        VBAParser.EXIT_DO,
        VBAParser.EXIT_FOR,
        VBAParser.EXIT_FUNCTION,
        VBAParser.EXIT_PROPERTY,
        VBAParser.EXIT_SUB,
        VBAParser.FALSE,
        VBAParser.FRIEND,
        VBAParser.FOR,
        VBAParser.FUNCTION,
        VBAParser.GET,
        VBAParser.GLOBAL,
        VBAParser.GOSUB,
        VBAParser.GOTO,
        VBAParser.IF,
        VBAParser.IMP,
        VBAParser.IMPLEMENTS,
        VBAParser.IN,
        VBAParser.INPUT,
        VBAParser.IS,
        VBAParser.INTEGER,
        VBAParser.LOCK,
        VBAParser.LONG,
        VBAParser.LOOP,
        VBAParser.LET,
        VBAParser.LIB,
        VBAParser.LIKE,
        VBAParser.LINE_INPUT,
        VBAParser.LOCK_READ,
        VBAParser.LOCK_WRITE,
        VBAParser.LOCK_READ_WRITE,
        VBAParser.LSET,
        VBAParser.MOD,
        VBAParser.NAME,
        VBAParser.NEXT,
        VBAParser.NEW,
        VBAParser.NOT,
        VBAParser.NOTHING,
        VBAParser.NULL,
        VBAParser.OBJECT,
        VBAParser.ON_ERROR,
        VBAParser.ON_LOCAL_ERROR,
        VBAParser.OPEN,
        VBAParser.OPTIONAL,
        VBAParser.OPTION_BASE,
        VBAParser.OPTION_EXPLICIT,
        VBAParser.OPTION_COMPARE,
        VBAParser.OPTION_PRIVATE_MODULE,
        VBAParser.OR,
        VBAParser.OUTPUT,
        VBAParser.PARAMARRAY,
        VBAParser.PRESERVE,
        VBAParser.PRINT,
        VBAParser.PRIVATE,
        VBAParser.PROPERTY_GET,
        VBAParser.PROPERTY_LET,
        VBAParser.PROPERTY_SET,
        VBAParser.PTRSAFE,
        VBAParser.PUBLIC,
        VBAParser.PUT,
        VBAParser.RANDOM,
        VBAParser.RANDOMIZE,
        VBAParser.RAISEEVENT,
        VBAParser.READ,
        VBAParser.READ_WRITE,
        VBAParser.REDIM,
        VBAParser.REM,
        VBAParser.RESET,
        VBAParser.RESUME,
        VBAParser.RETURN,
        VBAParser.RSET,
        VBAParser.SEEK,
        VBAParser.SELECT,
        VBAParser.SET,
        VBAParser.SHARED,
        VBAParser.SINGLE,
        VBAParser.STATIC,
        VBAParser.STEP,
        VBAParser.STOP,
        VBAParser.STRING,
        VBAParser.SUB,
        VBAParser.TAB,
        VBAParser.TEXT,
        VBAParser.THEN,
        VBAParser.TO,
        VBAParser.TRUE,
        VBAParser.TYPE,
        VBAParser.TYPEOF,
        VBAParser.UNLOCK,
        VBAParser.UNTIL,
        VBAParser.VARIANT,
        VBAParser.VERSION,
        VBAParser.WEND,
        VBAParser.WITH,
        VBAParser.WITHEVENTS,
        VBAParser.WRITE,
        VBAParser.XOR
    };
    #endregion

    private class CommentIntervalsListener : VBAParserBaseListener
    {
        private readonly IList<Interval> _intervals = new List<Interval>();

        public bool IsComment(IToken token, out Interval commentInterval)
        {
            if (!_intervals.Any())
            {
                commentInterval = Interval.Invalid;
                return false;
            }

            var tokenInterval = new Interval(token.TokenIndex, token.TokenIndex);
            commentInterval = _intervals.SingleOrDefault(e => e.ProperlyContains(tokenInterval));
            return !commentInterval.Equals(default(Interval));
        }

        public override void ExitCommentOrAnnotation(VBAParser.CommentOrAnnotationContext context)
        {
            _intervals.Add(context.SourceInterval);
        }
    }
}

Does anything strike anyone as weird or wrong in that string formatter class? I don't do web stuff very often, so this might very well be a very naive approach to building the formatted HTML. If it's any relevant, the website is ASP.NET MVC (not Core, since it's referencing the Rubberduck.SmartIndender and Rubberduck.Parsing assemblies).

\$\endgroup\$
2
\$\begingroup\$

You are basically building a compiler. You have your tokenizer and parser available, but your target language generator is missing. Sure, hardcoding in html can be done for a small use case, but I would prefer to use an existing API.

Suggested API: HtmlTextWriter from System.Web.

Alternative API: HtmlElement from System.Windows.Forms.

Both API's allow building html elements in code and back- and forward mapping between managed code and html text.

\$\endgroup\$
  • 1
    \$\begingroup\$ The System.Windows.Forms namespace would be the last place I would ever look for a HtmlElement :-o How did they come up with the idea to hide it there? Oh boy. \$\endgroup\$ – t3chb0t May 18 at 12:21
  • \$\begingroup\$ @t3chb0t good point - but I like the idea of building the HTML with that kind of object rather than concatenating/building a string. Surely System.Web has a similar DOM classes somewhere, right? \$\endgroup\$ – Mathieu Guindon May 18 at 13:21
  • \$\begingroup\$ @Mathieu Guindon: check out docs.microsoft.com/en-us/dotnet/api/…, I will update my post \$\endgroup\$ – dfhwze May 18 at 13:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.