受控文本输入框:按字形限长 · 注入控制字符 · twemoji 渲染
把本系列拆过的几个 Unicode 概念合到一个真实的输入组件里。处理一段自由文本,三件事最容易出错:
限长该数用户感知的「一个字符」(字素簇)而非 .length(UTF-16 码元);
文本里可能混入看不见的控制字符(零宽与隐形字符、U+202E bidi 反转),
需要能受控注入、也能被察觉;同一个 emoji 在各平台字形不一(Basic_Emoji),
要一致显示就用 twemoji 的 SVG。文本框本身用 @vega/smart-input 接管(IME / 撤销 / 拦截),
只描述一个 formatter。
受控编辑区:按字形限长 + 字符注入 + emoji 内联 SVG
左边的编辑区由 createSmartInput 接管。因为纯 <textarea> 渲染不了内联图片,
这里改用 contenteditable + smart-input 的 renderHtml 富渲染:每个 twemoji 支持的 emoji
在编辑区里直接用 SVG 显示(取不到 SVG 才回退系统字体);为保住光标 / 计数正确,SVG 仅作覆盖层,底层仍是真实码点
(满足 textContent === text)。normalize 把超过 24 个字形的部分截断——按
Intl.Segmenter 的 grapheme 计数,🤦🏼♂️(7 个码元)只算 1 个、截断不切碎 ZWJ 序列。
右边两种方式把字符插到光标处:常用不可见 / 控制字符的快捷 chip、自定义 codepoint 输入
(支持一次填多个,如 U+202E U+2322,合法时下方实时给出渲染结果);下方还有按 Unicode 分组的 emoji 选择器。
注入控制字符后,计数器会立刻显示「字形数」「隐形字符」双双变化——验证了「肉眼一个都没多,限额却被悄悄吃掉」。
str.length(UTF-16 码元)限长,一个 😀 占 2、🤦🏼♂️ 占 7,
用户「打了几个字」与系统「数出几格」对不上,截断点还可能落在代理对中间产生半个乱码字符。按 grapheme 计数 + 在字形边界截断才与用户感知一致。
U+202E 的危险:点「RLO」注入
U+202E 后,它把后文显示顺序反转——文件名 invoicegpj.exe 看着像
invoiceexe.jpg,这就是 Trojan Source。控制字符看不见却真实存在,受控输入应当能注入演示、更要能审计揪出。
和前面几页的关系:「文件名」取码点用的正是 字符的编码 里 code point ⇄ UTF-16 的换算,
ZWJ / VS16 的取舍来自 Basic_Emoji 与 RGI 序列两页拆过的序列结构。