功能说明
truncateGraphemes 按字素簇而非 UTF-16 码元截断文本,因此家庭 emoji、带肤色的手势和组合重音符号不会被切成乱码。长度预算包含省略号,保证结果不超过上限。truncateMiddle 保留首尾、截断中间,适合展示长路径、哈希或 URL。常用于卡片标题、表格单元格、导航面包屑与日志摘要。
关键代码
模块加载时检测 Intl.Segmenter 是否可用,可用时创建字素分段器,不可用时退回 Array.from 的码点拆分作为降级。主函数先分段,长度未超限直接返回原文;超限时预留一个字素给省略号,再拼接前缀。中间截断把保留预算对半分给首尾,奇数部分优先给头部。maxGraphemes 为零时返回空字符串,避免输出只有省略号。
使用注意
降级路径按码点拆分,仍可能拆开由多个码点组成的字素,例如旗帜或组合 emoji,若目标浏览器不支持 Segmenter 应接受这一限制。省略号按一个显示单位计算,传入多字符标记会挤占正文预算。截断只处理显示,不应作用于存储值。对包含换行的文本,调用方应先折叠空白,否则结果可能仍带换行。
验证方式
测试纯 ASCII、包含 emoji、带组合重音和家庭 emoji 的字符串,确认截断后不出现孤立代理项或破碎字素。验证长度恰好等于上限、超出一位、上限为零与空字符串。测试中间截断的奇偶预算分配及短标记。再模拟没有 Intl.Segmenter 的环境,确认降级路径可运行且不抛错。
代码示例
const segmenter = typeof Intl.Segmenter === "function"
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null;
export function truncateGraphemes(text, maxGraphemes, ellipsis = "\u2026") {
if (!Number.isInteger(maxGraphemes) || maxGraphemes < 0) {
throw new RangeError("maxGraphemes must be a non-negative integer");
}
const value = String(text);
const graphemes = segmenter
? [...segmenter.segment(value)].map((part) => part.segment)
: Array.from(value);
if (graphemes.length <= maxGraphemes) return value;
if (maxGraphemes === 0) return "";
const suffix = ellipsis ? ellipsis : "";
const budget = Math.max(0, maxGraphemes - 1);
return graphemes.slice(0, budget).join("") + suffix;
}
export function truncateMiddle(text, maxGraphemes, marker = "\u2026") {
const value = String(text);
const graphemes = segmenter
? [...segmenter.segment(value)].map((part) => part.segment)
: Array.from(value);
if (graphemes.length <= maxGraphemes) return value;
if (maxGraphemes <= 1) return marker;
const keep = maxGraphemes - 1;
const head = Math.ceil(keep / 2);
const tail = Math.floor(keep / 2);
return graphemes.slice(0, head).join("") + marker + graphemes.slice(-tail).join("");
}