前阵子从 Excel 里复制几行数据,粘出来的文本半角空格、全角空格、Tab 和空行混在一起。我打开了工具箱里的文本清理页,顶部一排八个复选框,勾一个刷新一次结果。麻烦出在我想要一个特定次序:先去空格,再把全角空格转成半角。
页面上八个框的顺序不是执行顺序
本篇的主角是文本清理页,文件为 application/index/view/tools/text-trim.html,模板共 144 行 / 5885 字节:八枚 opt-* 复选框写在 L12–L33,process() 的清理逻辑在 L92–L130,正好八步。界面从上到下与代码里的先后,两张顺序表对照如下。我把 process() 逐字摘进纯 Node 探针 tmp/tt_probe.js——不改写一个字、不修任何 bug、用 ids 对象喂勾选态——跑出来的真实执行顺序是:fullwidth → html → tab → all-space → trim-line → empty-line → merge-empty → no-newline。
| 界面(markup)顺序 | 代码执行顺序 |
|---|---|
| trim-line | fullwidth |
| all-space | html |
| empty-line(默认勾选) | tab |
| merge-empty | all-space |
| no-newline | trim-line |
| html-tag | empty-line |
| tab | merge-empty |
| fullwidth-space | no-newline |
两张表几乎完全反序:界面上排在最后的 fullwidth-space 永远第一个跑,no-newline 永远最后跑。也就是说,你在页面上按什么顺序勾选,对结果不产生任何影响,执行顺序是写死在代码里的。
想换序也换不了
我做了逐对换序真值表,两枚同时启用、和探针里手动交换两步先后各跑一遍:
- fullwidth ↔ all-space:输入
a\u3000b\u3000c,按页面顺序输出abc(3 个码元);换序后输出a b c(5 个码元),码元差异 = 4,不可交换。all-space 恒排在 fullwidth 之后,所以「先去空格再全角转半角」在本页没有操作路径。 - html-tag ↔ trim-line:输入
<b>hello </b>,页面顺序得hello(5 个码元),换序得hello(6 个码元),码元差异 = 1,同样不可交换。 - 可交换的三对:trim-line ↔ empty-line、merge-empty ↔ empty-line、tab ↔ all-space,码元差异都是 0。
至于给每行加序号、加前缀这类需求,那是另一个页面干的事,我用按行处理那页完成。
两个框勾在一起等于没勾
代码里 empty-line 在 L118,merge-empty 在 L123:empty-line 先跑,把所有 l.trim()==="" 的行剔掉,进到 merge-empty 的串里已经没有 \n{3,} 游程,merge-empty 命中次数恒为 0。实测三组输入命中全是 0:A\n\n\nB → A\nB;A\n\n\n\n\nB\n\n\nC → A\nB\nC;默认样本 → 去空行后的串。空行这件事我在那篇笔记里细讲过,这里不复述。
只勾 merge-empty 时判据是 \n{3,} → \n\n:游程 3、4、5 个换行都塌成 2 个换行,恰好 2 个换行(即 1 个空行)纹丝不动。
统计那四个数分别是什么口径
origLen 取 raw.length,是 UTF-16 码元;同一张表里的字节列是另用 Buffer.byteLength 的 utf8 口径量的:
图 1:同一段文本的码元与字节两把尺子。
reduced 的代码写了负值分支 reduced>=0?reduced:0。默认样本 origLen=56,在全部 2^8=256 种勾选组合上 reduced 落在 [0,16],负值命中 0/256。行数按 split("\n").length(L139/L140)计:尾随换行的 a\n 原始行数 = 2;a\r\nb 原始行数 = 2 且 \r 留在行内;\n\n 原始行数 = 3;默认样本原始行数 = 4。仅勾 empty-line 时默认样本结果行数 = 3(删掉中间那个空行);\n\n 结果行数 = 1,结果为空串。
默认样本在 256 种勾选下的结果分布
把默认样本的 256 种组合穷举一遍,输出串去重后不同结果数 = 8,各组组合数 64/64/32/32/16/16/16/16,合计 256。四个最大组的代表逐字:
- 64 组:
Hello World Hello JavaScript Hello Everyone - 64 组:
HelloWorld HelloJavaScript HelloEveryone - 32 组:
HelloWorld\nHelloJavaScript\n\nHelloEveryone - 16 组:
Hello World \n Hello JavaScript \n Hello Everyone。
图 2:去重后的 8 种结果各自对应的组合数。
Q:我勾了「去除所有空格」,结果里怎么还有空格?
all-space 只执行 replace(/ /g,""),删的是半角空格。对 a b\u00A0c\u3000d\te 它只删掉那个半角空格,U+00A0、U+3000、Tab 全留着。
Q:只勾「去除空行」,为什么只有空格的行也被删了?
empty-line 的判据是 l.trim()!===""。仅空格、仅 Tab、仅 U+00A0、仅 U+3000 的行,只勾 empty-line 一步就删得掉:在 [empty-line]、[trim-line+empty-line]、[fullwidth+trim-line+empty-line]、[fullwidth+empty-line] 四种组合下结果均为 X\nY。
Q:为什么打开页面统计是空的,输入字之后才有?
process 只在复选框 onchange、textarea oninput 和 selectAll 里被调用,模板里 grep process( 命中 11 处,没有一处是加载即执行,打开页面时结果框与统计区都是空的——这是字符串层判断,本批未开浏览器,DOM 层未实测。
Q:勾了「合并空行」,结果一点变化都没有,为什么?
同时勾了 empty-line 时,empty-line 先把空行全删光,merge-empty 命中 0,等于没勾;只勾 merge-empty 时它只把 \n{3,} 塌成 \n\n,只有 1 个空行的游程本来就不满足 3 个换行,保持原样。