富文本字数与字符统计:emoji、中日文和提交上限

为 RichTextEditor 添加符合应用规则的计数器,而不必在每次按键时替换文档。本指南提供经过测试的计数模块及完整浏览器示例:校验 200 个字符的上限,同时完整保留超限草稿。
定义字符的计数单位
JavaScript 字符串长度统计 UTF-16 代码单元。一个家庭 emoji 或带组合重音的字母,看起来是一个字符,却可能包含多个单元。本示例使用 Intl.Segmenter 统计字素簇,包含空格和换行,并将 CRLF 或 CR 统一为 LF。
字词数统计指定文档语言下被识别为词的分段。这不保证与 Microsoft Word、编辑器内置状态栏、短信计费或社交平台规则相同。应区分这些策略,并向作者说明上限采用的单位。
下载计数模块
下载文本统计模块。它接受纯文本,而非 HTML。通过 getPlainText() 读取文档,不要统计 getHTMLCode() 返回的 HTML。标签不是字词;段落边界和提取策略返回的标记都需要明确测试。
模块要求浏览器或服务器支持文本分段。缺少支持时会报告错误,而不会悄悄改用空格拆分,因为后者不适合中文和日文。
连接计数器和提交检查
从应用中提供编辑器资源和模块。示例只检查提交条件,不发送或保存内容。输入时延迟合并计数,提交表单时则立即重新计算。拒绝提交不会改变编辑器中的草稿。
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<style>body{font:16px/1.6 system-ui,sans-serif;max-width:960px;margin:24px auto;padding:0 16px}button{min-height:44px;padding:8px 16px}</style>
<h1>Word and character counter</h1>
<form id="message-form">
<div style="max-width:100%;overflow-x:auto"><div id="editor"></div></div>
<p id="counter"></p>
<p>Limit: 200 graphemes, including spaces and line breaks. This demo does not save.</p>
<button type="submit">Check submission</button>
<p id="result" role="status" aria-live="polite"></p>
</form>
<script type="module">
import { countText } from "/js/text-metrics.mjs";
const editor = new RichTextEditor("#editor", { toolbar: "full", height: "320px", showStatistics: false });
editor.setHTMLCode("<p>Write a short note.</p>");
const LOCALE = "en"; // Choose the document language, not necessarily the UI language.
const LIMIT = 200;
const counter = document.getElementById("counter");
const result = document.getElementById("result");
function updateCount() {
try {
const metrics = countText(editor.getPlainText(), LOCALE);
counter.textContent = metrics.words + " words · " + metrics.characters +
" / " + LIMIT + " characters (graphemes)";
return metrics;
} catch {
counter.textContent = "Counter unavailable. Check locale and Intl.Segmenter support.";
return null;
}
}
let timer, checkedText = null;
editor.attachEvent("change", () => {
if (editor.getPlainText() !== checkedText) result.textContent = "";
clearTimeout(timer);
timer = setTimeout(updateCount, 200);
});
document.getElementById("message-form").addEventListener("submit", event => {
event.preventDefault();
clearTimeout(timer);
const metrics = updateCount(); // Do not trust a delayed displayed count.
checkedText = editor.getPlainText();
if (!metrics) { result.textContent = "Cannot validate this count."; return; }
if (metrics.characters > LIMIT) {
result.textContent = "Too long. Shorten the draft; your content is still here.";
editor.focus();
return;
}
result.textContent = "Within limit. Ready for your server-side validation.";
// Add your authenticated save here. This demo sends no request.
});
updateCount();
// Exposed only so you can inspect this standalone demo in developer tools:
window.counterDemo = { editor, updateCount };
</script>
在服务器上采用相同策略
服务器应根据提交的文档和约定的文本提取规则再次验证。不要信任客户端提供的计数或额外的纯文本字段。权限、HTML 净化和请求字节数应分别检查;200 个字素簇并不意味着 HTML 请求体很小。
在现代 Node.js 应用中提取纯文本后,也可以复用该模块。统一正规化规则和语言设置,并运行共享测试样本:浏览器与服务器版本使用的 Unicode 和分段数据可能不同。如果上限具有合同意义,应选择并固定权威的服务器计数策略。
测试语言和输入方式
- 检查空文本、空格、段落换行和 CRLF 输入。
- 尝试家庭 emoji、旗帜和组合重音。不要为了满足上限而拆开或截断它们。
- 使用真实的中文和日文文档,不要假设词之间都有空格。
- 粘贴超限内容,在显示计数刷新之前立即提交。检查应拒绝提交并保留草稿。
- 删除足够文字后再次提交,确认可以通过。在自己的集成中测试输入法组合输入和撤销。
此计数器不负责必填内容规则,也不决定纯图片文档是否有效。不要通过重写编辑器 HTML 或取消每次按键来实现本示例,否则可能破坏选区、撤销和输入法行为。