nandi/frqpublic Fork 0
ba4e71bc611466c9263edf4b76967146aa60230f
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Split the tree three ways, and let the phone be Flutter's 7070931 · on ba4e71bc611466c9263edf4b76967146aa60230f · nandi · 7d ago
glyphs.cljc · 84 lines · 3.5 KBGDScript3 Blame HistoryRaw
 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
(ns frq.glyphs
  "Where a line of chat stops being text and becomes a picture.

  Vidya paints a `:label` with Ubuntu Light and a DejaVu subset behind it 
  UI punctuation, maths-in-prose, box drawing  and no emoji at all. So an
  emoji anybody typed into a message was drawn as tofu: the hollow box a font
  puts up for a codepoint it has no glyph for. The same emoji in a reaction
  pill drew properly all along, because a pill looks its glyph up in the
  Twemoji pack instead of setting it as text  which is what `:emoji` does
  too, without the pill.

  The split therefore has to happen before the text reaches a label, and this
  is where: the message cut into the parts a font can draw and the parts only
  the pack can."
  (:require [frq.emoji :as emoji]))

(def ^:private tones
  "The five skin-tone modifiers.

  `emoji/catalog` leaves toned variants out on purpose  they multiply it by
  five and say nothing a reaction needs to say  but the pack has a picture
  for every one, and vidya falls back to the untoned picture where it has
  none. So a tone is stripped for the lookup and kept for the drawing."
  #{0x1F3FB 0x1F3FC 0x1F3FD 0x1F3FE 0x1F3FF})

(defn- untoned [s]
  (apply str (remove #(contains? tones (int %)) (seq s))))

(def ^:private pack
  "Every emoji there is a picture for. `emoji/catalog` is already filtered to
  exactly that, which is why the lookup is against it and not against a range
  of codepoints: a codepoint the pack has missed would be swapped out of the
  text for a picture that does not exist, and drawn as the same tofu by a
  longer road."
  (set (map first emoji/catalog)))

(def ^:private starts
  "The first codepoint of each of them. A message with no emoji in it — which
  is nearly all of them  costs one set lookup a character this way, and no
  substrings at all."
  (set (map #(first (seq (first %))) emoji/catalog)))

(def ^:private scan-limit
  "The most codepoints one emoji can run to. A family is a single glyph made
  of four people, three joiners and a tone each, so the scan cannot assume a
  cluster is short  it is the longest thing in the catalog, plus room for the
  tones the catalog itself leaves out."
  (+ 5 (reduce max 1 (map #(count (first %)) emoji/catalog))))

(defn- emoji-at
  "The longest emoji the pack knows that starts at `i`, or nil.

  Longest first, because the short one is a prefix of the long one: 👨 is the
  head of 👨👩👧, and taking the man would leave his family as three more
  glyphs and two tofu joiners."
  [cps i]
  (loop [len (min scan-limit (- (count cps) i))]
    (when (pos? len)
      (let [s (apply str (subvec cps i (+ i len)))]
        (if (or (contains? pack s) (contains? pack (untoned s)))
          s
          (recur (dec len)))))))

(defn runs
  "`text` as alternating `[:text s]` and `[:emoji s]` runs, in order.

  An `:emoji` run is one glyph: a node draws one picture, so two emoji side by
  side are two runs and not one."
  [text]
  (let [cps (vec (seq (or text "")))
        n (count cps)
        flush (fn [acc buf] (cond-> acc (seq buf) (conj [:text (apply str buf)])))]
    (loop [i 0 buf [] acc []]
      (if (>= i n)
        (flush acc buf)
        (if-let [hit (and (contains? starts (nth cps i)) (emoji-at cps i))]
          (recur (+ i (count hit)) [] (conj (flush acc buf) [:emoji hit]))
          (recur (inc i) (conj buf (nth cps i)) acc))))))

(defn emoji?
  "Whether any of `runs` is a picture — which is what decides between a plain
  label and a row that has to mix the two."
  [runs]
  (boolean (some #(= :emoji (first %)) runs)))