【RN】实现markdown文本简单解析

需求

  • 支持文本插入,比如 xxx {product_name} xxx ,如果提供了product_name变量的值为feedback,则可以渲染出 xxx feedback xxx。
  • 支持链接解析,比如 [baidu](https://www.baidu.com/),可以直接渲染成超链接的形式。
  • 支持插入reactnode元素,比如 xxx {jump_node} xxx,且jump_node是一个reactnode元素,则可以将node节点插入到{}位置上。

实现

步骤:

  • 先解析链接, 返回这样子的数据结构。超链接返回url-text的对象,非超链接直接返回文本字符串
javascript 复制代码
export interface LinkPart {
  text: string;
  url?: string;
  onClick?: string;
}

export type ParsedTextPart = string | LinkPart;

[
  {
    text: 'baidu',
    url: 'https://www.baidu.com/',
  },
  'other content',
  'other content',
];
  • 遍历解析后的超链接数组,如果是对象,则渲染超链接;如果是字符串,继续解析
  • 解析字符串,判断需要解析的{}里面的文本是否是纯文本,如果是纯文本,则直接Text渲染;如果是react元素,则渲染该元素

TextTemplate.tsx:

javascript 复制代码
import React, { ReactNode } from 'react';
import { routeCenter } from '@shopeepay-rn/route-center';
import { usePageContainerContext } from '@shopeepay-rn/page-container';
import { StyleProp, Text, TextStyle, View, ViewStyle } from 'react-native';
import { parseLinkText } from '../../utils';
import styles from './styles';

interface Props {
  template: string;
  // eg: {product_name:'spp', click_node:<Text></Text>}
  replaceValueMap: Record<string, string | number | ReactNode>;
  textStyle?: StyleProp<TextStyle>;
  containerStyle?: StyleProp<ViewStyle>;
}

/**
 * 支持解析字符串、解析react元素、解析超链接
 * @param template 需要解析的字符串
 * @param replaceValueMap 需要替换的key-value,value支持字符串和react元素
 * @param textStyle 字体样式
 * @param containerStyle 容器样式
 * @returns react元素
 */
export const TextTemplate = ({
  template,
  replaceValueMap,
  textStyle,
  containerStyle,
}: Props) => {
  const { rootTag } = usePageContainerContext();

  const parseText = (text: string, index: number) => {
    const result: React.ReactNode[] = [];
    let lastIndex = 0;

    text.replace(/{(\w+)}/g, (match: string, key: string, offset: number) => {
      const replaceValue = replaceValueMap[key];
      if (offset > lastIndex) {
        // 未被匹配到的
        result.push(
          <Text key={index} style={textStyle}>
            {text.substring(lastIndex, offset)}
          </Text>
        );
      }

      if (React.isValidElement(replaceValue)) {
        // 需要替换的是reactnode元素
        result.push(React.cloneElement(replaceValue, { key: index }));
      } else if (typeof replaceValue === 'string') {
        // 需要替换的是字符串
        result.push(
          <Text key={index} style={textStyle}>
            {replaceValue}
          </Text>
        );
      }

      lastIndex = offset + match.length;
      return '';
    });

    if (lastIndex < text.length) {
      result.push(
        <Text key={index} style={textStyle}>
          {text.substring(lastIndex)}
        </Text>
      );
    }

    return result;
  };

  const parseTemplate = (text: string) => {
    // 解析链接
    const linkTexts = parseLinkText(text);

    return linkTexts?.map((part, index) => {
      return typeof part === 'string' ? (
        // 对于字符串,需要解析 纯字符串 还是 reactnode元素
        parseText(part, index)
      ) : (
        <Text
          key={index}
          style={styles.link}
          onPress={() =>
            routeCenter.navigateWeb(
              part.url || '',
              {
                navbar: {
                  title: part.text || '',
                },
              },
              rootTag
            )
          }
        >
          {part.text}
        </Text>
      );
    });
  };

  return (
    <View style={[styles.textView, containerStyle]}>
      <Text>{parseTemplate(template)}</Text>
    </View>
  );
};

parseLinkText.ts:

javascript 复制代码
export interface LinkPart {
  text: string;
  url?: string;
  onClick?: string;
}

export type ParsedTextPart = string | LinkPart;

const parseLinkText = (text: string): ParsedTextPart[] => {
  const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
  const parts: ParsedTextPart[] = [];
  let lastIndex = 0;

  text.replace(
    regex,
    (match: string, p1: string, p2: string, offset: number) => {
      if (offset > lastIndex) {
        parts.push(text.substring(lastIndex, offset));
      }

      parts.push({ text: p1, url: p2 });
      lastIndex = offset + match.length;
      return '';
    }
  );

  if (lastIndex < text.length) {
    parts.push(text.substring(lastIndex));
  }

  return parts;
};

// FIXME: 添加 unit test
export { parseLinkText };

使用

javascript 复制代码
<TextTemplate
  template={"you can test the TextTemplate component, parse {string_text}, parse [baidu](https://www.baidu.com/) link, parse {click_node} to show popup"}
  replaceValueMap={{string_text:"test string",click_node:<Text>other react node</Text>}}
  textStyle={styles.titleText}
/>
相关推荐
程序员大阳23 分钟前
HTML+CSS+JS对话式实战(5)-活动管理页之查看报名名单的实现
javascript·css·html·查看报名名单
Highcharts.js1 小时前
Highcharts Orbit 完全指南:把每一张图表变成交互式分析工具
react.js·前端框架·highcharts·orbit·使用指南·ai 图表·交互式分析工具
新*3 小时前
从一段 WebSocket 代码聊开:bytes 不是 zlib、binaryType 决定一切,15MB 大消息的零拷贝优化
javascript·python·websocket·性能优化·blob·zlib·arraybuffer
OpsEye4 小时前
别再挨个充会员了:多模型编码,最麻烦的从来不是选模型
javascript·ai编程
学以智用5 小时前
jsencrypt.js 完整详解
javascript
Hilaku8 小时前
你们公司有 35 岁以上的前端吗?
前端·javascript·程序员
鸿蒙开发9 小时前
小程序存档写到一半怎么办?双槽保存与损坏恢复实战
前端·javascript·react.js
知忆_IS9 小时前
TerriaMap 开发实战04:MVT数据自定义符号化 - terriajs源码修改
javascript·webgis·terriamap·terriajs
云上工程笔记12 小时前
SRT字幕一键转手绘动画:srt-whiteboard-animation的复刻实录
大数据·开发语言·javascript·人工智能·机器学习
FungLeo12 小时前
成为全栈·Vue管理后台篇·<scriptsetup>:把页面拆成可读、可测的 Vue 组件
前端·javascript·vue.js·typescript·前端组件化·composition api·vue sfc