☰
libTidy 消息系统扩展指南:为 HTML Tidy 5.8.0 新增警告/错误报告消息的完整三步流程
2026/10/16 2:58:14 网站建设 项目流程

【免费下载链接】foo_openlyrics

An open-source lyric display panel for foobar2000

项目地址:https://gitcode.com/gh_mirrors/fo/foo_openlyrics
点击查看免费下载

导读

本文以 HTML Tidy 5.8.0 的官方开发文档 MESSAGES.md 为骨架,系统讲解如何向libTidy添加一条新的警告/错误报告消息。libTidy 的警告/错误报告与脚注消息系统相当复杂,但大部分复杂性对开发者完全隐藏:你只需在三个位置登记——枚举宏、语言字符串表、消息分发表——即可完成一条新消息的全部接入工作。读完本文,你将掌握消息键(key)的定义方式、FOREACH_REPORT_MSG宏的作用机制、dispatchTable[]的调度原理,并能够借助仓库中真实案例(代理对BAD_SURROGATE_PAIR等三条消息)复现完整添加流程。

消息系统的整体设计

Tidy 拥有一个相当复杂的警告/错误报告与脚注消息系统,但绝大多数复杂度都被刻意隐藏,目的是让"添加一条新消息"尽可能简单。在 tidyenum.h 中定义了四个与此相关的枚举:

  1. tidyStrings:以TIDYSTRINGS_FIRST = 500起始且必须位于首位,是 Tidy 中全部字符串的清单(除其他枚举提供的字符串外)。注意:不要直接修改该枚举,而是修改预处理宏。
  2. TidyOptionId:新增选项时才需要,另有专门的 OPTIONS.md 文档说明。
  3. TidyConfigCategory:新增选项分类时才需要。
  4. TidyReportLevel:定义消息严重级别(TidyInfo、TidyWarning、TidyError等,见 tidyenum.h)。

所有枚举值在 libTidy 内部只按名称使用(客户端应用也不应信任数值),因此新字符串可以按英文首字母顺序插入——这有助于定期审计全部字符串。

tidyStrings中的消息必须定义在类似FOREACH_...(FN)的宏中。以报告消息为例,tidyenum.h 中的FOREACH_REPORT_MSG(FN)按字母顺序罗列了全部报告消息键。这些宏保证:每当字符串被添加或删除,用于本地化和键查找的数据结构(tidyStringsKeys[])都会自动同步更新,从而最大限度地限制开发者出错的可能。

从 tidyenum.h 的tidyStrings枚举结构可以看到,FOREACH_REPORT_MSG(MAKE_ENUM)被夹在REPORT_MESSAGE_FIRST与REPORT_MESSAGE_LAST之间,之后才是 accessibility 与控制台消息,最后以TIDYSTRINGS_LAST收尾——这也印证了"第一个/最后一个必须保留"的约束。

新增一条报告消息的三步流程

以下以官方文档的示例——新增三条警告消息BAD_SURROGATE_PAIR、BAD_SURROGATE_TAIL、BAD_SURROGATE_LEAD(它们对应 lexer.c 中代理对(surrogate pair)检查逻辑)——为线索,逐步演示完整流程。这三条消息目前在仓库中均已落地,可以作为对照范本。

Step 1:在FOREACH_REPORT_MSG(FN)宏中添加消息键

由于这三条是错误报告消息,它们属于tidyStrings枚举,并且恰好能归入以FOREACH_REPORT_MSG(FN)开头的宏。把消息键值嵌套在FN()语法中即可。在 tidyenum.h 中实际可见的落地形态为:

FN(BAD_SURROGATE_LEAD) \ FN(BAD_SURROGATE_PAIR) \ FN(BAD_SURROGATE_TAIL) \

它们被安排在宏的字母序位置(BAD_SUMMARY_HTML5之后、CANT_BE_NESTED之前)。这一步骤完成后,tidyStrings枚举会自动获得这三个新成员,REPORT_MESSAGE_FIRST/REPORT_MESSAGE_LAST的边界语义也由宏展开自动维护。

