SuperAPI AI 工具集

kmp-html-entity-decoder

编程与开发

查看项目

更新于 10-17 16:15

技能介绍

用途

kmp-html-entity-decoder 面向 Kotlin Multiplatform 共享代码:当平台自带的标记辅助函数只存在于某一个目标平台,且这些函数通常还会顺带剥离标签、做超出需求的事情时,可以用纯共享代码实现字符实体解码。资料建议把解码逻辑放在解析器与展示层都已依赖的最低模块中,实现只需一张命名实体表加两条正则表达式。

主要能力

  • 解码命名实体:遍历固定的命名实体映射表,对输入文本做替换。
  • 解码十六进制数字实体:形如 A、A,正则使用 &#[xX]([0-9a-fA-F]+);?。
  • 解码十进制数字实体:形如 A,正则使用 &#(\d+);?。
  • 数字解析使用 toIntOrNull,超长数字溢出时返回 null,匹配内容原样保留,不会中断整体操作。
  • 码点转换需覆盖代理对:0..0xFFFF 直接用 Char(cp);0x10000..0x10FFFF 通过 charArrayOf(Char(0xD800 + (v shr 10)), Char(0xDC00 + (v and 0x3FF))).concatToString() 转为代理对;超出 Unicode 范围返回 null,保留原文。CharArray.concatToString() 在共享标准库中,可继续留在 common 代码。

示例(资料中经适配的代码):

fun decodeEntities(text: String): String {
    var result = text
    for ((entity, char) in NAMED) {
        result = result.replace(entity, char, ignoreCase = true)
    }
    result = Regex("""&#[xX]([0-9a-fA-F]+);?""").replace(result) { m ->
        m.groupValues[1].toIntOrNull(16)?.let { codePointToString(it) } ?: m.value
    }
    result = Regex("""&#(\d+);?""").replace(result) { m ->
        m.groupValues[1].toIntOrNull()?.let { codePointToString(it) } ?: m.value
    }
    return result
}

private fun codePointToString(cp: Int): String? = when {
    cp in 0..0xFFFF -> Char(cp).toString()
    cp in 0x10000..0x10FFFF -> {
        val v = cp - 0x10000
        charArrayOf(Char(0xD800 + (v shr 10)), Char(0xDC00 + (v and 0x3FF))).concatToString()
    }
    else -> null
}

使用方式与实现陷阱

资料以“陷阱”形式给出使用要点,均为实现层面的事实:

  1. 超过 16 位的码点会中断操作。 Char(code: Int) 在码点超出 Char.MIN_VALUE.code..Char.MAX_VALUE.code 时抛异常。😀(emoji)能正常解析为整数,随后中断解码。所有大于 0xFFFF 的码点必须转成代理对,且需手写转换,因为便捷辅助函数仅限平台。
  2. 先跑命名实体表会一次解码两层。 表按插入顺序迭代(mapOf 构建的是链式哈希映射),若 & 实体排在尖括号实体之前,&amp;lt; 先变成 &lt;,几轮后变成 <;数字表达式同理,&amp;#39; 会变成 &#39; 再变成撇号。资料给出两种修复且强调二者不同:把 & 替换放到所有遍历(包括数字遍历)的最后;或改为单次扫描,逐字符判断并在构建器中输出替换结果,永不回读已输出内容——后者在结构上保证每次调用只解一层,且更快。
  3. 表与正则的大小写必须一致。 表替换带 ignoreCase = true,而正则若只写小写 x,则 &#X41; 匹配不到、原样上屏,&#x41; 正常解码。应在正则中使用 [xX]。
  4. 可选尾分号让解码比格式更贪婪。 ;? 配合贪婪数字串意味着 &#65 无终止符也会解码,普通文本中该形状会被改写。真实编码器总会输出分号;只有确认存在省略分号的产出方时才接受无终止符形式,且不要把它放进命名表——无终止符的名字没有明确结尾。
  5. 固定表之外的实体会原样保留。 数字遍历救不了命名实体,因为名字不带数字。应有意识地决定所列名字是否覆盖输入实际内容,并用 grep 在抓取的响应或保存的测试样本上打印幸存者(不要在自身源码上跑):
grep -rEo '&[a-zA-Z][a-zA-Z0-9]{1,10};' <fixtures-dir> | sed 's/.*://' | sort -u
  1. 只在边界解码一次、且在解析之后。 不要在解析器与上层各解一次,否则用户输入的普通 & 会消失;应先解码捕获字段,而非原始行,否则 &#91; 会先被解码成左方括号,制造出原输入中不存在的记录分隔符。
  2. 解码不是净化。 该函数把实体文本还原为字符,与转义相反。解码后的文本不得拼进标记、查询或 shell 命令;应在我方模型边缘解码,再在输出路径重新转义。

验证方法

资料给出以下检查命令与断言方式:

确认只有一个解码器、共享代码没有去调平台实现:

grep -rn "fun decodeEntities\|fun decodeHtmlEntities" --include='*.kt' .

查找所有调用点,确认没有叠加解码:

grep -rn "decodeEntities(\|decodeHtmlEntities(" --include='*.kt' . | grep -v "fun decode"

确认数字遍历覆盖十六进制标记的两种大小写:

grep -rn "&#x(\[0-9a-fA-F\]\|&#\[xX\]" --include='*.kt' .

用人工写好的期望值断言单次调用结果,因为一次调用必须恰好去掉一层:decode("&amp;lt;") 必须等于 "&lt;",decode("&amp;#39;") 必须等于 "&#39;",decode("a & b") 必须原样返回。逐实体遍历的实现会在这前两项上失败(返回 < 和 ')。不要用解码器自身做往返测试:decode(decode(s)) == decode(s) 在三组输入上对有缺陷的解码器都成立(过度解码在首次调用内完成,输出已是不动点),而对正确的解码器不成立(每次调用剥一层)。幂等是错误的性质,用它做测试会给该缺陷放行。

还应喂入 emoji 实体(&#128512;)和超出 Unicode 范围的实体(&#1114112;):前者必须产出对应字符,后者必须原样返回,两者都不能中断操作。

适用场景与限制

适用于 Kotlin Multiplatform 共享代码中需要解码 HTML 实体、又不希望依赖单一平台标记辅助函数(且不想连带剥离标签)的场景。限制:命名实体范围取决于手写固定表;不解码表外实体;不承担净化职责;无终止符形式的支持需谨慎取舍。资料未提供该方案的安装命令、发布形式、许可证或性能基准数据。