65.9K
CodeProject 正在变化。 阅读更多。
Home

另一个 C++ 到 HTML 转换实用程序(用 C# 编写)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (4投票s)

2006年1月31日

5分钟阅读

viewsIcon

35800

downloadIcon

309

一篇关于如何将可编译的 C++ 源代码转换为 HTML 页面以便高亮显示的文章。

背景

在我开始编写这个实用程序之前,我曾试图将一些 C++ 源代码发布到我的博客上。当我尝试这样做时,结果几乎是一场灾难。然后我意识到我当时使用的编辑器只能复制粘贴纯 HTML 源代码。我立刻意识到创建一个/使用一个转换程序将 C++ 源代码转换为具有正确语法高亮的 HTML 源将是一件很酷的事情。

我最初尝试在不参考任何外部资源的情况下创建这个程序。结果并不理想。然后我搜索了 CodeProject。在那里,我发现了一篇由 q123456789 撰写的精彩文章。它叫做 "A C++ to HTML conversion utility"。这个程序包含了我需要的一切。但是,我不太喜欢它,因为它不可扩展。我真正想要的是一个基于 GUI 的程序,有两个编辑框;我将 C++ 代码粘贴到一个编辑框中,然后点击“转换”;在第二个编辑框中,我将看到 C++ 代码被转换成 HTML 源,可以粘贴到我的博客中。我决定用 C# 将 q123456789 的出色代码改造成一个简单的 GUI 程序。

使用代码

你不必学习“编程语言”课程就能理解这段代码。 原始文章 很好地解释了它的工作原理。我的想法,让我为你总结一下,是逐个字符地遍历 C++ 源代码,识别字符是什么,然后将其存储在一个缓冲区中,直到遇到一个不属于你正在尝试识别的字符串类型的字符。然后你将字符放回流中。最后,你识别字符串的类型。类型可以是

  • code
  • comment
  • CPP 字符串
  • 关键字
  • 预处理器
  • 不知道是什么。

它们在我的 CS 代码中定义如下:

public enum TokenType { code, comment, cppstring, keyword, pp, none };

注释基本上是任何以“//”开头的字符串,直到找到“\n”,或者任何用“/*”和“*/”括起来的字符串。

//This is a comment
/* This is another type of comment */

CPP 字符串是任何用双引号括起来的字符串。例如:

"This is a string."
"This is a string with character\"double quotes\" in it."

关键字是 C++ 关键字。这是一个很长的列表,所以我不会在这里列出它们。

我用“pp”作为预处理器的类型名。预处理器是这样的:

#include
#ifndef
#else
#endif
...

任何无法归入以上四类的,我都称之为代码。

当我开始处理这个问题时,我面临的最大问题是,在 原始文章(由 q123456789 撰写)中,使用 I/O 流逐个字符地获取(使用 basic_istream::get())来遍历代码,并解析为标记或代码片段。它还会不时地回退(basic_istream::unget())字符。当我尝试用 C# 执行此操作时,我有点震惊,一时不知所措。然后我意识到,在 C# 中,有一个名为 StringReader 的类,位于 System.IO 命名空间中。一旦我意识到这一点,我就能够继续编码了。

然后我不得不第二次停下来。原因是 StringReader 类没有提供 unget() 方法。好吧,我不会被这些小问题所阻止。经过一番阅读,我意识到 q123456789 在他的程序中处理这个问题的方式,即获取流中的一个字符,然后回退流,在使用 StringReader 类时是行不通的。

替代方案非常简单——程序会查看字符但不会前进。如果字符是可保留的,那么程序就会在流中前进一个字符。当我查看 StringReader 类时,我找到了我想使用的 `Peek()` 和 `Read()` 方法。`Peek()` 方法返回一个整数,代表当前位置的字符。但它不会移动到下一个字符。`Read()` 方法返回当前位置字符的整数,然后移动到下一个字符。在我的程序中,它会进行一次 `peek` 来获取当前位置字符的整数值,确保它不是 -1,然后使用 Converter.ToChar(int) 将整数转换为字符。如果程序喜欢这个字符,它会调用 `Read()` 来移动到流中的下一个字符,然后将字符附加到缓冲区。如果它发现一个不喜欢的字符,它就不会调用 `Read()` 来移动流中的指针位置。

这是我编写的 C++ 源代码解析方法:

public bool ParseCode(StringReader orig_code_stream)
{
    int iCharVal = orig_code_stream.Peek();
    if (iCharVal != -1)
    {
        orig_code_stream.Read();
        char c = Convert.ToChar(iCharVal);
        switch (c)
        {
            case '/':
                iCharVal = orig_code_stream.Peek();
                if (iCharVal == -1)
                    return false;

                c = Convert.ToChar(iCharVal);
                if (c == '*')
                {
                    orig_code_stream.Read();

                    token_val.Append("/*");
                    this.token_type = TokenType.comment;

                    iCharVal = orig_code_stream.Peek();
                    while (iCharVal != -1)
                    {
                        orig_code_stream.Read();
                        c = Convert.ToChar(iCharVal);
                        if (c == '/')
                        {
                            if (token_val.Length > 2
                                && token_val[token_val.Length-1] == '*')
                            {
                                token_val.Append("/");
                                return true;
                            }
                        }
                        else
                        {
                            token_val.Append(c);
                        }

                        iCharVal = orig_code_stream.Peek();
                    }
                }
                else if (c == '/')
                {
                    orig_code_stream.Read();

                    token_val.Append("//");
                    this.token_type = TokenType.comment;

                    iCharVal = orig_code_stream.Peek();
                    while (iCharVal != -1 && 
                       (c = Convert.ToChar(iCharVal)) != '\n')
                    {
                        orig_code_stream.Read();
                        token_val.Append(c);
                        iCharVal = orig_code_stream.Peek();
                    }

                    if (c == '\n')
                    {
                        orig_code_stream.Read();
                        token_val.Append(c);
                    }
                    return true;
                }
                token_val.Append("/");
                return false;

            case '#':
                this.token_val.Append('#');

                iCharVal = orig_code_stream.Peek();
                if (iCharVal == -1)
                    return false;

                c = Convert.ToChar(iCharVal);
                while (c == ' ' || c == '\r' || c == '\n' || c == '\t')
                {
                    this.token_val.Append(c);
                    orig_code_stream.Read();
                    iCharVal = orig_code_stream.Peek();
                    if (iCharVal == -1)
                        return false;
                    c = Convert.ToChar(iCharVal);
                }

                while (Char.IsLetter(c) && Char.IsLower(c))
                {
                    this.token_val.Append(c);
                    orig_code_stream.Read();
                    iCharVal = orig_code_stream.Peek();
                    if (iCharVal == -1)
                        break;
                    c = Convert.ToChar(iCharVal);
                }

                if (IsTokenPrePorcessor(this.token_val.ToString()))
                {
                    this.token_type = TokenType.pp;
                    return true;
                }
                return false;
            case '\'':
            case '\"':
            {
                char q = c;
                token_val.Append(q);
                while(true)
                {
                    iCharVal = orig_code_stream.Peek();
                    if (iCharVal == -1)
                        return false;

                    c = Convert.ToChar(iCharVal);
                    if (c == q)
                    {
                        if (token_val.Length >= 2)
                        {
                            if (!(token_val[token_val.Length - 1] == '\\' &&
                                token_val[token_val.Length - 2] != '\\'))
                            {
                                token_val.Append(q);
                                orig_code_stream.Read();
                                this.token_type = TokenType.cppstring;
                                return true;
                            }
                        }
                        else
                        {
                            token_val.Append(q);
                            orig_code_stream.Read();
                            this.token_type = TokenType.cppstring;
                            return true;
                        }
                    }
                    token_val.Append(c);
                    orig_code_stream.Read();
                }
            }
            case 'a':
            case 'b':
            case 'c':
            case 'd':
            case 'e':
            case 'f':
            case 'g':
            case 'i':
            case 'l':
            case 'm':
            case 'n':
            case 'o':
            case 'p':
            case 'r':
            case 's':
            case 't':
            case 'u':
            case 'v':
            case 'w':
            case 'x':
                token_val.Append(c);
                iCharVal = orig_code_stream.Peek();
                if (iCharVal == -1)
                    return false;

                c = Convert.ToChar(iCharVal);
                while (Char.IsLetter(c) || 
                       Char.IsDigit(c) || c == '_')
                {
                    token_val.Append(c);
                    orig_code_stream.Read();
                    iCharVal = orig_code_stream.Peek();
                    if (iCharVal == -1)
                        return false;

                    c = Convert.ToChar(iCharVal);
                }

                if (IsTokenKeyword(token_val.ToString()))
                {
                    this.token_type = TokenType.keyword;
                    return true;
                }
                else
                {
                    this.token_type = TokenType.code;
                    return true;
                }
                //return false;
            default:
                token_val.Append(c);
                iCharVal = orig_code_stream.Peek();
                if (iCharVal == -1)
                    return false;
                
                c = Convert.ToChar(iCharVal);
                while (c != '/' && c != '#' && 
                    !(Char.IsLetter(c) && Char.IsLower(c))
                    && c != '\'' && c != '\"')
                {
                    token_val.Append(c);
                    orig_code_stream.Read();
                    iCharVal = orig_code_stream.Peek();
                    if (iCharVal == -1)
                    {
                        if (token_val.Length > 0)
                        {
                            this.token_type = TokenType.code;
                            return true;
                        }
                        else
                            return false;
                    }
                    c = Convert.ToChar(iCharVal);
                }
                this.token_type = TokenType.code;
                return true;

        }

    }

    return false;
}

如何使用这个方法?这是你会在 Form 类中找到的 C# 代码块:

private string ConvertCppToHtml(string orig_line_val)
{
    StringReader strm = new StringReader(orig_line_val);
    StringBuilder bld = new StringBuilder();

    bld.Append("<table border=1" + 
        " bordercolor=#000000 bordercolordark=#000000>");
    bld.Append("<tr>");
    bld.Append("<td>");
    bld.Append("<font size=2 face=\'Courier New\'>");
    while (true)
    {
        Tokenizer tok = new Tokenizer();
        if (tok.ParseCode(strm))
        {
            string outputVal = ChangeCharToHtml(tok.Value);
            switch (tok.Type)
            {
                case Tokenizer.TokenType.comment:
                    bld.Append("<font size=2 color=\'#008000\'>");
                    bld.Append(outputVal);
                    bld.Append("</font>");
                    break;
                case Tokenizer.TokenType.cppstring:
                    bld.Append("<font size=2 color=\'#800000\'>");
                    bld.Append(outputVal);
                    bld.Append("</font>");
                    break;
                case Tokenizer.TokenType.keyword:
                case Tokenizer.TokenType.pp:
                    bld.Append("<font size=2 color=\'#0000ff\'>");
                    bld.Append(outputVal);
                    bld.Append("</font>");
                    break;
                default:
                    bld.Append(outputVal);
                    break;
            }
        }
        else
            break;
    }
    bld.Append("</font>");
    bld.Append("</td>");
    bld.Append("</tr>");
    bld.Append("</table>");

    return bld.ToString();
}

这段代码的作用是,使用一个 while 循环,它会持续创建一个 Tokenizer 对象。这个对象会一次解析一个 C++ 代码片段。根据标记的类型,它会使用 HTML 标签来高亮显示该标记。完成后,将生成一个简单的表格,显示带有漂亮语法高亮的 C++ 源代码。

我需要告诉你的最后一件事是关于这个名为 ChangeCharToHtml() 的方法。这个方法的作用是,它会将一些字符更改为 HTML 字符。这些字符包括:

  • "&" 更改为 "&"
  • "<" 更改为 "&lt;"
  • ">" 更改为 "&gt;"
  • "\"" 更改为 "&quot;"
  • " " 更改为 "&nbsp;"
  • "\t" 更改为 "&nbsp;&nbsp;&nbsp;"
  • "\r" 更改为 ""
  • "\n" 更改为 "<br/>"

为了替换这些字符,我使用了 Regex.Replace() 方法。这里有一点你需要知道,替换的顺序应该如上列表所示。这是代码:

private string ChangeCharToHtml(string orig_val)
{
    string retVal = orig_val;
    retVal = Regex.Replace(retVal, "&", 
             "&", RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, "<", 
             "&lt;", RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, ">", 
             "&gt;", RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, "\"", 
             "&quot;", RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, " ", 
             "&nbsp;", RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, "\t", 
             "&nbsp;&nbsp;&nbsp;", RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, "\r", "", 
             RegexOptions.IgnoreCase);
    retVal = Regex.Replace(retVal, "\n", 
             "<br/>", RegexOptions.IgnoreCase);

    return retVal;
}

关注点

希望你喜欢这个小教程。下载源代码并编译后,你应该仔细看看代码。你会发现 Tokenizer 类有很多可以改进的地方。例如,如果你真的很喜欢重构,你应该清理一下 ParseCode() 方法。每个 case 都可以被分离成 privateprotected 方法,这样你就可以在将它们集成到 ParseCode() 方法之前对每个解析方法进行单元测试。如果你有任何问题,请在下面的论坛中留言。感谢您的访问。

历史

  • 2006/1/28 -- 初稿。
© . All rights reserved.