about summary refs log tree commit diff
path: root/src/view/com/composer/text-input/text-input-util.ts
blob: 7268e10f0a4646ab665c8d9ef4a20815551fb74e (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
import {AppBskyRichtextFacet, RichText} from '@atproto/api'

export type LinkFacetMatch = {
  rt: RichText
  facet: AppBskyRichtextFacet.Main
}

export function suggestLinkCardUri(
  suggestLinkImmediately: boolean,
  nextDetectedUris: Map<string, LinkFacetMatch>,
  prevDetectedUris: Map<string, LinkFacetMatch>,
  pastSuggestedUris: Set<string>,
): string | undefined {
  const suggestedUris = new Set<string>()
  for (const [uri, nextMatch] of nextDetectedUris) {
    if (!isValidUrlAndDomain(uri)) {
      continue
    }
    if (pastSuggestedUris.has(uri)) {
      // Don't suggest already added or already dismissed link cards.
      continue
    }
    if (suggestLinkImmediately) {
      // Immediately add the pasted or intent-prefilled link without waiting to type more.
      suggestedUris.add(uri)
      continue
    }
    const prevMatch = prevDetectedUris.get(uri)
    if (!prevMatch) {
      // If the same exact link wasn't already detected during the last keystroke,
      // it means you're probably still typing it. Disregard until it stabilizes.
      continue
    }
    const prevTextAfterUri = prevMatch.rt.unicodeText.slice(
      prevMatch.facet.index.byteEnd,
    )
    const nextTextAfterUri = nextMatch.rt.unicodeText.slice(
      nextMatch.facet.index.byteEnd,
    )
    if (prevTextAfterUri === nextTextAfterUri) {
      // The text you're editing is before the link, e.g.
      // "abc google.com" -> "abcd google.com".
      // This is a good time to add the link.
      suggestedUris.add(uri)
      continue
    }
    if (/^\s/m.test(nextTextAfterUri)) {
      // The link is followed by a space, e.g.
      // "google.com" -> "google.com " or
      // "google.com." -> "google.com ".
      // This is a clear indicator we can linkify it.
      suggestedUris.add(uri)
      continue
    }
    if (
      /^[)]?[.,:;!?)](\s|$)/m.test(prevTextAfterUri) &&
      /^[)]?[.,:;!?)]\s/m.test(nextTextAfterUri)
    ) {
      // The link was *already* being followed by punctuation,
      // and now it's followed both by punctuation and a space.
      // This means you're typing after punctuation, e.g.
      // "google.com." -> "google.com. " or
      // "google.com.foo" -> "google.com. foo".
      // This means you're not typing the link anymore, so we can linkify it.
      suggestedUris.add(uri)
      continue
    }
  }
  for (const uri of pastSuggestedUris) {
    if (!nextDetectedUris.has(uri)) {
      // If a link is no longer detected, it's eligible for suggestions next time.
      pastSuggestedUris.delete(uri)
    }
  }

  let suggestedUri: string | undefined
  if (suggestedUris.size > 0) {
    suggestedUri = Array.from(suggestedUris)[0]
    pastSuggestedUris.add(suggestedUri)
  }

  return suggestedUri
}

// https://stackoverflow.com/questions/8667070/javascript-regular-expression-to-validate-url
// question credit Muhammad Imran Tariq https://stackoverflow.com/users/420613/muhammad-imran-tariq
// answer credit Christian David https://stackoverflow.com/users/967956/christian-david
function isValidUrlAndDomain(value: string) {
  return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
    value,
  )
}