Step 2:在language_en.h中为消息键添加格式字符串

下一步是为消息键添加format字符串到 language_en.h。该文件是libTidy 的基础语言(base language),即使你打算支持其他语言,也必须先把格式字符串加到这里;后续各语言文件(如 language_de.h、language_fr.h、language_pt_br.h)会基于英文原文进行翻译。

文件内部按"使用位置"分组、组内通常按字母序排列。报告消息集中位于 language_en.h 一带,三条新消息的实际条目为:

{ BAD_SURROGATE_LEAD, 0, "Trailing (Low) surrogate pair U+%04X, with no leading (High) entity, replaced with U+FFFD." }, { BAD_SURROGATE_PAIR, 0, "Have out-of-range surrogate pair U+%04X:U+%04X, replaced with U+FFFD value." }, { BAD_SURROGATE_TAIL, 0, "Leading (High) surrogate pair U+%04X, with no trailing (Low) entity, replaced with U+FFFD." },

每个条目都是一个三成员的languageDictionaryEntry结构:key(消息键)、pluralForm(报告消息通常为 0)、value(格式字符串)。

关于格式字符串有两点关键约束:

  • 可用参数取决于输出例程:请查阅 message.c 确认你可以使用哪些参数类型(如%u、%s)。当前可用数据被限制在已有的消息输出例程范围内;不要在 libTidy 内部使用printf输出消息。
  • 不支持带编号的 printf 参数:参数按报告输出函数调用它们的顺序被消费,%1$s这类编号形式不可用。

以BAD_SURROGATE_PAIR为例,格式串中的两个U+%04X对应 message.c 中从va_list依次取出的两个uint参数c1、c2。

Step 3:在message.c的dispatchTable[]中登记消息键

最后一步(通常也是最后一步)是把消息键加入 message.c 中的dispatchTable[]结构。该结构决定了每条报告的TidyReportLevel(严重级别)和消息格式化器(消息的打印方式)。结构体定义如下:

static struct _dispatchTable { uint code; /**< 消息代码 */ TidyReportLevel level; /**< 消息默认的 TidyReportLevel */ messageFormatter *handler; /**< 报告使用的格式化器 */ uint next; /**< 如需同时输出多条消息,指向下一条代码 */ } dispatchTable[] = { ... };

三个代理对消息在表中的登记形态(message.c):

{ BAD_SURROGATE_LEAD, TidyWarning, formatStandard }, { BAD_SURROGATE_PAIR, TidyWarning, formatStandard }, { BAD_SURROGATE_TAIL, TidyWarning, formatStandard },

它们都使用TidyWarning级别与通用的formatStandard格式化器。formatStandard负责绝大多数报告的通用格式化,其内部通过switch (code)按消息键分支处理参数类型(见 message.c),代理对三个键统一走uint c1 = va_arg(...); uint c2 = va_arg(...)分支。

从消息键到实际输出:底层调用链

了解注册位置之后,再看消息从"发出"到"打印"的完整调用链,能帮助你正确选择格式化器与参数顺序。

报告入口:TY_(Report)与便捷函数

所有报告最终汇入 message.c 的TY_(Report)(TidyDocImpl* doc, Node *element, Node *node, uint code, ...)。它会:

  1. 用va_start收集变长参数;
  2. 调用静态函数vReport(message.c),后者遍历dispatchTable[]找到匹配的code;
  3. 取出该条目登记的handler(格式化器)与level(严重级别),通过va_copy复制参数后调用handler(...)生成TidyMessageImpl,再交给messageOut()输出;
  4. 若条目设置了next字段,则递归输出后续关联消息。

