rehypeSplitWordsIntoSpans.js 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { visit } from 'unist-util-visit';
  2. /**
  3. * rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
  4. * 仅在流式渲染阶段使用,避免已渲染文字重复动画。
  5. */
  6. export function rehypeSplitWordsIntoSpans(options = {}) {
  7. const { previousContentLength = 0 } = options;
  8. return (tree) => {
  9. let currentCharCount = 0; // 当前已处理的字符数
  10. visit(tree, 'element', (node) => {
  11. if (
  12. ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(node.tagName) &&
  13. node.children
  14. ) {
  15. const newChildren = [];
  16. node.children.forEach((child) => {
  17. if (child.type === 'text') {
  18. try {
  19. // 使用 Intl.Segmenter 精准拆分中英文及标点
  20. const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
  21. const segments = segmenter.segment(child.value);
  22. Array.from(segments)
  23. .map((seg) => seg.segment)
  24. .filter(Boolean)
  25. .forEach((word) => {
  26. const wordStartPos = currentCharCount;
  27. const wordEndPos = currentCharCount + word.length;
  28. // 判断这个词是否是新增的(在 previousContentLength 之后)
  29. const isNewContent = wordStartPos >= previousContentLength;
  30. newChildren.push({
  31. type: 'element',
  32. tagName: 'span',
  33. properties: {
  34. className: isNewContent ? ['animate-fade-in'] : [],
  35. },
  36. children: [{ type: 'text', value: word }],
  37. });
  38. currentCharCount = wordEndPos;
  39. });
  40. } catch (_) {
  41. // Fallback:如果浏览器不支持 Segmenter
  42. const textStartPos = currentCharCount;
  43. const isNewContent = textStartPos >= previousContentLength;
  44. if (isNewContent) {
  45. // 新内容,添加动画
  46. newChildren.push({
  47. type: 'element',
  48. tagName: 'span',
  49. properties: {
  50. className: ['animate-fade-in'],
  51. },
  52. children: [{ type: 'text', value: child.value }],
  53. });
  54. } else {
  55. // 旧内容,不添加动画
  56. newChildren.push(child);
  57. }
  58. currentCharCount += child.value.length;
  59. }
  60. } else {
  61. newChildren.push(child);
  62. }
  63. });
  64. node.children = newChildren;
  65. }
  66. });
  67. };
  68. }