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






4.38/5 (4投票s)
2006年1月31日
5分钟阅读

35800

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 字符。这些字符包括:
- "&" 更改为 "&"
- "<" 更改为 "<"
- ">" 更改为 ">"
- "\"" 更改为 """
- " " 更改为 " "
- "\t" 更改为 " "
- "\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, "<",
"<", RegexOptions.IgnoreCase);
retVal = Regex.Replace(retVal, ">",
">", RegexOptions.IgnoreCase);
retVal = Regex.Replace(retVal, "\"",
""", RegexOptions.IgnoreCase);
retVal = Regex.Replace(retVal, " ",
" ", RegexOptions.IgnoreCase);
retVal = Regex.Replace(retVal, "\t",
" ", RegexOptions.IgnoreCase);
retVal = Regex.Replace(retVal, "\r", "",
RegexOptions.IgnoreCase);
retVal = Regex.Replace(retVal, "\n",
"<br/>", RegexOptions.IgnoreCase);
return retVal;
}
关注点
希望你喜欢这个小教程。下载源代码并编译后,你应该仔细看看代码。你会发现 Tokenizer
类有很多可以改进的地方。例如,如果你真的很喜欢重构,你应该清理一下 ParseCode()
方法。每个 case
都可以被分离成 private
或 protected
方法,这样你就可以在将它们集成到 ParseCode()
方法之前对每个解析方法进行单元测试。如果你有任何问题,请在下面的论坛中留言。感谢您的访问。
历史
- 2006/1/28 -- 初稿。