打开QT的编辑器,第一个想到的往往是记事本、Word这类通用工具。但真当手里攒了一篇幾百楼的论坛体长篇时,你会发现通用文本编辑器反而成了最大的短板——楼层号全靠手打、引用回复对不齐、修改中间某一楼后面全部要重新编号、导出格式还得一句句手动清理。
我就是被这些破事逼到动手写了一个基于Qt的txt读写论坛体编辑器。这篇文章就把这个项目从需求梳理、界面设计到核心代码实现、踩坑记录完整拆一遍,希望能给同样在折腾文本工具、或者想用Qt做小工具落地的人一点参考。
1. 需求拆解:论坛体编辑器到底在解决什么问题
1.1 论坛体创作者的真实痛点
在动手写代码之前,必须先搞清楚目标用户到底在为什么事情烦心。所谓论坛体,就是模仿论坛帖子格式进行创作的一种文本形式,比如开头是“标题:xxx”,然后是“楼主”“1楼”“2楼”的楼层结构,正文里穿插“引用”“回复”“点赞”“楼主留言”,有些人还会加时间戳、IP归属地模拟、折叠楼等细节。
这种文体在语C圈、同人圈、短视频文案脚本里非常常见,特点是:结构性强、重复性劳动多、修改成本高。
我见过太多人用Word或者手机备忘录写论坛体,遇到的情况基本就这几种:楼上引用手工复制粘贴对不齐;删掉中间一条回复后面全部楼号都乱了,需要手动改几十处;想导出成txt发到社交平台,发现格式乱七八糟还要手工清理;写到第三十个楼层之后滚动查找很痛苦。
这些痛点汇总之后,需求其实非常清晰:一个能自动管理楼层结构、保持引用格式统一、支持快捷插入回复元素、并且基于txt读写实现轻量保存的专用编辑器。
1.2 为什么不用现成的Markdown编辑器或增强记事本
可能有人会说,用Typora或者VS Code加插件不就行了吗,为什么非要自己写一个。
这个问题的答案很简单:现有工具解决的是通用文本编辑问题,但论坛体的格式规则太特殊了。它既不是Markdown那种标准化语法,也不是Word那种富文本排版,而是一套民间形成的、平台相关的口头约定。
比如常见的论坛体格式:
标题:今天发现合租室友是我推 楼主:事情是这样的,昨天晚上我加班回家…… 1楼:蹲一个后续 楼主回复(1楼):别急,我慢慢写 2楼:不是,你们搞语C的真是啥都能编吗 引用(2楼):不是,你们搞语C的真是啥都能编吗 2楼回复(楼主):编不编你往下看就知道了这个格式里,“楼主回复(1楼)”后面其实是套了一个引用层的。如果靠通用编辑器,你只能老老实实缩进对齐,但缩进多少个空格纯靠肉眼。用我做的这个编辑器,插入一条“回复某楼”的操作就是点一个按钮,格式自动生成,引用关系自动对齐,楼号自动更新。
这就像你在Excel里手打公式和用透视表的区别:都能做,但后者是专门为结构化数据处理而生的。论坛体编辑器本质上就是把“帖子格式”这种半结构化文本做了一层轻量化管理。
1.3 技术选型:为什么选Qt而不是Electron或Python Tkinter
作为常年做桌面工具的人,我选Qt有几个很实在的理由。
Qt的C++核心保证了文本处理的性能。论坛体动辄几百楼、几万字的文本量虽然不算大,但你要知道在编辑过程中需要频繁做插入、替换、重编号的操作,Qt的信号槽机制和QTextDocument的底层模型对这些操作支持得很好,不会出现大型文档编辑卡顿的情况。
Qt对txt读写和编码处理的支持一直在迭代,处理UTF-8、GBK、UTF-16这些常见文本编码非常顺手。这一点对我这个项目来说是刚需,因为很多论坛体老文从网上下载下来都是GBK编码的txt,如果用默认的UTF-8直接打开,楼上楼下全是乱码。我在后面会专门讲编码的处理。
Qt是跨平台的。我用Windows为主,但也有朋友要在macOS上跑,Qt一套代码编译两个平台,省去很多重复开发时间。
对比其他方案:Electron界面是好看,但打包体积动辄150MB起步,而且内存占用对轻量文本工具来说有点过重;Python Tkinter上手是快,但发布部署要带Python环境,交互细节也难做好。Qt用QSS做样式定制,配合自绘控件,界面能达到“小而美”的效果,打包用windeployqt或者linuxdeployqt,整体体积控制在20MB以内。
2. 界面布局与交互设计
2.1 功能区划分与用户使用动线
一个编辑器好不好用,界面信息的排布比功能多少更关键。我把主窗口分成三个区域。
左侧是整个帖子的楼层导航树,用QTreeWidget实现,动态显示当前所有楼层和回复关系。点击任意一层,中间的编辑区会直接跳转到对应内容。这个导航树对几百楼的文本来说非常重要,类似IDE里的文件树,省去了滚动查找的时间。
中间是主编辑区,用QPlainTextEdit作为基底。为什么不选QTextEdit?因为QPlainTextEdit本身就是为纯文本设计的,处理几万行纯文本时性能和内存占用都更优,论坛体的txt定位决定了这里的内容绝大多数是纯文本,没必要引入富文本的重量级模型。我现在这个项目里,主编辑区只用QPlainTextEdit,后续如果要加局部颜色高亮,再叠加QSyntaxHighlighter做语法高亮也不迟。
右侧是一个模板与操作面板,包含“插入楼主”“插入楼层”“插入引用”“插入楼主回复”“插入分割线”等按钮。这个面板相当于整个工具的快捷键面板,所有高频操作都能一键完成。
整个用户动线是:左侧选楼层→中间编辑内容→右侧点插入模板。三个区域通过Qt的信号槽机制联动,交互循环很自然。
2.2 富交互细节:自动编号与语法高亮
界面设计的核心不只是好看,更重要的是让用户“少做选择”。我在这个编辑器里实现了两个关键的交互细节。
第一个是楼层自动编号。编辑区本身不存储楼号,楼号是由程序根据段落顺序动态计算的。用户在模板操作面板点“插入楼层”,编辑区插入的不是“5楼:”这样写死的文字,而是一个占位符。显示的时候格式化引擎实时计算当前楼号,生成正确的楼层编号。这样做的最大好处是:你删掉第12楼之后,第13楼到第50楼的编号全部自动更新,不再需要手动调整。
第二个是论坛体语法高亮。我用QSyntaxHighlighter写了一个论坛体专用的高亮规则:楼主标识显示为蓝色加粗;“引用”标识显示为灰色斜体;“1楼”“2楼”这类楼层前缀显示为绿色;分割线显示为暗红色。高亮只影响显示,不影响真实存储的txt内容。这一点很重要:导出txt时得到的是干净的纯文本,不会带额外标记。
2.3 快捷键体系设计
光有按钮还不够,高频操作用快捷键才顺手。我设计了一套符合直觉的快捷键体系:插入楼层用Ctrl+1、插入引用用Ctrl+2、插入楼主回复用Ctrl+3、插入分割线用Ctrl+4、插入楼主用Ctrl+5。保存用Ctrl+S,另存为Ctrl+Shift+S。楼层导航树上下移动用Alt+方向键。
快捷键的意义在于让用户的双手保持在键盘上,尤其是当你需要连续插入多条回复的时候,点鼠标和按快捷键的速率差距是数量级的差异。实测下来,熟练用户在编辑一篇100楼的论坛体时,纯键盘操作比鼠标操作节省大约40%的时间。
3. Qt txt读写核心实现
3.1 QFile、QTextStream与编码处理
这一块是整个项目的硬核部分,也是网上问得最多的地方:Qt怎么读写txt、中文编码怎么处理、文件太大会不会卡死。
先看基础读写。Qt里对文件读写最经典的组合是QFile加QTextStream。QFile负责管理文件句柄,QTextStream提供文本流操作,会自动处理缓冲、换行符转换等问题。
// 读取txt文件 QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, "提示", "文件打开失败:" + file.errorString()); return; } QTextStream in(&file); // 关键:统一使用 UTF-8 编码读取 in.setEncoding(QStringConverter::Utf8); QString content = in.readAll(); file.close();写入类似,只是模式改为QIODevice::WriteOnly。
// 写入txt文件 QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, "提示", "文件保存失败:" + file.errorString()); return; } QTextStream out(&file); out.setEncoding(QStringConverter::Utf8); out << content; file.close();这里有一个非常容易踩的坑:编码。如果你用老版本的Qt或者手动指定QTextCodec::setCodecForLocale的方式来处理GBK,一旦文件里同时包含GBK和UTF-8内容,结果就是一半正常一半乱码。我的方案是:默认打开时先检测文件前三个字节是否为UTF-8 BOM(EF BB BF),如果有BOM,用UTF-8读;如果没有BOM,尝试用UTF-8读,如果出现无法解析的字符,再转为GBK重新读取。
检测编码的函数很简单:
QString detectEncoding(const QByteArray &data) { // 有 UTF-8 BOM if (data.size() >= 3 && (uchar)data[0] == 0xEF && (uchar)data[1] == 0xBB && (uchar)data[2] == 0xBF) { return "UTF-8-BOM"; } // 尝试 UTF-8 解码,如果有无效字符则回退到 GBK QTextDecoder decoder(QStringDecoder::Utf8); QString result = decoder.decode(data); if (decoder.hasError()) { return "GBK"; } return "UTF-8"; }写入时我统一用UTF-8无BOM格式。为什么不用带BOM?因为很多社交平台的导入工具对BOM很敏感,会把\uFEFF识别成一个特殊字符导致开头多一个空行。UTF-8无BOM是当前跨平台兼容性最好的方案,Linux、macOS、Windows的记事本新版都支持得很好。
3.2 QSaveFile:原子保存防止断电丢数据
文本编辑器最怕的是什么?写到一半程序崩了,文件损坏。这个问题用QFile直接WriteOnly保存就存在隐患:如果写入过程中发生异常,文件可能只写了一半。
Qt提供了一个非常实用的类:QSaveFile。它的逻辑是:先写入一个临时文件,确认所有数据都成功落盘后,再使用rename操作原子替换目标文件。整个过程对用户来说是透明的,但安全性提升了一个档次。
bool saveContent(const QString &filePath, const QString &content) { QSaveFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { return false; } QTextStream out(&file); out.setEncoding(QStringConverter::Utf8); out << content; if (!file.commit()) { // commit 失败则临时文件会被自动清理 return false; } return true; }QSaveFile的commit操作内部调用了系统的rename逻辑,在Windows上相当于MoveFileEx,在Linux上是rename。这个机制不需要额外引入第三方库,直接就用Qt自带的,强烈推荐。
3.3 大文本处理与性能优化
论坛体虽然平均只有几万字,但架不住有些用户会把一个中长篇同人文灌进来,十几万字也是可能的。QPlainTextEdit对十万字级别的文本处理其实已经比较吃力了,尤其是使用setPlainText一次性设置全部内容,以及每次编辑都触发全文重新计算楼层号,都会造成明显卡顿。
优化路径有两个方向。第一是分块加载,一个文件如果超过1MB,不再一次性读入内存并setPlainText,而是用QTextStream按行读取,通过appendPlainText逐批插入,配合QApplication::processEvents让界面在加载过程中保持响应,避免“程序未响应”的假死状态。
第二个方向是延迟计算。楼层导航树的更新不要每次输入都触发,而是监听QPlainTextEdit的textChanged信号,用QTimer做500毫秒的防抖,用户停止输入半秒后才重新扫描楼层结构。这样从“每敲一个字就全量扫描”变成“停下来才扫描”,性能开销小了一个数量级。我在第5章还会专门讲这个防抖的实现。
3.4 自动备份与临时文件管理
写作工具一定要考虑数据安全。我做了两重保障:第一重是手动保存用QSaveFile保证原子性;第二重是自动备份。
具体逻辑是:程序启动后,如果检测到当前打开的txt文件存在,先复制一份到%APPDATA%/ForumEditor/backup/目录下,文件名加上时间戳。每次成功保存后,也做一次增量备份,保留最近10个版本。这样就算用户误操作把内容改没了,也能从备份恢复到之前的状态。实现上就是QFile::copy加QDir遍历清理旧备份,代码量不大但关键时刻能救命。
4. 论坛体格式化引擎
4.1 楼层扫描与自动编号原理
格式化引擎是论坛体编辑器和普通记事本的本质区别。它不是做字符串拼接,而是维护一个结构化的楼层列表。
我在内存里用一个QVector<ForumBlock>来维护整个帖子的结构:
struct ForumBlock { enum BlockType { Title, // 标题 Host, // 楼主内容 Floor, // 普通楼层回复 Quote, // 引用 HostReply, // 楼主回复某楼 Divider, // 分割线 MetaInfo // 其他信息(时间戳、IP属地等) }; BlockType type; int floorNumber; // 楼层号,-1表示无效 int quoteTarget; // 引用的目标楼层,-1表示无 QString content; // 内容 QString author; // 作者标识 };当用户点击“插入楼层”按钮时,程序并不直接在文本里插入“3楼:”这样一个固定的字符串,而是插入一个特殊的标记行,比如[[FLOOR]]。然后格式化引擎从头到尾扫描整个文档,用正则识别出这些标记,再根据它们在文档中的顺序,依次生成“1楼”“2楼”“3楼”这样的实际编号。
扫描逻辑的核心是这样一段代码:
void ForumFormatter::rescanDocument() { const QString text = editor->toPlainText(); const QStringList lines = text.split('\n'); blocks.clear(); int floorCounter = 1; for (const QString &line : lines) { QString trimmed = line.trimmed(); if (trimmed.startsWith("[[TITLE]]")) { blocks.append({ForumBlock::Title, -1, -1, trimmed.mid(9), "标题"}); } else if (trimmed.startsWith("[[HOST]]")) { blocks.append({ForumBlock::Host, 0, -1, trimmed.mid(8), "楼主"}); } else if (trimmed.startsWith("[[FLOOR]]")) { blocks.append({ForumBlock::Floor, floorCounter++, -1, trimmed.mid(9), QString("访客")}); } else if (trimmed.startsWith("[[QUOTE:") && trimmed.endsWith("]]")) { int target = trimmed.mid(8).trimmed().toInt(); blocks.append({ForumBlock::Quote, -1, target, QString(), "引用"}); } // ... 其他类型类似 } // 更新导航树和状态栏 updateNavigationTree(); }当然,这只是核心逻辑的精简版,实际项目中还需要处理嵌套引用、楼主回复跨楼挂载、楼层删除后再编号等边界情况。但核心思路就是:文本是源,结构是派生数据,结构永远跟文本保持同步。
4.2 引用与回复层级处理
论坛体里最头疼的是“楼中楼”结构,也就是一层楼下面挂多条回复。为了在txt纯文本里表达这种嵌套关系,常见的做法是缩进对齐。我的格式化引擎用两个空格作为一级缩进。
当用户点“插入引用”时,弹出的输入框让用户选择“引用哪一楼”,格式器自动生成:
引用(3楼): 我看你这话说得不太对当用户点“插入楼主回复”时,自动生成:
楼主回复(3楼): 我怎么不对了,你往下看这里的“(3楼)”不是手工输入的,而是格式器根据当前光标所在位置自动判断的。具体做法:光标当前停留的那一行如果是“[[FLOOR]]”标记,那目标楼号就是当前未分配的楼层号;如果光标在某个已有引用块内部,那就取最近的一个楼层标记作为目标。
层级关系我会在解析时维护一个栈,遇到引用就压栈,遇到普通楼层就出栈,复杂度是O(n),对几千行文本完全无压力。
4.3 导出参数与样式
编辑和导出是两个环节,导出时可以选择要不要保留标记符号。我提供三种导出模式:精简模式(只保留正文和楼层号,去除所有[[标记]])、标准模式(保留楼层号和引用结构)、完整模式(导出所有内容,包括时间戳、IP等)。
导出用QAioDevice或者QSaveFile写新文件,和保存编辑文件隔离。这个设计的理由是:编辑文件用的是带标记的中间格式,方便格式化引擎识别;用户要发布的txt是干净的最终格式。两种格式分离,既保证了编辑效率,又保证了最终作品的质量。
5. 常见问题与排查实录
5.1 Qt环境安装与项目配置的坑
这个项目从Qt 5.15开始写,后来迁移到Qt 6.5 LTS。碰到的第一个坑就是Qt安装。
Windows下建议直接下载Qt官方的在线安装包,选择Qt 6.5.3版本下的MSVC 2019 64-bit套件,同时勾选Qt Debug和Qt Release。这里有个容易踩坑的点:如果你用的编译器是MinGW,而安装时选了MSVC套件,编译会直接报错。建议先确定自己的编译器再对应安装。Visual Studio用户选MSVC,别的用户统一选MinGW。
Linux下用apt安装也行,但Ubuntu 20.04的源里默认是Qt 5.12,做基础工具够用,但某些新API没有。如果坚持用Qt 6,可以从Qt官网下载安装程序,或者直接用aqtinstall命令行工具:
pip install aqtinstall aqt install-qt linux desktop 6.5.3 linux_gcc_64这个工具比图形安装程序更适合在服务器或者无桌面环境下安装,也可以配合CI流水线做自动化。
5.2 中文乱码,永远是对编码的两个传统误解
写Qt txt工具的同行一定经历过乱码问题。乱码本质上是写入和读取时的编码解释不一致导致的,但有不少新手以为“设置setCodecForLocale就能解决一切”。
QTextCodec::setCodecForLocale在Qt 5里已经标记为deprecated,它只影响控制台输出的默认编码,并不影响QFile和QTextStream的读写。正确做法是给每个QTextStream显式设置编码,在同一项目中统一使用“文件读写一律UTF-8”的约定。
另外还有一个容易忽略的坑:就算你的程序读写都是UTF-8,但如果系统本身不是UTF-8区域(比如Windows中文系统默认GBK),那么QFileDialog::getOpenFileName返回的路径如果是中文,会有极低概率出现路径定位错误。解决办法是统一使用QString存储路径,不要在中间环节手动转码,让Qt内部处理系统编码转换。
5.3 程序发布后打不开:DLL缺失与目录结构
开发环境跑得好好的,打包发给别人,对方双击没反应,这种问题十有八九是运行库没带全。
Windows下Qt程序发布,我通常用这个流程:首先用Qt的Release模式编译;拿到生成的exe后,打开Qt命令行工具,进入exe所在目录;运行windeployqt.exe工具:
cd /d "D:\build\forum-editor\release" D:\Qt\6.5.3\msvc2019_64\bin\windeployqt.exe forum-editor.exewindeployqt会自动把exe依赖的Qt库目录和DLL复制过来。之后再把程序用到的style sheet文件(qss)、翻译文件(qm)、备份目录等额外资源按相对路径放好,整个发布目录大概就是exe加一堆DLL加platforms目录。
这里有个坑:windeployqt默认不会复制某些第三方DLL,比如openssl。如果你的程序用了Qt Network并且需要HTTPS,就需要手动把libcrypto-3-x64.dll和libssl-3-x64.dll复制到exe目录。如果漏掉,程序启动不报错,但访问网络功能会静默失败。
如果打包后在其他机器上仍然双击没反应,可以直接打开cmd,把exe从命令行拖进去运行,看有没有错误输出。这一步能定位90%的启动问题。
5.4 大文件保存时卡界面的解决方案
早期版本我用的是“保存按钮点击后,同步写文件”的方式,一旦文件到10万行规模,保存操作会让界面卡住2到3秒。这在Windows上表现为窗口白屏、任务栏“未响应”,给用户的体验非常差。
解决办法有两个配合使用:第一,保存操作放到子线程。用QtConcurrent::run启动一个后台任务写文件,写完通过信号通知主线程更新状态栏;第二,写文件时用分块写,不要一次性把整个QString交给QTextStream。
实际上,QTextStream内部有缓冲,一次性写入几十MB字符串也不至于卡太狠,但如果你在主线程做这件事,再遇上Windows Defender全盘扫描文件,那卡顿就不可避免。所以我的建议是:保存操作一律子线程,核心代码很短:
void MainWindow::onSave() { QString content = editor->toPlainText(); QString filePath = this->filePath; QtConcurrent::run([this, content, filePath]() { bool ok = saveContent(filePath, content); emit saveFinished(ok); }); }不要害怕跨线程访问。注意,内容在进入线程前先通过toPlainText()拷贝一份,不要在线程里访问编辑器的UI对象。线程只负责写文件,操作完成发信号,这是Qt线程最安全的基本模式。
6. 扩展思路:往国际化、JSON配置和自定义控件走一步
6.1 Qt国际化(i18n)的落地方式
论坛体编辑器的目标用户目前以中文为主,但如果后续放到GitHub开源,或多语言支持能拉来更多用户。Qt的国际化方案很成熟:先用tr()包裹所有可翻译字符串,再用lupdate工具扫描源码生成ts文件,翻译后用lrelease生成qm文件,最后在main函数里根据QLocale加载对应的qm。
int main(int argc, char *argv[]) { QApplication app(argc, argv); QTranslator translator; const QStringList uiLanguages = QLocale::system().uiLanguages(); for (const QString &locale : uiLanguages) { const QString baseName = "forumeditor_" + QLocale(locale).name(); if (translator.load(":/i18n/" + baseName)) { app.installTranslator(&translator); break; } } MainWindow window; window.show(); return app.exec(); }实际操作中,我自己项目里没有全部接入翻译,只是界面上的静态按钮和菜单做了国际化,动态生成的楼层内容保持原文。国际化做到这个程度已经能覆盖大部分出海的场景了。
6.2 用JSON做配置与自定义格式化模板
随着功能迭代,硬编码的“楼层标记格式”渐渐满足不了所有用户。有人希望显示为“#1楼”,有人喜欢带时间,有人希望引用时自动加日期。与其不断在代码里加switch分支,不如把规则提取到配置文件里。
用Qt的QJsonDocument读写一个config.json非常方便:
{ "floorFormat": "[[FLOOR]]", "quoteFormat": "[[QUOTE:%1]]", "hostReplyFormat": "[[HOST_REPLY:%1]]", "editorFontSize": 13, "backupMaxCount": 10 }程序启动时读取这个JSON,配置所有格式化标记和编辑器参数。修改标记格式不再需要重新编译,这个设计也方便在不同平台之间迁移用户配置。
6.3 自定义进度条与保存动画
每次保存完成弹一个对话框已经过时了,我想做成一个更顺滑的交互:保存启动时,在状态栏右侧出现一个mini进度条动画,显示写入进度;保存完成动画消失,状态栏显示“已保存 13:45:22”。
Qt里做这个可以用QProgressBar放到QStatusBar里,配合定时器模拟进度,等真实保存信号返回后完成动画。虽然论坛体文本通常秒存,这个动画时间极短,但对用户的心理暗示是有价值的:程序知道自己在做什么,而且保存这件事是可视化的。
7. 踩坑实录:那些文档里找不到的细节
写这个项目过程中,最让我崩溃的是三个小问题。
第一个是QPlainTextEdit的setPlainText在Windows上偶尔会触发布局重算,当文本量超过几百万字符时会有短暂的白色闪烁。解决办法是先用document()->setPlainText()替代控件层方法,它绕过控件级布局刷新。但这个接口在Qt 6里变成了setPlainText的底层实现,效果就不明显了。如果还闪烁,可以用QTextCursor逐块插入,再配合setUpdatesEnabled(false)临时禁用刷新。
第二个是QTreeWidget里节点ID和楼层号对齐问题。导航树要稳定跟踪某个楼层的选中状态,就不能用显示文本匹配,因为文本会变。我用Qt::UserRole存每个节点的持久化ID,不管楼层号怎么改,都能保证导航树选中状态不跳。这也是一个容易被忽略的数据建模问题:区分显示数据和底层数据。
第三个是备份目录的权限。在Windows下如果用户以普通用户运行,%APPDATA%目录通常可写;但在公司的域控环境或有安全软件的环境下,这个目录可能被限制写入。我的处理是启动时检测备份目录是否可写,如果不可写,自动降级到程序目录下的backup文件夹,并在状态栏提示用户。
这三点都不是什么高深技术,但实操中如果不注意,就会变成用户反馈里的“奇怪bug”。
最后再分享一个小技巧
关于论坛体编辑器,我最后想补充一点自己的心得:解析和渲染一定要做到“解析层和显示层分离”。我在早期版本里试过把楼号直接写进textarea的文本里,后来发现一旦用户手工删掉一个“3楼:”的文本,后面所有楼层号全部错乱。后来改成用格式化标记占位、显示层动态渲染的架构之后,用户再怎么乱删乱改,都不会破坏数据结构。
做这类面向特定场景的小工具,最忌讳的就是“把所有规则写死在文本里”。文本应该永远是最原始的数据,规则和格式应当是文本之上的解释层。这个设计原则不仅适用于论坛体编辑器,也适用于你之后做的所有文本处理类工具。保持结构清晰、层次分明,以后加功能、修bug都会从容很多。