← 中文资源

简体中文版本

按字素截断文本

以字素簇为单位截断界面文本,避免拆开 emoji 与组合符号,并把省略号计入长度预算。

阅读英文原文

功能说明

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("");
}