由于TY_(Report)需要传 NULL 且变长参数缺乏类型安全,libTidy 还提供了一批便捷报告函数(message.c),例如:

  • TY_(ReportAttrError)、TY_(ReportEntityError)、TY_(ReportFileError)
  • TY_(ReportEncodingError)/TY_(ReportEncodingWarning)
  • TY_(ReportSurrogateError)(doc, code, c1, c2)(message.c,代理对消息专用)

新增报告时应优先复用现有通用输出函数;实在无法满足时才新增便捷函数,以保持类型安全。

实际触发点:lexer.c 中的代理对检查

代理对消息的真实触发点在词法分析器 lexer.c。当 Tidy 解析文档中的字符引用(character reference)时,会判断:

  • 遇到低位代理(Low surrogate)且紧跟另一个实体字符,则尝试用GetSurrogatePair()合并;合并失败(如超范围)时报BAD_SURROGATE_PAIR(lexer.c);
  • 有高位代理(High surrogate)但无后续低位实体时,报BAD_SURROGATE_TAIL并用U+FFFD替换(lexer.c);
  • 只有低位代理而无高位配对时,报BAD_SURROGATE_LEAD并用U+FFFD替换(lexer.c)。

这解释了三条消息格式串中参数的含义:U+%04X打印的是十六进制码点,U+FFFD是 Unicode 替换字符。

端到端验证:回归测试用例

仓库中已有针对这三条消息的回归测试,可直接作为"新增消息后的验收范本"。

输入用例 case-483@1.html 构造了四种场景:

<p>valid pair; no warning - &#55357;&#56845;</p> <p>out of range pair - &#55359;&#57342;</p> <p>no trailing pair - &#55357;no tail</p> <p>no leading pair - no lead&#56845;</p>

其中合法代理对不产生警告,其余三种非法形态分别触发超范围对、无尾对、无头对。期望输出 case-483.txt 精确记录了三条警告:

line 9 column 6 - Warning: Have out-of-range surrogate pair U+D83F:U+1FFFE, replaced with U+FFFD value. line 10 column 6 - Warning: Leading (High) surrogate pair U+D83D, with no trailing (Low) entity, replaced with U+FFFD. line 11 column 6 - Warning: Trailing (Low) surrogate pair U+DE0D, with no leading (High) entity, replaced with U+FFFD. Tidy found 3 warnings and 0 errors!

这个测试同时验证了三件事:消息键已正确注册、格式化器参数顺序正确、严重级别(TidyWarning)符合预期——正是文档"三步流程"的完整闭环。

客户端如何接收这些消息

从 tidylib.c 可以看到,libTidy 对外提供了tidySetReportFilter/tidySetReportCallback等回调接口。因此,无论是命令行tidy还是集成 libTidy 的应用(例如本仓库 openlyrics_build 所构建的静态库消费者),都可以通过消息键名或TidyReportLevel对警告/错误进行过滤、格式化与展示,而无需关心消息内部实现。

小结

向 libTidy 添加一条警告/错误报告消息只需三步:

步骤修改位置内容
Step 1tidyenum.h 的FOREACH_REPORT_MSG(FN)以FN(KEY)形式加入消息键
Step 2language_en.h 的报告消息段加入{ KEY, 0, "格式字符串" },注意参数类型与消费顺序
Step 3message.c 的dispatchTable[]登记{ KEY, 级别, 格式化器 }

之后通过TY_(Report)或便捷函数发出报告即可。若需调整参数签名,请先阅读message.c中已有格式化器的实现;最稳妥的做法是搜索一个与你需求相似的消息键,沿着它在上述三处的定义与用法完整复制一份——正如文档总结的那样:"这并不难。"

【免费下载链接】foo_openlyrics

An open-source lyric display panel for foobar2000

项目地址:https://gitcode.com/gh_mirrors/fo/foo_openlyrics
点击查看免费下载

相关推荐

上一篇:Workspace Daemon API(ws-daemon API)深度解析:Gitpod 工作区内容管理与容器底层操作的 gRPC 接口指南
下一篇:Ceph 内核客户端挂载全解析:mount.ceph 挂载辅助程序使用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询