1<?php
2
3namespace Emojione;
4
5/**
6 * Client for Emojione
7 */
8
9class Client implements ClientInterface
10{
11    public $ascii = false; // convert ascii smileys?
12    public $shortcodes = true; // convert shortcodes?
13    public $unicodeAlt = true; // use the unicode char as the alt attribute (makes copy and pasting the resulting text better)
14    public $imageType = 'png'; // or svg
15    public $cacheBustParam = '?v=2.2.7';
16    public $sprites = false;
17    public $imagePathPNG = 'https://cdn.jsdelivr.net/emojione/assets/png/';
18    public $imagePathSVG = 'https://cdn.jsdelivr.net/emojione/assets/svg/';
19    public $imagePathSVGSprites = './../../assets/sprites/emojione.sprites.svg';
20    public $imageTitleTag = true;
21    public $unicode_replaceWith = false;
22    public $ignoredRegexp = '<object[^>]*>.*?<\/object>|<span[^>]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>';
23    public $unicodeRegexp = '([*#0-9](?>\\xEF\\xB8\\x8F)?\\xE2\\x83\\xA3|\\xC2[\\xA9\\xAE]|\\xE2..(\\xF0\\x9F\\x8F[\\xBB-\\xBF])?(?>\\xEF\\xB8\\x8F)?|\\xE3(?>\\x80[\\xB0\\xBD]|\\x8A[\\x97\\x99])(?>\\xEF\\xB8\\x8F)?|\\xF0\\x9F(?>[\\x80-\\x86].(?>\\xEF\\xB8\\x8F)?|\\x87.\\xF0\\x9F\\x87.|..((\\xE2\\x80\\x8D\\xF0\\x9F\\x97\\xA8)|(\\xF0\\x9F\\x8F[\\xBB-\\xBF])|(\\xE2\\x80\\x8D\\xF0\\x9F\\x91[\\xA6-\\xA9]){2,3}|(\\xE2\\x80\\x8D\\xE2\\x9D\\xA4\\xEF\\xB8\\x8F\\xE2\\x80\\x8D\\xF0\\x9F..(\\xE2\\x80\\x8D\\xF0\\x9F\\x91[\\xA6-\\xA9])?))?))';
24
25    public $shortcodeRegexp = ':([-+\\w]+):';
26
27    protected $ruleset = null;
28
29    public function __construct(RulesetInterface $ruleset = null)
30    {
31        if ( ! is_null($ruleset) )
32        {
33            $this->ruleset = $ruleset;
34        }
35    }
36
37    // ##########################################
38    // ######## core methods
39    // ##########################################
40
41    /**
42     * First pass changes unicode characters into emoji markup.
43     * Second pass changes any shortnames into emoji markup.
44     *
45     * @param   string  $string The input string.
46     * @return  string  String with appropriate html for rendering emoji.
47     */
48    public function toImage($string)
49    {
50        $string = $this->unicodeToImage($string);
51        $string = $this->shortnameToImage($string);
52        return $string;
53    }
54
55    /**
56     * Uses toShort to transform all unicode into a standard shortname
57     * then transforms the shortname into unicode.
58     * This is done for standardization when converting several unicode types.
59     *
60     * @param   string  $string The input string.
61     * @return  string  String with standardized unicode.
62     */
63    public function unifyUnicode($string)
64    {
65        $string = $this->toShort($string);
66        $string = $this->shortnameToUnicode($string);
67        return $string;
68    }
69
70    /**
71     * This will output unicode from shortname input.
72     * If Client/$ascii is true it will also output unicode from ascii.
73     * This is useful for sending emojis back to mobile devices.
74     *
75     * @param   string  $string The input string.
76     * @return  string  String with unicode replacements.
77     */
78    public function shortnameToUnicode($string)
79    {
80        if ($this->shortcodes)
81        {
82            $string = preg_replace_callback('/'.$this->ignoredRegexp.'|('.$this->shortcodeRegexp.')/Si', array($this, 'shortnameToUnicodeCallback'), $string);
83        }
84
85        if ($this->ascii)
86        {
87            $ruleset = $this->getRuleset();
88            $asciiRegexp = $ruleset->getAsciiRegexp();
89
90            $string = preg_replace_callback('/'.$this->ignoredRegexp.'|((\\s|^)'.$asciiRegexp.'(?=\\s|$|[!,.?]))/S', array($this, 'asciiToUnicodeCallback'), $string);
91        }
92
93        return $string;
94    }
95
96    /**
97     * This will replace shortnames with their ascii equivalent.
98     * ex. :wink: --> ;^)
99     * This is useful for systems that don't support unicode or images.
100     *
101     * @param   string  $string The input string.
102     * @return  string  String with ascii replacements.
103     */
104    public function shortnameToAscii($string)
105    {
106        $string = preg_replace_callback('/'.$this->ignoredRegexp.'|('.$this->shortcodeRegexp.')/Si', array($this, 'shortnameToAsciiCallback'), $string);
107
108        return $string;
109    }
110
111    /**
112     * This will replace ascii with their shortname equivalent, it bases on reversed ::shortnameToAsciiCallback
113     * ex. :) --> :slight_smile:
114     * This is useful for systems that don't ascii emoji.
115     *
116     * @param   string  $string The input ascii.
117     * @return  string  String with shortname replacements.
118     */
119    public function asciiToShortname($string)
120    {
121        $ruleset = $this->getRuleset();
122        $asciiRegexp = $ruleset->getAsciiRegexp();
123        return preg_replace_callback('/'.$this->ignoredRegexp.'|((\\s|^)'.$asciiRegexp.'(?=\\s|$|[!,.?]))/S', array($this, 'asciiToShortnameCallback'), $string);
124    }
125
126    /**
127     * This will output image markup (for png or svg) from shortname input.
128     *
129     * @param   string  $string The input string.
130     * @return  string  String with appropriate html for rendering emoji.
131     */
132    public function shortnameToImage($string)
133    {
134        if ($this->shortcodes)
135        {
136            $string = preg_replace_callback('/'.$this->ignoredRegexp.'|('.$this->shortcodeRegexp.')/Si', array($this, 'shortnameToImageCallback'), $string);
137        }
138
139        if ($this->ascii)
140        {
141            $ruleset = $this->getRuleset();
142            $asciiRegexp = $ruleset->getAsciiRegexp();
143
144            $string = preg_replace_callback('/'.$this->ignoredRegexp.'|((\\s|^)'.$asciiRegexp.'(?=\\s|$|[!,.?]))/S', array($this, 'asciiToImageCallback'), $string);
145        }
146
147        return $string;
148    }
149
150    /**
151     * This will return the shortname from unicode input.
152     *
153     * @param   string  $string The input string.
154     * @return  string  shortname
155     */
156    public function toShort($string)
157    {
158        return preg_replace_callback('/'.$this->ignoredRegexp.'|'.$this->unicodeRegexp.'/S', array($this, 'toShortCallback'), $string);
159    }
160
161    /**
162     * This will output image markup (for png or svg) from unicode input.
163     *
164     * @param   string  $string The input string.
165     * @return  string  String with appropriate html for rendering emoji.
166     */
167    public function unicodeToImage($string)
168    {
169        return preg_replace_callback('/'.$this->ignoredRegexp.'|'.$this->unicodeRegexp.'/S', array($this, 'unicodeToImageCallback'), $string);
170    }
171
172    // ##########################################
173    // ######## preg_replace callbacks
174    // ##########################################
175
176    /**
177     * @param   array   $m  Results of preg_replace_callback().
178     * @return  string  Ascii replacement result.
179     */
180    public function shortnameToAsciiCallback($m)
181    {
182        if ((!is_array($m)) || (!isset($m[1])) || (empty($m[1])))
183        {
184            return $m[0];
185        }
186        else
187        {
188            $ruleset = $this->getRuleset();
189            $shortcode_replace = $ruleset->getShortcodeReplace();
190            $ascii_replace = $ruleset->getAsciiReplace();
191
192            $aflipped = array_flip($ascii_replace);
193
194            $shortname = $m[0];
195
196            if (!isset($shortcode_replace[$shortname]))
197            {
198                return $m[0];
199            }
200
201            $unicode = $shortcode_replace[$shortname];
202
203            return isset($aflipped[$unicode]) ? $aflipped[$unicode] : $m[0];
204        }
205    }
206
207    /**
208     * @param   array   $m  Results of preg_replace_callback().
209     * @return  string  Unicode replacement result.
210     */
211    public function shortnameToUnicodeCallback($m)
212    {
213        if ((!is_array($m)) || (!isset($m[1])) || (empty($m[1]))) {
214            return $m[0];
215        }
216        else {
217            $ruleset = $this->getRuleset();
218            $unicode_replace = $ruleset->getUnicodeReplace();
219
220
221            $shortname = $m[1];
222
223            if (!array_key_exists($shortname, $unicode_replace)) {
224                return $m[0];
225            }
226
227
228            $unicode = $unicode_replace[$shortname];
229
230            return $unicode;
231        }
232    }
233
234    /**
235     * @param   array   $m  Results of preg_replace_callback().
236     * @return  string  Image HTML replacement result.
237     */
238    public function shortnameToImageCallback($m)
239    {
240        if ((!is_array($m)) || (!isset($m[1])) || (empty($m[1]))) {
241            return $m[0];
242        }
243        else {
244            $ruleset = $this->getRuleset();
245            $shortcode_replace = $ruleset->getShortcodeReplace();
246
247            $shortname = $m[1];
248
249            if (!isset($shortcode_replace[$shortname]))
250            {
251                return $m[0];
252            }
253
254
255            $unicode = $shortcode_replace[$shortname];
256            $filename = $unicode;
257            $titleTag = $this->imageTitleTag ? 'title="'.htmlspecialchars($shortname).'"' : '';
258
259            if ($this->unicodeAlt)
260            {
261                $alt = $this->convert($unicode);
262            }
263            else
264            {
265                $alt = $shortname;
266            }
267
268            if ($this->imageType == 'png')
269            {
270                if ($this->sprites)
271                {
272                    return '<span class="emojione emojione-'.$unicode.'" title="'.htmlspecialchars($shortname).'">'.$alt.'</span>';
273                }
274                else
275                {
276                    return '<img class="emojione" alt="'.$alt.'" '.$titleTag.' src="'.$this->imagePathPNG.$filename.'.png'.$this->cacheBustParam.'"/>';
277                }
278            }
279
280            if ($this->sprites)
281            {
282                return '<svg class="emojione"><description>'.$alt.'</description><use xlink:href="'.$this->imagePathSVGSprites.'#emoji-'.$unicode.'"></use></svg>';
283            }
284            else
285            {
286                return '<object class="emojione" data="'.$this->imagePathSVG.$filename.'.svg'.$this->cacheBustParam.'" type="image/svg+xml" standby="'.$alt.'">'.$alt.'</object>';
287            }
288        }
289    }
290
291    /**
292     * @param   array   $m  Results of preg_replace_callback().
293     * @return  string  Unicode replacement result.
294     */
295    public function asciiToUnicodeCallback($m)
296    {
297        if ((!is_array($m)) || (!isset($m[3])) || (empty($m[3])))
298        {
299            return $m[0];
300        }
301        else
302        {
303            $ruleset = $this->getRuleset();
304            $ascii_replace = $ruleset->getAsciiReplace();
305
306            $shortname = $m[3];
307            $unicode = $ascii_replace[$shortname];
308            return $m[2].$this->convert($unicode);
309        }
310    }
311
312    /**
313     * @param   array   $m  Results of preg_replace_callback().
314     * @return  string  Shortname replacement result.
315     */
316    public function asciiToShortnameCallback($m)
317    {
318        if ((!is_array($m)) || (!isset($m[3])) || (empty($m[3])))
319        {
320            return $m[0];
321        }
322        else
323        {
324            $ruleset = $this->getRuleset();
325            $ascii_replace = $ruleset->getAsciiReplace();
326
327            $shortcode_replace = array_flip(array_reverse($ruleset->getShortcodeReplace()));
328            $shortname = $m[3];
329            $unicode = $ascii_replace[$shortname];
330            return $m[2].$shortcode_replace[$unicode];
331        }
332    }
333
334    /**
335     * @param   array   $m  Results of preg_replace_callback().
336     * @return  string  Image HTML replacement result.
337     */
338    public function asciiToImageCallback($m)
339    {
340        if ((!is_array($m)) || (!isset($m[3])) || (empty($m[3])))
341        {
342            return $m[0];
343        }
344        else
345        {
346            $ruleset = $this->getRuleset();
347            $ascii_replace = $ruleset->getAsciiReplace();
348
349            $shortname = html_entity_decode($m[3]);
350            $unicode = $ascii_replace[$shortname];
351            $titleTag = $this->imageTitleTag ? 'title="'.htmlspecialchars($shortname).'"' : '';
352
353            // unicode char or shortname for the alt tag? (unicode is better for copying and pasting the resulting text)
354            if ($this->unicodeAlt)
355            {
356                $alt = $this->convert($unicode);
357            }
358            else
359            {
360                $alt = htmlspecialchars($shortname);
361            }
362
363            if ($this->imageType == 'png')
364            {
365                if ($this->sprites)
366                {
367                    return $m[2].'<span class="emojione emojione-'.$unicode.'" title="'.htmlspecialchars($shortname).'">'.$alt.'</span>';
368                }
369                else
370                {
371                    return $m[2].'<img class="emojione" alt="'.$alt.'" '.$titleTag.' src="'.$this->imagePathPNG.$unicode.'.png'.$this->cacheBustParam.'"/>';
372                }
373            }
374
375            if ($this->sprites)
376            {
377                return $m[2].'<svg class="emojione"><description>'.$alt.'</description><use xlink:href="'.$this->imagePathSVGSprites.'#emoji-'.$unicode.'"></use></svg>';
378            }
379            else
380            {
381                return $m[2].'<object class="emojione" data="'.$this->imagePathSVG.$unicode.'.svg'.$this->cacheBustParam.'" type="image/svg+xml" standby="'.$alt.'">'.$alt.'</object>';
382            }
383        }
384    }
385
386    /**
387     * @param   array   $m  Results of preg_replace_callback().
388     * @return  string  shortname result
389     */
390    public function toShortCallback($m)
391    {
392        if ((!is_array($m)) || (!isset($m[1])) || (empty($m[1])))
393        {
394            return $m[0];
395        }
396        else
397        {
398            $ruleset = $this->getRuleset();
399            $unicode_replace = $ruleset->getUnicodeReplace();
400
401            $unicode = $m[1];
402
403            if (!in_array($unicode, $unicode_replace))
404            {
405                $unicode .= "\xEF\xB8\x8F";
406
407                if (!in_array($unicode, $unicode_replace))
408                {
409                    $unicode = substr($m[1], 0, 4);
410
411                    if (!in_array($unicode, $unicode_replace))
412                    {
413                        return $m[0];
414                    }
415                }
416            }
417
418            return array_search($unicode, $unicode_replace);
419        }
420    }
421
422    /**
423     * @param   array   $m  Results of preg_replace_callback().
424     * @return  string  Image HTML replacement result.
425     */
426    public function unicodeToImageCallback($m)
427    {
428        if ((!is_array($m)) || (!isset($m[1])) || (empty($m[1])))
429        {
430            return $m[0];
431        }
432        else
433        {
434            $ruleset = $this->getRuleset();
435            $shortcode_replace = $ruleset->getShortcodeReplace();
436            $unicode_replace = $ruleset->getUnicodeReplace();
437
438            $unicode = $m[1];
439
440            if (!in_array($unicode, $unicode_replace))
441            {
442                $unicode .= "\xEF\xB8\x8F";
443
444                if (!in_array($unicode, $unicode_replace))
445                {
446                    $unicode = substr($m[1], 0, 4);
447
448                    if (!in_array($unicode, $unicode_replace))
449                    {
450                        if ("\xE2\x83\xA3" === substr($m[1], 1, 3))
451                        {
452                            $unicode = substr($m[1], 0, 1) . "\xEF\xB8\x8F\xE2\x83\xA3";
453
454                            if (!in_array($unicode, $unicode_replace))
455                            {
456                                return $m[0];
457                            }
458                        }
459                        else
460                        {
461                            return $m[0];
462                        }
463                    }
464                }
465            }
466
467            $shortname = array_search($unicode, $unicode_replace);
468            $filename = $shortcode_replace[$shortname];
469            $titleTag = $this->imageTitleTag ? 'title="'.htmlspecialchars($shortname).'"' : '';
470
471            if ($this->unicodeAlt)
472            {
473                $alt = $unicode;
474            }
475            else
476            {
477                $alt = $shortname;
478            }
479
480            if ($this->imageType == 'png')
481            {
482                if ($this->sprites)
483                {
484                    return '<span class="emojione emojione-'.$filename.'" title="'.htmlspecialchars($shortname).'">'.$alt.'</span>';
485                }
486                else
487                {
488                    return '<img class="emojione" alt="'.$alt.'" '.$titleTag.' title="'.htmlspecialchars($shortname).'" src="'.$this->imagePathPNG.$filename.'.png'.$this->cacheBustParam.'"/>';
489                }
490            }
491
492            if ($this->sprites)
493            {
494                return '<svg class="emojione"><description>'.$alt.'</description><use xlink:href="'.$this->imagePathSVGSprites.'#emoji-'.$filename.'"></use></svg>';
495            }
496            else
497            {
498                return '<object class="emojione" data="'.$this->imagePathSVG.$filename.'.svg'.$this->cacheBustParam.'" type="image/svg+xml" standby="'.$alt.'">'.$alt.'</object>';
499            }
500        }
501    }
502
503    // ##########################################
504    // ######## helper methods
505    // ##########################################
506
507    /**
508     * Converts from unicode to hexadecimal NCR.
509     *
510     * @param   string  $unicode unicode character/s
511     * @return  string  hexadecimal NCR
512     * */
513    public function convert($unicode)
514    {
515        if (stristr($unicode,'-'))
516        {
517            $pairs = explode('-',$unicode);
518            return '&#x'.implode(';&#x',$pairs).';';
519        }
520        else
521        {
522            return '&#x'.$unicode.';';
523        }
524    }
525
526    /**
527     * Get the Ruleset
528     *
529     * @return RulesetInterface The Ruleset
530     */
531    public function getRuleset()
532    {
533        if ( $this->ruleset === null )
534        {
535            $this->ruleset = new Ruleset;
536        }
537
538        return $this->ruleset;
539    }
540}
541