about summary refs log tree commit diff
path: root/src/view/com/util/text/RichText.tsx
blob: a7bc92a45647a4181f7b6ebdd878408a8365ebae (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import React from 'react'
import {TextStyle, StyleProp} from 'react-native'
import {TextLink} from '../Link'
import {Text} from './Text'
import {lh} from '../../../lib/styles'
import {toShortUrl} from '../../../../lib/strings'
import {useTheme, TypographyVariant} from '../../../lib/ThemeContext'
import {usePalette} from '../../../lib/hooks/usePalette'

type TextSlice = {start: number; end: number}
type Entity = {
  index: TextSlice
  type: string
  value: string
}

export function RichText({
  type = 'md',
  text,
  entities,
  lineHeight = 1.2,
  style,
  numberOfLines,
}: {
  type?: TypographyVariant
  text: string
  entities?: Entity[]
  lineHeight?: number
  style?: StyleProp<TextStyle>
  numberOfLines?: number
}) {
  const theme = useTheme()
  const pal = usePalette('default')
  const lineHeightStyle = lh(theme, type, lineHeight)
  if (!entities?.length) {
    if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) {
      style = {
        fontSize: 26,
        lineHeight: 30,
      }
      return <Text style={[style, pal.text]}>{text}</Text>
    }
    return (
      <Text type={type} style={[style, pal.text, lineHeightStyle]}>
        {text}
      </Text>
    )
  }
  if (!style) style = []
  else if (!Array.isArray(style)) style = [style]
  entities.sort(sortByIndex)
  const segments = Array.from(toSegments(text, entities))
  const els = []
  let key = 0
  for (const segment of segments) {
    if (typeof segment === 'string') {
      els.push(segment)
    } else {
      if (segment.entity.type === 'mention') {
        els.push(
          <TextLink
            key={key}
            type={type}
            text={segment.text}
            href={`/profile/${segment.entity.value}`}
            style={[style, lineHeightStyle, pal.link]}
          />,
        )
      } else if (segment.entity.type === 'link') {
        els.push(
          <TextLink
            key={key}
            type={type}
            text={toShortUrl(segment.text)}
            href={segment.entity.value}
            style={[style, lineHeightStyle, pal.link]}
          />,
        )
      }
    }
    key++
  }
  return (
    <Text
      type={type}
      style={[style, pal.text, lineHeightStyle]}
      numberOfLines={numberOfLines}>
      {els}
    </Text>
  )
}

function sortByIndex(a: Entity, b: Entity) {
  return a.index.start - b.index.start
}

function* toSegments(text: string, entities: Entity[]) {
  let cursor = 0
  let i = 0
  do {
    let currEnt = entities[i]
    if (cursor < currEnt.index.start) {
      yield text.slice(cursor, currEnt.index.start)
    } else if (cursor > currEnt.index.start) {
      i++
      continue
    }
    if (currEnt.index.start < currEnt.index.end) {
      let subtext = text.slice(currEnt.index.start, currEnt.index.end)
      if (!subtext.trim()) {
        // dont yield links to empty strings
        yield subtext
      } else {
        yield {
          entity: currEnt,
          text: subtext,
        }
      }
    }
    cursor = currEnt.index.end
    i++
  } while (i < entities.length)
  if (cursor < text.length) {
    yield text.slice(cursor, text.length)
  }
}