SOURCE

const NodeType= {
  Empty : 'Empty',
  Text : 'Text',
  PlainText : 'PlainText',
  Bold : 'Bold',
  View :'View',
  Image : 'Image',
  Touch : 'Touch',
  OldTouch : 'OldTouch', // 老的RN TagContent适配
  Custom : 'Custom',
}

 const NodeAliasMap = {
  font: NodeType.Text,
  b: NodeType.Bold,
  view: NodeType.View,
  div: NodeType.View,
  i: NodeType.Image,
  touch: NodeType.Touch,
  a: NodeType.OldTouch,
  c: NodeType.OldTouch,
  custom: NodeType.Custom,
}


function newNode  (type= NodeType.Empty, props = {}, children=[]) {
  return {
    type,
    props,
    children,
  }
}

const parseOpenTag = (input) => {
  input = input.trim()
  // 匹配tagName
  let res = /^(\w+)\s*/.exec(input)
  if (res) {
    const tagName = res[1]
    const tagType = NodeAliasMap[tagName] || NodeType[tagName] || NodeType.Empty
    const props = {}

    input = input.slice(res[0].length)

    while (input) {
      // 匹配例如show size=12
      res = /^(\w+)\s+/.exec(input)
      if (res) {
        const key = res[1]
        props[key] = true
        input = input.slice(res[0].length)
        continue
      }

      // 匹配例如show$
      res = /^(\w+)$/.exec(input)
      if (res) {
        const key = res[1]
        props[key] = true

        break
      }

      // 匹配例如size=12
      res = /^(\w+)=([^'"`\s]+)\s*/.exec(input)
      if (res) {
        const key = res[1]
        let value = res[2]
        try {
          value = JSON.parse(value)
        } catch (error) {}

        props[key] = value
        input = input.slice(res[0].length)
        continue
      }

      // 匹配例如size='{'aaa': 123}'
      res = /^(\w+)='(\{.*?\})'\s*/.exec(input)
      if (res) {
        const key = res[1]
        const value = res[2]

        props[key] = value
        input = input.slice(res[0].length)
        continue
      }

      // 匹配例如size='12'
      res = /^(\w+)='([^']*)'\s*/.exec(input)
      if (res) {
        const key = res[1]
        const value = res[2]

        props[key] = value
        input = input.slice(res[0].length)
        continue
      }

      // 匹配例如size="{"aaa": 123}"
      res = /^(\w+)="(\{.*?\})"\s*/.exec(input)
      if (res) {
        const key = res[1]
        const value = res[2]

        props[key] = value
        input = input.slice(res[0].length)
        continue
      }

      // 匹配例如size="12"
      res = /^(\w+)="([^"]*)"\s*/.exec(input)
      if (res) {
        const key = res[1]
        const value = res[2]

        props[key] = value
        input = input.slice(res[0].length)
        continue
      }

      // 匹配例如size=`12`
      res = /^(\w+)=`([^`]*)`\s*/.exec(input)
      if (res) {
        const key = res[1]
        let value = res[2]
        try {
          value = JSON.parse(value)
        } catch (error) {}

        props[key] = value
        input = input.slice(res[0].length)
        continue
      }

      break
    }

    return newNode(tagType, props)
  }

  return newNode()
}

function  _parse (
  input,
  parentNode,
  isFirstRecursion // 是否是第一次递归
)  {
  let startIndex = 0 // 未处理字符的起点
  let index = 0 // 外循环当前指针
  while (index < input.length) {
    const char = input.charAt(index)
    // console.log('index:', index, char)
    // 当遇到疑似标签起始符时
    if (char === '<') {
      let node
      let subIndex = index + 1 // 内循环当前指针
      // 需要根据后面的字符判断前面的<是否为标签起始符
      while (subIndex < input.length) {
        const subChar = input.charAt(subIndex)
        // console.log('subIndex:', subIndex, subChar)
        // 当遇到标签关闭符时,可以判断之前的<为标签起始符
        // 如果能提取出node,则此标签为开标签,否则为闭标签
        if (subChar === '>') {
          // 如果标签前面有字符,先处理
          const str = input.slice(startIndex, index)
          if (str) {
            node = newNode(NodeType.PlainText, {
              text: str,
            })
            parentNode.children.push(node)
          }
          const tagStr = input.slice(index + 1, subIndex)
          if (input.charAt(index + 1) === '/') {
            // 闭标签
            // 如果递归还有上层,返回上层处理
            if (!isFirstRecursion) {
              return {
                node: parentNode,
                parsedInput: input.slice(0, subIndex + 1),
              }
            }
            node = null
            startIndex = subIndex + 1
          } else if (tagStr[tagStr.length - 1] === '/') {
            // 开闭标签
            node = parseOpenTag(tagStr.slice(0, tagStr.length - 1))
            startIndex = subIndex + 1
          } else {
            // 开标签
            node = parseOpenTag(tagStr)
            const { parsedInput } = _parse(input.slice(subIndex + 1), node, false)
            subIndex += parsedInput.length
            startIndex = subIndex + 1
          }
          break
        } else if (subChar === '<') {
          // 前一个<无效
          node = newNode(NodeType.PlainText, {
            text: input.slice(startIndex, subIndex),
          })
          startIndex = subIndex
          subIndex--
          break
        }
        subIndex++
      }
      if (node) {
        parentNode.children.push(node)
        index = subIndex
      }
    }

    index++
  }
  // console.log(startIndex, index)
  // 处理尾部余留文案
  const str = input.slice(startIndex, index)
  if (str) {
    const node = newNode(NodeType.PlainText, {
      text: str,
    })
    parentNode.children.push(node)
  }
  return {
    node: parentNode,
    parsedInput: input.slice(0, index),
  }
}

function parse  (input) {
  if (!input) return null
  // 处理换行标签
  input = input.replace(/<br\s*\/?>/gi, '\n')
  const node = _parse(input, newNode(NodeType.Empty), true).node
  if (node.type === NodeType.Empty && node.children.length === 1) {
    return node.children[0]
  }
  return node
}

let stra='<view style=`{"display": "flex","flexDirection": "row"}`><font color=#999999 size=13 numberOfLines=1>货主接通成功后扣</font><i uri="https://imagecdn.ymm56.com/ymmfile/static/resource/b4b2de00-f30c-4e5e-8b3f-aa8621680643.png" defaultSize=`{"width": 20,"height": 20}` style=`{"width": 19,"height": 19}` resizeStyle="fix-height" /><font color=#FA871E  size=13 style=`{ "marginLeft" : -2 }`>{{ copyData }}</font><font color=#999999 size=13 numberOfLines=1>,抢单不重复扣豆</font></view>'
let ss=        '<font color=#999999 size=13 numberOfLines=1>货主接通成功后扣</font><i uri="https://imagecdn.ymm56.com/ymmfile/static/resource/b4b2de00-f30c-4e5e-8b3f-aa8621680643.png" defaultSize=`{"width": 20,"height": 20}` style=`{"width": 19,"height": 19}` resizeStyle="fix-height" /><font color=#FA871E  size=13 style=`{ "marginLeft" : -2 }`>{{ copyData }}</font><font color=#999999 size=13 numberOfLines=1>,抢单不重复扣豆</font>'

let res=parse(ss)
console.log(ss)
console.log(JSON.stringify(ss))
console 命令行工具 X clear

                    
>
console