You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React文本渲染组件中匹配成对星号内容实现Markdown加粗效果

问题核心原因

你当前的处理逻辑是先按空格拆分每个词再做规则匹配,跨多词的加粗内容(比如这是一整句加粗的文本)会被拆成**这是、一整句、加粗的、文本**多个独立片段,自然无法匹配到成对的星号。同时你当前用的加粗正则是贪婪匹配,即使是单个词内有多个加粗标记也会出现匹配异常。

修改方案

调整处理顺序:先按加粗规则把整行文本分割成「普通文本片段」和「加粗文本片段」交替的结构,对每个片段再执行原有逻辑处理提及、话题标签、URL,就能同时支持单/多词加粗,也不会影响原有功能。

修改后的代码如下:

import {
  Theme,
  createStyles,
  WithStyles,
  withStyles,
  Link as MUI_Link,
} from "../components/material";
import { split_on_new_line } from "../utils";
import Link from "next/link";
import { RouterProps } from "next/router";
import { Send_to_root_T } from "../pages/_app";
import { get_a_user_info_by_username$$ } from "../utils/api";

const styles = function(theme: Theme) {
  return createStyles({
    wrapper: {
      marginTop: theme.spacing.unit,
      marginLeft: theme.spacing.unit,
      marginRight: theme.spacing.unit,
      display: "block",
    },
    line: {
      display: "inline-block",
      width: "100%",
    },
    boldText: {
      fontWeight: "bold"
    }
  });
};

export interface Props extends WithStyles<typeof styles> {
  content: string;
  router: RouterProps;
  send_to_root: Send_to_root_T;
  link_class?: string;
  regular_text_class?: string;
}

// 正则提到组件外避免重复声明,加粗正则改用非贪婪匹配
const hashtag_regex = /#[^\s!@#$%^&*()=+./,\[{\]};:'"?><،؛؟‌]+/gi;
const url_regex = /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi;
const mention_regex = /@[0-9a-zA-Z_](\.?[0-9a-zA-Z-_])*/gi;
const bold_regex = /(\*\*.+?\*\*)/g;
const bold_strip_regex = /^\*\*(.*)\*\*$/;

// 抽取原有的单片段处理逻辑,方便复用
const processTextSegment = (segment: string, props: Props, keyPrefix: string|number) => {
  return segment.split(" ").map((item, i) => {
    // 每次匹配前重置正则lastIndex,避免全局匹配的缓存问题
    mention_regex.lastIndex = 0;
    url_regex.lastIndex = 0;
    hashtag_regex.lastIndex = 0;

    if (mention_regex.test(item)) {
      const username = item.slice(1);
      return (
        <span key={`${keyPrefix}-mention-${i}`}>
          <MUI_Link
            style={{
              cursor: "pointer",
            }}
            className={props.link_class}
            onClick={function(evt: React.MouseEvent) {
              evt.stopPropagation();
              props.send_to_root("SHOW_PROGRESS");
              get_a_user_info_by_username$$(username).subscribe({
                next: function(result) {
                  const uuid = result.data.uuid;

                  props.router.push(
                    {
                      pathname: `/profile`,
                      query: { user_id: uuid },
                    },
                    `/profile/${uuid}`,
                  );
                },
              });
            }}
          >
            {`@${username}`}
          </MUI_Link>{" "}
        </span>
      );
    }

    if (url_regex.test(item)) {
      return (
        <span key={`${keyPrefix}-url-${i}`}>
          <MUI_Link
            className={props.link_class}
            target="_blank"
            referrerPolicy="no-referrer"
            rel="noopener"
            href={item.startsWith("http") ? item : `http://${item}`}
            onClick={function(evt: React.SyntheticEvent) {
              evt.stopPropagation();
            }}
            style={{ cursor: "pointer" }}
          >
            {`${item}`}{" "}
          </MUI_Link>
        </span>
      );
    }

    if (hashtag_regex.test(item)) {
      return (
        <Link
          href={`/hashtag_posts?hashtag_text=${item.slice(1)}`}
          as={`/hashtags/${item.slice(1)}/posts`}
          key={`${keyPrefix}-hashtag-${i}`}
        >
          <a style={{ textDecoration: "none" }}>
            <MUI_Link
              className={props.link_class}
              component="span"
              onClick={function(evt: React.SyntheticEvent) {
                evt.stopPropagation();
                props.send_to_root("SHOW_PROGRESS");
              }}
              style={{ cursor: "pointer" }}
            >
              {`${item}`}{" "}
            </MUI_Link>
          </a>
        </Link>
      );
    }

    return item + " ";
  })
}

function ContentProcessedText(props: Props) {
  const c = props.classes;
  const content = props.content;

  return (
    <span className={c.wrapper}>
      {split_on_new_line(content).map(function(chunk, lineIndex) {
        // 先按加粗规则分割整行文本
        const lineSegments = chunk.split(bold_regex);
        return (
          <span key={lineIndex} className={c.line}>
            {lineSegments.map((seg, segIndex) => {
              // 判断当前片段是否是加粗内容
              if (bold_strip_regex.test(seg)) {
                // 提取加粗的文本内容
                const boldContent = seg.replace(bold_strip_regex, '$1');
                return (
                  <span key={`line-${lineIndex}-seg-${segIndex}`} className={c.boldText}>
                    {/* 对加粗内容内部也按原有规则处理提及、链接、话题 */}
                    {processTextSegment(boldContent, props, `line-${lineIndex}-bold-${segIndex}`)}
                  </span>
                )
              }
              // 普通文本直接走原有处理逻辑
              return processTextSegment(seg, props, `line-${lineIndex}-normal-${segIndex}`);
            })}
          </span>
        );
      })}
    </span>
  );
}

export default withStyles(styles)(ContentProcessedText);

额外优化点

  1. 把正则声明提到组件外部,避免每次渲染重复创建正则实例
  2. 匹配前重置正则的lastIndex属性,解决全局匹配模式下的缓存错位问题
  3. 把原有单文本片段的处理逻辑抽成公共函数,普通文本和加粗文本内部都可以复用,支持加粗内容里嵌套提及、话题、链接
  4. 加粗正则改用非贪婪匹配,避免同一行内多个加粗标记被错误合并

内容的提问来源于stack exchange,提问作者Abolfazl Vaysi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 05:18:00