Lines 96.15% 25 / 26
Methods 90.90% 10 / 11
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 text 100.00% 1 / 1 100.00% 1 / 1 1
 bold 100.00% 1 / 1 100.00% 1 / 1 1
 code 100.00% 1 / 1 100.00% 1 / 1 1
 link 100.00% 1 / 1 100.00% 1 / 1 1
 plain 100.00% 1 / 1 100.00% 1 / 1 1
 forSlack 100.00% 1 / 1 100.00% 1 / 1 1
 forTelegram 100.00% 1 / 1 100.00% 1 / 1 1
 escapeSlack 100.00% 1 / 1 100.00% 1 / 1 1
 add 75.00% 3 / 4 0.00% 0 / 1 2.06
 render 100.00% 13 / 13 100.00% 1 / 1 5
12final class ChatText
13{
14    private const array SLACK = ['text' => '%1$s', 'bold' => '*%1$s*', 'code' => '`%1$s`', 'link' => '<%2$s|%1$s>'];
15
16    private const array TELEGRAM = ['text' => '%1$s', 'bold' => '<b>%1$s</b>', 'code' => '<code>%1$s</code>', 'link' => '<a href="%2$s">%1$s</a>'];
17
18    /** @var list<array{'text'|'bold'|'code'|'link', string, string}> kind, text, URL */
19    private array $parts = [];
20
21    public function __construct(string $text = '')
22    {
23        $this->text($text);
24    }
25
26    public function text(string $text): self
27    {
28        return $this->add('text', $text);
29    }
30
31    public function bold(string $text): self
32    {
33        return $this->add('bold', $text);
34    }
35
36    public function code(string $text): self
37    {
38        return $this->add('code', $text);
39    }
40
41    public function link(string $label, string $url): self
42    {
43        return $this->add('link', $label, $url);
44    }
45
46    /** The visible text, e.g. for the log. */
47    public function plain(): string
48    {
49        return \implode('', \array_map(static fn (array $part): string => $part[1], $this->parts));
50    }
51
52    public function forSlack(int $maxLength = 3000): string
53    {
54        return $this->render(self::SLACK, self::escapeSlack(...), $maxLength);
55    }
56
57    public function forTelegram(int $maxLength = 4000): string
58    {
59        return $this->render(self::TELEGRAM, static fn (string $text): string => \htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE), $maxLength);
60    }
61
62    /** Slack's control characters in a message's text. */
63    public static function escapeSlack(string $text): string
64    {
65        return \strtr($text, ['&' => '&amp;', '<' => '&lt;', '>' => '&gt;']);
66    }
67
68    private function add(string $kind, string $text, string $url = ''): self
69    {
70        if ('' !== $text) {
71            \assert(\in_array($kind, ['text', 'bold', 'code', 'link'], true));
72            $this->parts[] = [$kind, $text, $url];
73        }
74
75        return $this;
76    }
77
78    /**
79     * @param array<string, string>    $formats per kind, %1$s the text and %2$s the URL
80     * @param callable(string): string $escape
81     */
82    private function render(array $formats, callable $escape, int $maxLength): string
83    {
84        $rendered = '';
85        $left = $maxLength;
86        foreach ($this->parts as [$kind, $text, $url]) {
87            if ($left < 1) {
88                return $rendered.'…';
89            }
90
91            $cut = \mb_strlen($text) > $left;
92            if ($cut) {
93                $text = \mb_substr($text, 0, $left);
94            }
95
96            $left -= \mb_strlen($text);
97            $rendered .= \sprintf($formats[$kind], $escape($text), $escape($url));
98            if ($cut) {
99                return $rendered.'…';
100            }
101        }
102
103        return $rendered;
104    }
105}