Browse Source

Renew course chat - refs BT#11284 #tmi (#1225)

* Fix get history files from course chat - refs BT#11284

* Clean code + Create template for course chat - refs BT#11284

* Renew course chat - refs BT#11284

* Allow course chat between two users - refs BT#11284

* Improve course chat style and template - refs BT#11284

* Add event when user access to chat tool - refs BT#11284

* Show all course users instead of connected users only - refs BT#11284

* Fix course chat in basis courses - refs BT#111284

* Filter users on chat by course - refs BT#11284

* Update jquery.textcomplete.js - refs BT#11248

* Fix autocomplete for emojis in course chat - refs BT#11284

* Search url in messages for course chat - refs BT#11284

* Add ctrl + Enter event to send messages in course chat - refs BT#11284
Angel Fernando Quiroz Campos 8 years ago
parent
commit
bcfdf70f75

+ 38 - 166
app/Resources/public/css/chat.css

@@ -1,13 +1,5 @@
-body{
-	font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-	font-size: 13px;
-  	line-height: 18px;
-  	color: #333333;
-  	margin: 0;
-  	padding: 0;
-}
 .page-chat{
-	padding: 10px;
+	padding: 10px 0;
 	background:#ffffff;
 }
 .message-form-chat .nav{
@@ -16,48 +8,7 @@ body{
 .message-form-chat .nav-tabs .active a{
   background-color: #f6f6f6;
 }
-#clear-chat{
-	width:140px;
-    padding: 13px;
-    font-size: 14px;
-    float:right;
-}
-#clear-chat img{
-	width: 20px;
-	float: left;
-	margin-right: 5px;
-}
-#clear-chat a{
-	text-decoration: none;
 
-}
-#clear-chat a:hover{
-	opacity: 0.7;
-}
-#content-chat{
-	background:#ffffff;
-	padding: 30px;
-	-webkit-border-radius: 10px;
-	-moz-border-radius: 10px;
-	border-radius: 10px;
-}
-
-textarea {
-	width: 52%;
-	padding: 3%;
-	height: 100px;
-	float: left;
-	border: 0;
-	background-color: #ffffff;
-
-	font-family: Helvetica, arial, sans-serif;
-	-webkit-border-radius: 10px;
-	-moz-border-radius: 10px;
-	border-radius: 10px;
-	border: 1px solid #EEEEEE;
-	color: #666;
-	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35) inset;
-}
 .message-student .chat-image, .message-teacher .chat-image{
  	width: 80px;
  	height: auto;
@@ -85,7 +36,7 @@ textarea {
 	font-size: 13px;
 	padding: 15px;
 	vertical-align: top;
-	width: 310px;
+	width: calc(100% - 100px);
 }
 .message-teacher .icon-message{
 	display: inline-block;
@@ -121,7 +72,7 @@ textarea {
 	font-size: 13px;
 	padding: 15px;
 	vertical-align: top;
-	width: 360px;
+	width: calc(100% - 100px);
 }
 .message-teacher .message-date{
 	color: #666;
@@ -138,121 +89,30 @@ textarea {
 	text-align: right;
 	margin-top: 10px;
 }
-.user-online .profile{
-	list-style: none;
-	padding: 0;
-	margin:0;
-}
-.chat-message-block-name {
-    font-style: italic;
-    color: #666;
-}
-.user-online .profile li{
-	margin-bottom: 5px;
-}
-.user-online .profile li a{
-	color: #0088AA;
-	text-decoration: none;
-}
-.user-online .title{
-	background: url(chat.png) no-repeat 0 center;
-	color: #0088AA;
-	padding-top: 5px;
-	padding-left: 30px;
-	font-weight: bold;
-	font-size: 16px;
-	margin-bottom: 15px;
-	width: 90%;
-}
-.delete-chat{
-	background: #F8F8F8;
-	padding-bottom: 15px;
-}
-.list-group {
-  margin-bottom: 20px;
-  padding-left: 0;
-}
-.list-group-item {
-  position: relative;
-  display: block;
-  padding-top: 5px;
-  padding-bottom: 10px;
-  padding-left: 10px;
-  padding-right: 10px;
-  margin-bottom: -1px;
-  background-color: #EEEEEE;
-  border: 1px solid #E2E2E2;
-  -webkit-border-radius: 10px;
+
+.chat-user {
+	background-color: #EEEEEE;
+	border: 1px solid #E2E2E2;
+	-webkit-border-radius: 10px;
 	-moz-border-radius: 10px;
 	border-radius: 10px;
 	color: #666;
+	margin-bottom: -1px;
+	padding: 10px;
 }
-.user-connected{
-  border-right: 1px dashed #cccccc;
-  margin-right: 5px;
-}
-li.list-group-item:hover, li.list-group-item:focus {
-  text-decoration: none;
-  background-color: #E2E2E2;
-  cursor: pointer;
-  border: 1px solid #EEEEEE;
-}
-li.list-group-item:hover a{
-	color: #000;
-}
-.user-image-chat{
-	width: 40px;
- 	height: auto;
- 	max-height: 40px;
- 	display: inline-block;
- 	vertical-align: top;
- 	-webkit-border-radius: 10px;
+.chat-user .user-image-chat {
+	-webkit-border-radius: 10px;
 	-moz-border-radius: 10px;
+	border: 2px solid #FFF;
 	border-radius: 10px;
-	border:2px solid #ffffff;
+	display: inline-block;
+	height: auto;
+	margin-right: 10px;
+	max-height: 40px;
 	float: left;
-	margin-right: 5px;
-}
-.noSelect {
-    user-select: none;
-    -o-user-select: none;
-    -moz-user-select: none;
-    -khtml-user-select: none;
-    -webkit-user-select: none;
-}
-.btn-send {
-	background: #87e0fd; /* Old browsers */
-	background: -moz-linear-gradient(top, #87e0fd 0%, #53cbf1 40%, #05abe0 100%); /* FF3.6+ */
-	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#87e0fd), color-stop(40%,#53cbf1), color-stop(100%,#05abe0)); /* Chrome,Safari4+ */
-	background: -webkit-linear-gradient(top, #87e0fd 0%,#53cbf1 40%,#05abe0 100%); /* Chrome10+,Safari5.1+ */
-	background: -o-linear-gradient(top, #87e0fd 0%,#53cbf1 40%,#05abe0 100%); /* Opera 11.10+ */
-	background: -ms-linear-gradient(top, #87e0fd 0%,#53cbf1 40%,#05abe0 100%); /* IE10+ */
-	background: linear-gradient(to bottom, #87e0fd 0%,#53cbf1 40%,#05abe0 100%); /* W3C */
-	filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#87e0fd', endColorstr='#05abe0',GradientType=0 ); /* IE6-9 */
-	-webkit-border-radius: 15px;
-	-moz-border-radius: 15px;
-	border-radius: 15px;
-	border:1px solid #53cbf1;
-	display:inline-block;
-	color:#ffffff;
-	font-family:Arial;
-	font-size:22px;
-	font-weight:bold;
-	font-style:normal;
-	height:80px;
-	line-height:80px;
-	width:131px;
-	text-decoration:none;
-	text-align:center;
-	cursor: pointer;
-}
-.btn-send:hover {
-	opacity: 0.8;
-}
-.btn-send:active {
-	position:relative;
-	top:1px;
+	width: 40px;
 }
+
 .emoticons-chat img{
   	width: 24px;
   	border: none;
@@ -267,21 +127,17 @@ li.list-group-item:hover a{
 }
 
 .emoji-menu {
-    margin-left: 0px;
-    width:468px;
-    left: 11px !important;
+	margin-left: -234px;
+	margin-top: -224px;
+	width: 468px;
 }
 
 .emoji-wysiwyg-editor-preview,
 .emoji-wysiwyg-editor {
-    width: 454px;
     padding: 3%;
     height: 50px;
-    float: left;
     border: 0;
     background-color: #ffffff;
-
-    font-family: Helvetica, arial, sans-serif;
     -webkit-border-radius: 10px;
     -moz-border-radius: 10px;
     border-radius: 10px;
@@ -289,3 +145,19 @@ li.list-group-item:hover a{
     color: #666;
     box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35) inset;
 }
+
+#chat-tabs .tab-content{
+	margin-top: 15px;
+}
+
+.chat-history {
+	height: 400px;
+	overflow: auto;
+}
+
+#chat-users {
+	margin-left: 0;
+}
+#chat-users div.chat-user > div {
+	padding: 5px;
+}

+ 0 - 1
index.php

@@ -15,7 +15,6 @@ define('CHAMILO_LOAD_WYSIWYG', false);
 $cidReset = true;
 
 require_once 'main/inc/global.inc.php';
-require_once 'main/chat/chat_functions.lib.php';
 //require_once 'main/auth/external_login/facebook.inc.php';
 
 // The section (for the tabs).

+ 39 - 73
main/chat/chat.php

@@ -1,92 +1,58 @@
 <?php
 /* For licensing terms, see /license.txt */
 
-/**
- * Chat tool
- * @package chamilo.chat
- */
+define('CHAMILO_LOAD_WYSIWYG', false);
 
 require_once '../inc/global.inc.php';
-$current_course_tool  = TOOL_CHAT;
-$this_section = SECTION_COURSES;
-$nameTools = get_lang('ToolChat');
 
-$origin = isset($_GET["origin"]) ? Security::remove_XSS($_GET["origin"]) : null;
-$target = isset($_GET["target"]) ? Security::remove_XSS($_GET["target"]) : null;
+api_protect_course_script(true);
 
-if ($origin != 'whoisonline') {
-    api_protect_course_script(true);
-} else {
-    $origin = $_SESSION['origin'];
-    $target = $_SESSION['target'];
-    $_SESSION['origin']= $origin;
-    $_SESSION['target']= $target;
-}
-
-api_protect_course_group(GroupManager::GROUP_TOOL_CHAT, false);
-
-/*  TRACKING */
 Event::event_access_tool(TOOL_CHAT);
-header('Content-Type: text/html; charset=UTF-8');
 
-/*
- * Choose CSS style (platform's, user's, or course's)
- */
-$my_style = api_get_visual_theme();
-
-$mycourseid = api_get_course_id();
-if (!empty($mycourseid) && $mycourseid != -1) {
-	$open_chat_window = api_get_course_setting('allow_open_chat_window');
+// View
+$externalCSS = [
+    'jquery-emojiarea/jquery.emojiarea.css',
+    'jquery-textcomplete/jquery.textcomplete.css',
+    'emojione/css/emojione.min.css',
+    'emojione/css/autocomplete.css',
+    'highlight/styles/github.css'
+];
+
+foreach ($externalCSS as $css) {
+    $htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_JS_PATH) . $css);
 }
 
-$courseCode = Security::remove_XSS($_GET['cidReq']);
-
-?>
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="UTF-8" />
-<link rel="stylesheet" type="text/css" href="<?php echo api_get_path(WEB_CSS_PATH); ?>chat.css">
-<?php
-echo'<title>'.get_lang('Chat').' - '.$mycourseid.' - '.api_get_setting('siteName').'</title>';
-
-$groupId = api_get_group_id();
+$htmlHeadXtra[] = api_get_css(api_get_path(WEB_CSS_PATH) . 'chat.css');
+$htmlHeadXtra[] = api_get_css(api_get_path(WEB_CSS_PATH) . 'markdown.css');
 
-// If it is a group chat then the breadcrumbs.
-if (!empty($groupId)) {
-	$group_properties  = GroupManager :: get_group_properties($groupId);
-    $interbreadcrumb[] = array(
-        'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.api_get_cidreq(),
-        'name' => get_lang('Groups')
-    );
-	$interbreadcrumb[] = array(
-        'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.api_get_cidreq(),
-        'name' => get_lang('GroupSpace').' '.$group_properties['name']
-    );
-	$noPHP_SELF = true;
-	$shortBanner = false;
-	$add_group_to_title = ' ('.$group_properties['name'].')';
-	$groupfilter = 'group_id="'.$groupId.'"';
-} else {
-	$groupfilter = 'group_id=0';
-}
+$externalJS = [
+    'highlight/highlight.pack.js',
+    'jquery-textcomplete/jquery.textcomplete.js',
+    'emojione/js/emojione.min.js',
+    'jquery-emojiarea/jquery.emojiarea.js'
+];
 
-if (empty($open_chat_window)) {
-    Display::display_header($tool_name, 'Chat');
+foreach ($externalJS as $js) {
+    $htmlHeadXtra[] = api_get_js($js);
 }
 
-$url = api_get_path(WEB_CODE_PATH).'chat/';
-$params = api_get_cidreq();
+$iconList = [];
 
-echo '<div class="page-chat">';
-echo '<iframe src="'.$url.'chat_whoisonline.php?'.$params.'" name="chat_whoisonline" scrolling="no" style="height:550px; width:35%; border: 0px none; float:left"></iframe>';
-echo '<iframe src="'.$url.'chat_chat.php?origin='.$origin.'&target='.$target.'&'.$params.'" name="chat_chat" id="chat_chat" scrolling="auto" height="380" style="width:65%; border: 0px none; float:right"></iframe>';
-echo '<iframe src="'.$url.'chat_message.php?'.$params.'" name="chat_message" scrolling="no" height="182px" style="width:65%; border: 0px none; float:right"></iframe>';
-echo '<iframe src="'.$url.'chat_hidden.php?'.$params.'" name="chat_hidden" height="0px" style="height:0px; border: 0px none"></iframe>';
-echo '</div>';
+foreach (Emojione\Emojione::$shortcode_replace as $key => $icon) {
+    if (!in_array($key, CourseChatUtils::getEmojisToInclude())) {
+        continue;
+    }
 
-if (empty($open_chat_window)) {
-    Display::display_footer();
+    $iconList[$key] = strtoupper($icon) . '.png';
 }
 
-echo '</html>';
+$view = new Template(get_lang('Chat'), false, false, false, true, false);
+$view->assign('icons', $iconList);
+$view->assign('emoji_strategy', CourseChatUtils::getEmojiStrategry());
+$view->assign('emoji_smile', \Emojione\Emojione::toImage(':smile:'));
+
+$template = $view->get_template('chat/chat.tpl');
+$content = $view->fetch($template);
+
+$view->assign('content', $content);
+$view->display_one_col_template();

+ 0 - 217
main/chat/chat_chat.php

@@ -1,217 +0,0 @@
-<?php
-/* For licensing terms, see /license.txt */
-
-/**
- *	Chat frame that shows the message list
- *
- *	@author Olivier Brouckaert
- *	@package chamilo.chat
- */
-
-define('FRAME', 'chat');
-
-require_once '../inc/global.inc.php';
-
-$course = $_GET['cidReq'];
-$session_id = api_get_session_id();
-$group_id 	= api_get_group_id();
-$userId = api_get_user_id();
-$_course = api_get_course_info();
-$time = api_get_utc_datetime();
-
-// if we have the session set up
-if (!empty($course)) {
-    $reset = isset($_GET['reset']) ? (bool)$_GET['reset'] : null;
-    $tbl_user = Database::get_main_table(TABLE_MAIN_USER);
-    $query = "SELECT username FROM $tbl_user WHERE user_id='".$userId."'";
-    $result = Database::query($query);
-
-    list($pseudo_user) = Database::fetch_row($result);
-
-    $isAllowed = !(empty($pseudo_user) || !$_cid);
-    $isMaster = (bool)api_is_course_admin();
-
-    $date_now = date('Y-m-d');
-    $basepath_chat = '';
-    $document_path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
-    if (!empty($group_id)) {
-        $group_info = GroupManager :: get_group_properties($group_id);
-        $basepath_chat = $group_info['directory'].'/chat_files';
-    } else {
-        $basepath_chat = '/chat_files';
-    }
-    $chat_path = $document_path.$basepath_chat.'/';
-    $TABLEITEMPROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
-    $course_id = api_get_course_int_id();
-
-    if (!is_dir($chat_path)) {
-        if (is_file($chat_path)) {
-            @unlink($chat_path);
-        }
-
-        if (!api_is_anonymous()) {
-            @mkdir($chat_path, api_get_permissions_for_new_directories());
-            // Save chat files document for group into item property
-            if (!empty($group_id)) {
-                $doc_id = add_document($_course, $basepath_chat, 'folder', 0, 'chat_files');
-                api_item_property_update(
-                    $_course,
-                    TOOL_DOCUMENT,
-                    $doc_id,
-                    'FolderCreated',
-                    null,
-                    $group_id,
-                    null,
-                    null,
-                    null
-                );
-            }
-        }
-    }
-
-    $filename_chat = '';
-    if (!empty($group_id)) {
-        $filename_chat = 'messages-'.$date_now.'_gid-'.$group_id.'.log.html';
-    } else if (!empty($session_id)) {
-        $filename_chat = 'messages-'.$date_now.'_sid-'.$session_id.'.log.html';
-    } else {
-        $filename_chat = 'messages-'.$date_now.'.log.html';
-    }
-
-	if (!file_exists($chat_path.$filename_chat)) {
-		@fclose(fopen($chat_path.$filename_chat, 'w'));
-		if (!api_is_anonymous()) {
-			$doc_id = add_document($_course, $basepath_chat.'/'.$filename_chat, 'file', 0, $filename_chat);
-            api_item_property_update(
-                $_course,
-                TOOL_DOCUMENT,
-                $doc_id,
-                'DocumentAdded',
-                $userId,
-                $group_id,
-                null,
-                null,
-                null,
-                $session_id
-            );
-            api_item_property_update(
-                $_course,
-                TOOL_DOCUMENT,
-                $doc_id,
-                'invisible',
-                $userId,
-                $group_id,
-                null,
-                null,
-                null,
-                $session_id
-            );
-            item_property_update_on_folder($_course, $basepath_chat, $userId);
-		}
-	}
-
-	$basename_chat = '';
-	if (!empty($group_id)) {
-		$basename_chat = 'messages-'.$date_now.'_gid-'.$group_id;
-	} else if (!empty($session_id)) {
-		$basename_chat = 'messages-'.$date_now.'_sid-'.$session_id;
-	} else {
-		$basename_chat = 'messages-'.$date_now;
-	}
-
-	if ($reset && $isMaster) {
-
-		$i = 1;
-		while (file_exists($chat_path.$basename_chat.'-'.$i.'.log.html')) {
-			$i++;
-		}
-
-		@rename($chat_path.$basename_chat.'.log.html', $chat_path.$basename_chat.'-'.$i.'.log.html');
-		@fclose(fopen($chat_path.$basename_chat.'.log.html', 'w'));
-		$doc_id = add_document($_course, $basepath_chat.'/'.$basename_chat.'-'.$i.'.log.html', 'file', filesize($chat_path.$basename_chat.'-'.$i.'.log.html'), $basename_chat.'-'.$i.'.log.html');
-
-        api_item_property_update(
-            $_course,
-            TOOL_DOCUMENT,
-            $doc_id,
-            'DocumentAdded',
-            $userId,
-            $group_id,
-            null,
-            null,
-            null,
-            $session_id
-        );
-        api_item_property_update(
-            $_course,
-            TOOL_DOCUMENT,
-            $doc_id,
-            'invisible',
-            $userId,
-            $group_id,
-            null,
-            null,
-            null,
-            $session_id
-        );
-        item_property_update_on_folder($_course, $basepath_chat, $userId);
-
-        $doc_id = DocumentManager::get_document_id(
-            $_course,
-            $basepath_chat.'/'.$basename_chat.'.log.html'
-        );
-
-		update_existing_document($_course, $doc_id, 0);
-	}
-
-	$remove = 0;
-	$content = array();
-	if (file_exists($chat_path.$basename_chat.'.log.html')) {
-		$content = file($chat_path.$basename_chat.'.log.html');
-		$nbr_lines = sizeof($content);
-		$remove = $nbr_lines - 100;
-	}
-
-	if ($remove < 0) {
-		$remove = 0;
-	}
-
-	array_splice($content, 0, $remove);
-	require 'header_frame.inc.php';
-
-	if (isset($_GET['origin']) && $_GET['origin'] == 'whoisonline') {
-	    //the caller
-		$content[0] = get_lang('CallSent').'<br />'.$content[0];
-	}
-	if (isset($_GET['origin']) && $_GET['origin'] == 'whoisonlinejoin') {
-	    //the joiner (we have to delete the chat request to him when he joins the chat)
-		$track_user_table = Database::get_main_table(TABLE_MAIN_USER);
-		$sql = "UPDATE $track_user_table SET
-                    chatcall_user_id = '',
-                    chatcall_date = '',
-                    chatcall_text=''
-		        WHERE (user_id = ".$userId.")";
-		$result = Database::query($sql);
-	}
-        
-	echo '<div id="content-chat"';
-	foreach ($content as & $this_line) {
-        echo $this_line;
-	}
-	echo '</div>';
-	echo '<a name="bottom" style="text-decoration:none;">&nbsp;</a>';
-	if ($isMaster || $is_courseCoach) {
-		$rand = mt_rand(1, 1000);
-		echo '<div id="clear-chat">';
-		echo '<a class="btn btn-danger btn-small " href="'.api_get_self().'?rand='.$rand.'&reset=1&'.api_get_cidreq().'#bottom" onclick="javascript: if(!confirm(\''.addslashes(api_htmlentities(get_lang('ConfirmReset'), ENT_QUOTES)).'\')) return false;">'.
-            get_lang('ClearList').
-            '</a>';
-		echo '</div>';
-	}
-} else {
-	echo '</div>';
-	require 'header_frame.inc.php';
-	$message = get_lang('CloseOtherSession');
-	Display :: display_error_message($message);
-}
-require 'footer_frame.inc.php';

+ 0 - 242
main/chat/chat_functions.lib.php

@@ -1,242 +0,0 @@
-<?php
-/* For licensing terms, see /license.txt */
-/**
- *	@package chamilo.chat
- */
-use Michelf\MarkdownExtra;
-
-/**
- * @author isaac flores paz
- * @param integer $user_id
- *
- * @return boolean
- * @todo this function need more parameters seems not to be use anymore
- * @deprecated fix this function or create another
- */
-function user_connected_in_chat($user_id)
-{
- 	$tbl_chat_connected = Database::get_course_table(TABLE_CHAT_CONNECTED);
-    $user_id 	= intval($user_id);
- 	$session_id = api_get_session_id();
-    $group_id   = api_get_group_id();
-    $course_id  = api_get_course_int_id();
-
-	if (!empty($group_id)) {
-		$extra_condition = " AND to_group_id = '$group_id'";
-	} else {
-		$extra_condition = api_get_session_condition($session_id);
-	}
-
- 	$sql = 'SELECT COUNT(*) AS count
- 	        FROM '.$tbl_chat_connected .' c
- 	        WHERE
- 	            c_id = '.$course_id.' AND
- 	            user_id='.$user_id.$extra_condition;
- 	$result = Database::query($sql);
- 	$count  = Database::fetch_array($result,'ASSOC');
-
- 	return $count['count'] == 1;
-}
-
-/**
- * @param integer
- * @return void
- */
-function exit_of_chat($user_id)
-{
-    $user_id = intval($user_id);
-    $list_course = CourseManager::get_courses_list_by_user_id($user_id);
-    $tbl_chat_connected = Database::get_course_table(TABLE_CHAT_CONNECTED);
-
-    foreach ($list_course as $course) {
-        $response = user_connected_in_chat($user_id);
-
-        $sql = 'DELETE FROM '.$tbl_chat_connected.'
-                WHERE c_id = '.$course['real_id'].' AND user_id = '.$user_id;
-        Database::query($sql);
-
-    }
-}
-
-/**
- * @return void
- */
-function disconnect_user_of_chat()
-{
-    $list_info_user_in_chat = users_list_in_chat();
-    $course_id = api_get_course_int_id();
-    $groupId = api_get_group_id();
-    $now = time();
-    $cd_date = date('Y-m-d', $now);
-    $cdate_h = date('H', $now);
-    $cdate_m = date('i', $now);
-    $cdate_s = date('s', $now);
-	$cd_count_time_seconds = $cdate_h*3600 + $cdate_m*60 + $cdate_s;
-
-    if (is_array($list_info_user_in_chat) && count($list_info_user_in_chat) > 0) {
-        foreach ($list_info_user_in_chat as $list_info_user) {
-            $date_db_date = date('Y-m-d', api_strtotime($list_info_user['last_connection'], 'UTC'));
-            $date_db_h  = date('H', api_strtotime($list_info_user['last_connection'], 'UTC'));
-            $date_db_m  = date('i', api_strtotime($list_info_user['last_connection'], 'UTC'));
-            $date_db_s  = date('s', api_strtotime($list_info_user['last_connection'], 'UTC'));
-            $date_count_time_seconds = $date_db_h * 3600 + $date_db_m * 60 + $date_db_s;
-            if ($cd_date == $date_db_date) {
-                if (($cd_count_time_seconds - $date_count_time_seconds) > 5) {
-                    $tbl_chat_connected = Database::get_course_table(TABLE_CHAT_CONNECTED);
-                    $sql = 'DELETE FROM '.$tbl_chat_connected.'
-                            WHERE
-                                c_id = '.$course_id.' AND
-                                user_id = '.$list_info_user['user_id'].' AND
-                                to_group_id = '.$groupId.'
-                            ';
-                    Database::query($sql);
-                }
-            }
-        }
-    }
-}
-
-/**
- * @return array user list in chat
- */
-function users_list_in_chat()
-{
-	$list_users_in_chat = array();
- 	$tbl_chat_connected = Database::get_course_table(TABLE_CHAT_CONNECTED);
-    $course_id = api_get_course_int_id();
-
- 	$session_id = api_get_session_id();
-    $group_id   = api_get_group_id();
-
-	if (!empty($group_id)) {
-		$extra_condition = " WHERE to_group_id = '$group_id'";
-	} else{
-		$extra_condition = api_get_session_condition($session_id, false);
-	}
-    $extra_condition.= " AND c_id = $course_id ";
- 	$sql = 'SELECT user_id, last_connection FROM '.$tbl_chat_connected.$extra_condition;
- 	$result = Database::query($sql);
- 	while ($row = Database::fetch_array($result, 'ASSOC')) {
- 		$list_users_in_chat[] = $row;
- 	}
-
- 	return $list_users_in_chat;
-}
-
-/**
- * @param string $message
- * @param array $_course
- * @param int $group_id
- * @param int $session_id
- * @param bool $preview
- */
-function saveMessage($message, $userId, $_course, $session_id, $group_id, $preview = true)
-{
-    $userInfo = api_get_user_info($userId);
-    $fullName = $userInfo['complete_name'];
-    $isMaster = (bool)api_is_course_admin();
-
-    $document_path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
-    if (!empty($group_id)) {
-        $group_info = GroupManager :: get_group_properties($group_id);
-        $basepath_chat = $group_info['directory'].'/chat_files';
-    } else {
-        $basepath_chat = '/chat_files';
-    }
-    $chat_path = $document_path.$basepath_chat.'/';
-
-    if (!is_dir($chat_path)) {
-        if (is_file($chat_path)) {
-            @unlink($chat_path);
-        }
-    }
-
-    $date_now = date('Y-m-d');
-    $message = trim($message);
-    $timeNow = date('d/m/y H:i:s');
-
-    if (!empty($group_id)) {
-        $basename_chat = 'messages-'.$date_now.'_gid-'.$group_id;
-    } elseif (!empty($session_id)) {
-        $basename_chat = 'messages-'.$date_now.'_sid-'.$session_id;
-    } else {
-        $basename_chat = 'messages-'.$date_now;
-    }
-
-    if (!api_is_anonymous()) {
-        if (!empty($message)) {
-            Emojione\Emojione::$imagePathPNG = api_get_path(WEB_LIBRARY_PATH).'javascript/emojione/png/';
-            Emojione\Emojione::$ascii = true;
-
-            // Parsing emojis
-            $message = Emojione\Emojione::toImage($message);
-            // Parsing text to understand markdown (code highlight)
-            $message = MarkdownExtra::defaultTransform($message);
-            // Security XSS
-            $message = Security::remove_XSS($message);
-
-            if ($preview == true) {
-                return $message;
-            }
-
-            if (!file_exists($chat_path.$basename_chat.'.log.html')) {
-                $doc_id = add_document(
-                    $_course,
-                    $basepath_chat . '/' . $basename_chat . '.log.html',
-                    'file',
-                    0,
-                    $basename_chat . '.log.html'
-                );
-                api_item_property_update(
-                    $_course,
-                    TOOL_DOCUMENT,
-                    $doc_id,
-                    'DocumentAdded',
-                    $userId,
-                    $group_id,
-                    null,
-                    null,
-                    null,
-                    $session_id
-                );
-                api_item_property_update(
-                    $_course,
-                    TOOL_DOCUMENT,
-                    $doc_id,
-                    'invisible',
-                    $userId,
-                    $group_id,
-                    null,
-                    null,
-                    null,
-                    $session_id
-                );
-                item_property_update_on_folder(
-                    $_course,
-                    $basepath_chat,
-                    $userId
-                );
-            } else {
-                $doc_id = DocumentManager::get_document_id(
-                    $_course,
-                    $basepath_chat.'/'.$basename_chat.'.log.html'
-                );
-            }
-
-            $fp = fopen($chat_path.$basename_chat.'.log.html', 'a');
-            $userPhoto = UserManager::getUserPicture($userId, USER_IMAGE_SIZE_MEDIUM);
-            $filePhoto = '<img class="chat-image" src="'.$userPhoto.'"/>';
-            if ($isMaster) {
-                fputs($fp, '<div class="message-teacher"><div class="content-message"><div class="chat-message-block-name">'.$fullName.'</div><div class="chat-message-block-content">'.$message.'</div><div class="message-date">'.$timeNow.'</div></div><div class="icon-message"></div>'.$filePhoto.'</div>'."\n");
-            } else {
-                fputs($fp, '<div class="message-student">'.$filePhoto.'<div class="icon-message"></div><div class="content-message"><div class="chat-message-block-name">'.$fullName.'</div><div class="chat-message-block-content">'.$message.'</div><div class="message-date">'.$timeNow.'</div></div></div>'."\n");
-            }
-            fclose($fp);
-
-            $chat_size = filesize($chat_path.$basename_chat.'.log.html');
-
-            update_existing_document($_course, $doc_id, $chat_size);
-            item_property_update_on_folder($_course, $basepath_chat, $userId);
-        }
-    }
-}

+ 0 - 126
main/chat/chat_hidden.php

@@ -1,126 +0,0 @@
-<?php
-/* For licensing terms, see /license.txt */
-
-/**
- *	Hidden frame that refreshes the visible frames when a modification occurs
- *
- *	@author Olivier Brouckaert
- *	@package chamilo.chat
- */
-
-define('FRAME', 'hidden');
-
-require_once '../inc/global.inc.php';
-require_once 'chat_functions.lib.php';
-
-$tbl_user = Database::get_main_table(TABLE_MAIN_USER);
-$tbl_chat_connected = Database::get_course_table(TABLE_CHAT_CONNECTED);
-$userId = api_get_user_id();
-$course_id = api_get_course_int_id();
-
-$query = "SELECT username FROM $tbl_user WHERE user_id='".$userId."'";
-$result = Database::query($query);
-
-list($pseudo_user) = Database::fetch_row($result);
-
-$isAllowed = !(empty($pseudo_user) || !$_cid);
-$isMaster = (bool)api_is_course_admin();
-
-$date_now = date('Y-m-d');
-
-$group_id = api_get_group_id();
-$session_id = api_get_session_id();
-$session_condition = api_get_session_condition($session_id);
-$group_condition = " AND to_group_id = '$group_id'";
-
-$extra_condition = '';
-if (!empty($group_id)) {
-	$extra_condition = $group_condition;
-} else {
-	$extra_condition = $session_condition;
-}
-
-$extra_condition.= " AND c_id = $course_id";
-
-// get chat path
-$chat_path = '';
-$document_path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
-if (!empty($group_id)) {
-	$group_info = GroupManager :: get_group_properties($group_id);
-	$chat_path = $document_path.$group_info['directory'].'/chat_files/';
-} else {
-	$chat_path = $document_path.'/chat_files/';
-}
-
-// get chat file
-$basename_chat = '';
-if (!empty($group_id)) {
-	$basename_chat = 'messages-'.$date_now.'_gid-'.$group_id;
-} elseif (!empty($session_id)) {
-	$basename_chat = 'messages-'.$date_now.'_sid-'.$session_id;
-} else {
-	$basename_chat = 'messages-'.$date_now;
-}
-
-$chat_size_old = isset($_POST['chat_size_old']) ? intval($_POST['chat_size_old']) : null;
-
-$file = $chat_path.$basename_chat.'.log.html';
-$chat_size_new = 0;
-if (file_exists($file)) {
-    $chat_size_new = filesize($file);
-}
-
-$sql = "SELECT user_id FROM $tbl_chat_connected
-        WHERE user_id='".$userId."' $extra_condition";
-$result = Database::query($sql);
-
-// The user_id exists so we must do an UPDATE and not a INSERT
-$current_time = api_get_utc_datetime();
-
-if (Database::num_rows($result) == 0) {
-	$query = "INSERT INTO $tbl_chat_connected(c_id, user_id, last_connection,session_id,to_group_id)
-	          VALUES($course_id, '".$userId."','$current_time','$session_id','$group_id')";
-    Database::query($query);
-    $id = Database::insert_id();
-    if ($id) {
-        $sql = "UPDATE $tbl_chat_connected SET id = iid WHERE iid = $id";
-        Database::query($sql);
-    }
-} else {
-	$query = "UPDATE $tbl_chat_connected SET
-	          last_connection='".$current_time."'
-	          WHERE c_id = $course_id AND user_id='".$userId."' AND session_id='$session_id' AND to_group_id='$group_id'";
-    Database::query($query);
-}
-
-$query = "SELECT COUNT(user_id) FROM $tbl_chat_connected
-          WHERE last_connection>'".date('Y-m-d H:i:s',time()-60*5)."' $extra_condition";
-$result = Database::query($query);
-
-$connected_old = isset($_POST['connected_old']) ? intval($_POST['connected_old']) : null;
-list($connected_new) = Database::fetch_row($result);
-
-/*disconnected user of chat*/
-disconnect_user_of_chat();
-require 'header_frame.inc.php';
-?>
-<form name="formHidden" method="post" action="<?php echo api_get_self().'?'.api_get_cidreq(); ?>">
-<input type="hidden" name="chat_size_old" value="<?php echo $chat_size_new; ?>">
-<input type="hidden" name="connected_old" value="<?php echo $connected_new; ?>">
-</form>
-<?php
-
-if (isset($_SESSION["origin"]) && $_SESSION["origin"] == 'whoisonline') {
-    //check if our target has denied our request or not
-    $talk_to = $_SESSION["target"];
-    $track_user_table = Database::get_main_table(TABLE_MAIN_USER);
-    $sql = "select chatcall_text from $track_user_table where ( user_id = $talk_to )";
-    $result = Database::query($sql);
-    $row = Database::fetch_array($result);
-    if ($row['chatcall_text'] == 'DENIED') {
-        echo "<script language=\"javascript\" type=\"text/javascript\"> alert('".get_lang('ChatDenied')."'); </script>";
-        $sql = "update $track_user_table set chatcall_user_id = '', chatcall_date = '', chatcall_text='' WHERE (user_id = $talk_to)";
-        $result = Database::query($sql);
-    }
-}
-require 'footer_frame.inc.php';

+ 0 - 128
main/chat/chat_message.php

@@ -1,128 +0,0 @@
-<?php
-/* For licensing terms, see /license.txt */
-
-/**
- *	Allows to type the messages that will be displayed on chat_chat.php
- *
- *	@author Olivier Brouckaert
- * 	Modified by Alex Aragón (BeezNest)
- *	@package chamilo.chat
- */
-define('FRAME', 'message');
-
-require_once '../inc/global.inc.php';
-require_once api_get_path(SYS_CODE_PATH).'chat/chat_functions.lib.php';
-
-$userId = api_get_user_id();
-$userInfo = api_get_user_info();
-$course = api_get_course_id();
-$session_id = api_get_session_id();
-$group_id = api_get_group_id();
-$_course = api_get_course_info();
-
-// Juan Carlos Raña inserted smileys and self-closing window.
-?>
-<script>
-function close_chat_window() {
-    var chat_window = top.window.self;
-    chat_window.opener = top.window.self;
-    chat_window.top.close();
-}
-</script>
-<?php
-
-// Mode open in a new window: close the window when there isn't an user login
-if (empty($userId)) {
-    echo '<script languaje="javascript" type="text/javascript"> close_chat_window(); </script>';
-} else {
-    api_protect_course_script();
-}
-
-if (empty($course) || empty($userId)) {
-    exit;
-}
-
-/*	Constants and variables */
-$tbl_user = Database::get_main_table(TABLE_MAIN_USER);
-$sent = isset($_REQUEST['sent']) ? $_REQUEST['sent'] : null;
-
-require 'header_frame.inc.php';
-$chat_size = 0;
-
-if ($sent) {
-    saveMessage(
-        $_POST['message'],
-        $userId,
-        $_course,
-        $session_id,
-        $group_id,
-        false
-    );
-}
-?>
-<form
-    id="formMessage"
-    name="formMessage"
-    method="post"
-    action="<?php echo api_get_self().'?'.api_get_cidreq(); ?>" onsubmit="javascript: if(document.formMessage.message.value == '') { alert('<?php echo addslashes(api_htmlentities(get_lang('TypeMessage'), ENT_QUOTES)); ?>'); document.formMessage.message.focus(); return false; }"
-    autocomplete="off"
->
-    <input type="hidden" name="sent" value="1">
-    <div class="message-form-chat">
-        <div class="tabbable">
-            <ul class="nav nav-tabs">
-                <li class="active">
-                    <a href="#tab1" data-toggle="tab">
-                        <?php echo get_lang('Write'); ?>
-                    </a>
-                </li>
-                <li>
-                    <a href="#tab2" id="preview" data-toggle="tab">
-                        <?php echo get_lang('Preview'); ?>
-                    </a>
-                </li>
-                <li>
-                    <a href="#tab3" id="emojis" data-toggle="tab">
-                        <?php echo Emojione\Emojione::toImage(':smile:'); ?>
-                    </a>
-                </li>
-            </ul>
-            <div class="tab-content">
-                <div class="tab-pane active" id="tab1">
-                    <table border="0" cellpadding="5" cellspacing="0" width="100%">
-                        <tr>
-                            <td width="320" valign="middle">
-                                <?php
-                                $talkboxsize = (api_get_course_setting('allow_open_chat_window')) ? 'width: 350px; height: 80px' : 'width: 450px; height: 35px';
-                                ?>
-                                <textarea id="message" class="message-text" name="message" style=" <?php echo $talkboxsize; ?>"></textarea>
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>
-                                <div class="btn-group">
-                                    <button id="send" type="submit" value="<?php echo get_lang('Send'); ?>" class="btn btn-primary">
-                                        <?php echo get_lang('Send'); ?>
-                                    </button>
-                                </div>
-                            </td>
-                        </tr>
-                    </table>
-                </div>
-                <div class="tab-pane" id="tab2">
-                    <table border="0" cellpadding="5" cellspacing="0" width="100%">
-                        <tr>
-                            <td width="320" valign="middle">
-                                <div id="html-preview" class="emoji-wysiwyg-editor-preview">
-                                </div>
-                            </td>
-                        </tr>
-                    </table>
-                </div>
-            </div>
-        </div>
-    </div>
-    </form>
-<?php
-
-require 'footer_frame.inc.php';

+ 0 - 162
main/chat/chat_whoisonline.php

@@ -1,162 +0,0 @@
-<?php
-/* For licensing terms, see /license.txt */
-/**
- *	Shows the list of connected users
- *
- *	@author Olivier Brouckaert
- *	@package chamilo.chat
- */
-
-define('FRAME', 'online');
-
-require_once '../inc/global.inc.php';
-
-$course = api_get_course_id();
-$courseInfo = api_get_course_info();
-$group_id = api_get_group_id();
-$session_id = api_get_session_id();
-$user_id = api_get_user_id();
-$group_condition = " AND to_group_id = '$group_id'";
-
-$extra_condition = '';
-if (!empty($group_id)) {
-    $extra_condition = $group_condition;
-}
-
-if (!empty($course)) {
-    $showPic = isset($_GET['showPic']) ? intval($_GET['showPic']) : null;
-    $tbl_course_user = Database::get_main_table(TABLE_MAIN_COURSE_USER);
-    $tbl_session_course_user = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
-    $tbl_session = Database::get_main_table(TABLE_MAIN_SESSION);
-    $tbl_session_course = Database::get_main_table(TABLE_MAIN_SESSION_COURSE);
-    $tbl_user = Database::get_main_table(TABLE_MAIN_USER);
-    $tbl_chat_connected = Database::get_course_table(TABLE_CHAT_CONNECTED);
-
-    $query = "SELECT username FROM $tbl_user WHERE user_id='".$user_id."'";
-    $result = Database::query($query);
-
-    list($pseudo_user) = Database::fetch_array($result);
-
-	$isAllowed = !(empty($pseudo_user) || !$_cid);
-	$isMaster = api_is_course_admin();
-    $date_inter = api_get_utc_datetime(time() - 120);
-
-    $users = array();
-    $course_id = api_get_course_int_id();
-
-    if (empty($session_id)) {
-		$sql = "SELECT DISTINCT
-		            t1.user_id,
-		            username,
-		            firstname,
-		            lastname,
-		            picture_uri,
-		            email,
-		            t3.status
-                FROM $tbl_user t1, $tbl_chat_connected t2, $tbl_course_user t3
-                WHERE
-                    t2.c_id = $course_id AND
-                    t1.user_id=t2.user_id AND
-                    t3.user_id=t2.user_id AND
-                    t3.relation_type<>".COURSE_RELATION_TYPE_RRHH." AND
-                    t3.c_id = '".$courseInfo['real_id']."' AND
-                    t2.last_connection>'".$date_inter."' $extra_condition
-                ORDER BY username";
-
-        $result = Database::query($sql);
-        $users = Database::store_result($result);
-	} else {
-        $session_condition = api_get_session_condition($session_id, true, false, 't3.session_id');
-		// select learners
-		$query = "SELECT DISTINCT t1.user_id,username,firstname,lastname,picture_uri,email
-                  FROM $tbl_user t1, $tbl_chat_connected t2, $tbl_session_course_user t3
-		          WHERE
-		          t2.c_id = $course_id AND
-		          t1.user_id=t2.user_id AND t3.user_id=t2.user_id AND
-		          t3.session_id = '".$session_id."' AND
-		          t3.c_id = '".$courseInfo['real_id']."' AND
-		          t2.last_connection>'".$date_inter."' $session_condition
-		          ORDER BY username";
-		$result = Database::query($query);
-		while ($learner = Database::fetch_array($result)) {
-            $users[$learner['user_id']] = $learner;
-		}
-
-		// select session coach
-		$query = "SELECT DISTINCT t1.user_id,username,firstname,lastname,picture_uri,email
-		          FROM $tbl_user t1,$tbl_chat_connected t2,$tbl_session t3
-		          WHERE
-					t2.c_id = $course_id AND
-					t1.user_id=t2.user_id AND
-					t3.id_coach=t2.user_id AND
-					t3.id = '".$session_id."' AND
-					t2.last_connection > '".$date_inter."'
-					$extra_condition
-				  ORDER BY username";
-		$result = Database::query($query);
-		if ($coach = Database::fetch_array($result)) {
-			$users[$coach['user_id']] = $coach;
-		}
-
-		// select session course coach
-		$query = "SELECT DISTINCT t1.user_id,username,firstname,lastname,picture_uri,email
-				FROM $tbl_user t1,$tbl_chat_connected t2,$tbl_session_course_user t3
-				WHERE
-					t2.c_id = $course_id AND
-					t1.user_id=t2.user_id
-					AND t3.user_id =t2.user_id AND t3.status=2
-					AND t3.session_id = '".$session_id."'
-					AND t3.c_id = '".$course_id."'
-					AND t2.last_connection>'".$date_inter."' $extra_condition
-				ORDER BY username";
-
-		$result = Database::query($query);
-		$course_coaches = array();
-		while ($coaches = Database::fetch_array($result)) {
-			$users[$coaches['user_id']] = $coaches;
-		}
-	}
-	require 'header_frame.inc.php';
-
-	?>
-	<div class="user-connected">
-	<div id="user-online-scroll" class="user-online">
-		<div class="title"><?php echo get_lang('Users'); ?> <?php echo get_lang('Connected'); ?></div>
-		<div class="scrollbar"><div class="track"><div class="thumb"><div class="end"></div></div></div></div>
-		<div class="scrollbar-inner viewport"><div id="hidden" class="overview">
-		<ul class="profile list-group">
-			<?php
-            foreach ($users as & $user) {
-                if (empty($session_id)) {
-                    $status = $user['status'];
-                } else {
-                    $status = CourseManager::is_course_teacher($user['user_id'], api_get_course_id()) ? 1 : 5;
-                }
-                $fileUrl = UserManager::getUserPicture($user['user_id'], USER_IMAGE_SIZE_MEDIUM);
-                $url_user_profile=api_get_path(WEB_CODE_PATH).'social/profile.php?u='.$user['user_id'].'&';
-			?>
-			<li class="list-group-item">
-				<img src="<?php echo $fileUrl;?>" border="0" width="50" alt="" class="user-image-chat" />
-				<div class="user-name">
-					<a href="<?php echo $url_user_profile; ?>" target="_blank">
-                        <?php echo api_get_person_name($user['firstname'], $user['lastname']); ?>
-                    </a>
-					<?php
-                        if ($status == 1) {
-                            echo Display::return_icon('teachers.gif', get_lang('Teacher'), array('height' => '18'));
-                        } else {
-                            echo Display::return_icon('students.gif', get_lang('Student'), array('height' => '18'));
-                        }
-					?>
-				</div>
-				<div class="user-email"><?php echo $user['username']; ?></div>
-			</li>
-			<?php
-            }
-            unset($users);
-        ?>
-		</ul>
-	</div></div></div></div>
-	<?php
-}
-require 'footer_frame.inc.php';

+ 0 - 17
main/chat/footer_frame.inc.php

@@ -1,17 +0,0 @@
-<?php
-/* For licensing terms, see /license.txt */
-
-/**
- *	Footer of each frame of the Chat tool
- *
- *	@author Olivier Brouckaert
- *	@package chamilo.chat
- */
-?>
-<audio id="audio">
-	<source src="<?php echo api_get_path(WEB_PATH);?>main/chat/sound/notification.wav" type="audio/wav"></source>
-	<source src="<?php echo api_get_path(WEB_PATH);?>main/chat/sound/notification.ogg" type="audio/ogg"></source>
-	<source src="<?php echo api_get_path(WEB_PATH);?>main/chat/sound/notification.mp3" type="audio/mpeg"></source>
-</audio>
-</body>
-</html>

File diff suppressed because it is too large
+ 0 - 147
main/chat/header_frame.inc.php


+ 2 - 10
main/inc/ajax/chat.ajax.php

@@ -17,16 +17,8 @@ if (api_is_anonymous()) {
 
 // Course Chat
 if ($action == 'preview') {
-    require_once api_get_path(SYS_CODE_PATH).'chat/chat_functions.lib.php';
-
-    echo saveMessage(
-        $_REQUEST['message'],
-        api_get_user_id(),
-        api_get_course_info(),
-        api_get_session_id(),
-        api_get_group_id(),
-        true
-    );
+    echo CourseChatUtils::prepareMessage($_REQUEST['message']);
+    exit;
 }
 
 if (api_get_setting('allow_global_chat') == 'false') {

+ 76 - 0
main/inc/ajax/course_chat.ajax.php

@@ -0,0 +1,76 @@
+<?php
+/* For licensing terms, see /license.txt */
+/**
+ * Responses to AJAX calls for course chat
+ */
+
+require_once '../global.inc.php';
+
+if (!api_protect_course_script(false)) {
+    exit;
+}
+
+$courseId = api_get_course_int_id();
+$userId = api_get_user_id();
+$sessionId = api_get_session_id();
+$groupId = api_get_group_id();
+$json = ['status' => false];
+
+$courseChatUtils = new CourseChatUtils($courseId, $userId, $sessionId, $groupId);
+
+switch ($_REQUEST['action']) {
+    case 'track':
+        $courseChatUtils->keepUserAsConnected();
+        $courseChatUtils->disconnectInactiveUsers();
+
+        $friend = isset($_REQUEST['friend']) ? intval($_REQUEST['friend']) : 0;
+        $filePath = $courseChatUtils->getFileName(true, $friend);
+        $newFileSize = file_exists($filePath) ? filesize($filePath) : 0;
+        $oldFileSize = isset($_GET['size']) ? intval($_GET['size']) : -1;
+        $newUsersOnline = $courseChatUtils->countUsersOnline();
+        $oldUsersOnline = isset($_GET['users_online']) ? intval($_GET['users_online']) : 0;
+
+        $json = [
+            'status' => true,
+            'data' => [
+                'chatIsDenied' => $courseChatUtils->isChatDenied(),
+                'oldFileSize' => file_exists($filePath) ? filesize($filePath) : 0,
+                'history' => $newFileSize !== $oldFileSize ? $courseChatUtils->readMessages(false, $friend) : null,
+                'usersOnline' => $newUsersOnline,
+                'userList' => $newUsersOnline != $oldUsersOnline ? $courseChatUtils->listUsersOnline() : null,
+                'currentFriend' => $friend
+            ]
+        ];
+
+        break;
+    case 'preview':
+        $json = [
+            'status' => true,
+            'data' => [
+                'message' => CourseChatUtils::prepareMessage($_REQUEST['message'])
+            ]
+        ];
+        break;
+    case 'reset':
+        $friend = isset($_REQUEST['friend']) ? intval($_REQUEST['friend']) : 0;
+    
+        $json = [
+            'status' => true,
+            'data' => $courseChatUtils->readMessages(true, $friend)
+        ];
+        break;
+    case 'write':
+        $friend = isset($_REQUEST['friend']) ? intval($_REQUEST['friend']) : 0;
+        $writed = $courseChatUtils->saveMessage($_POST['message'], $friend);
+
+        $json = [
+            'status' => $writed,
+            'data' => [
+                'writed' => $writed
+            ]
+        ];
+        break;
+}
+
+header('Content-Type: application/json');
+echo json_encode($json);

+ 1843 - 0
main/inc/lib/CourseChatUtils.php

@@ -0,0 +1,1843 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+use Michelf\MarkdownExtra;
+
+/**
+ * Class CourseChat
+ * Manage the chat for a course
+ */
+class CourseChatUtils
+{
+    private $groupId;
+    private $courseId;
+    private $sessionId;
+    private $userId;
+
+    /**
+     * CourseChat constructor.
+     * @param int $courseId
+     * @param int $userId
+     * @param int $sessionId
+     * @param int $groupId
+     */
+    public function __construct($courseId, $userId, $sessionId = 0, $groupId = 0)
+    {
+        $this->courseId = intval($courseId);
+        $this->userId = intval($userId);
+        $this->sessionId = intval($sessionId);
+        $this->groupId = intval($groupId);
+    }
+
+    /**
+     * Get the users subscriptions (SessionRelCourseRelUser array or CourseRelUser array) for chat
+     * @return \Doctrine\Common\Collections\ArrayCollection
+     * @throws \Doctrine\ORM\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     * @throws \Doctrine\ORM\TransactionRequiredException
+     */
+    private function getUsersSubscriptions()
+    {
+        $em = Database::getManager();
+        $course = $em->find('ChamiloCoreBundle:Course', $this->courseId);
+
+        if ($this->sessionId) {
+            $criteria = \Doctrine\Common\Collections\Criteria::create()
+                ->where(
+                    \Doctrine\Common\Collections\Criteria::expr()->eq("course", $course)
+                );
+
+            return $em
+                ->find('ChamiloCoreBundle:Session', $this->sessionId)
+                ->getUserCourseSubscriptions()
+                ->matching($criteria);
+        }
+
+        return $em
+            ->find('ChamiloCoreBundle:Course', $course)
+            ->getUsers();
+    }
+
+    /**
+     * Prepare a message. Clean and insert emojis
+     * @param string $message The message to prepare
+     * @return string
+     */
+    public static function prepareMessage($message)
+    {
+        if (empty($message)) {
+            return '';
+        }
+
+        Emojione\Emojione::$imagePathPNG = api_get_path(WEB_LIBRARY_PATH) . 'javascript/emojione/png/';
+        Emojione\Emojione::$ascii = true;
+
+        $message = trim($message);
+        $message = nl2br($message);
+        // Security XSS
+        $message = Security::remove_XSS($message);
+        //search urls
+        $message = preg_replace(
+            '@((https?://)?([-\w]+\.[-\w\.]+)+\w(:\d+)?(/([-\w/_\.]*(\?\S+)?)?)*)@',
+            '<a href="$1" target="_blank">$1</a>',
+            $message
+        );
+        // add "http://" if not set
+        $message = preg_replace(
+            '/<a\s[^>]*href\s*=\s*"((?!https?:\/\/)[^"]*)"[^>]*>/i',
+            '<a href="http://$1" target="_blank">',
+            $message
+        );
+        // Parsing emojis
+        $message = Emojione\Emojione::toImage($message);
+        // Parsing text to understand markdown (code highlight)
+        $message = MarkdownExtra::defaultTransform($message);
+
+        return $message;
+    }
+
+    /**
+     * Save a chat message in a HTML file
+     * @param string$message
+     * @param int $friendId
+     * @return bool
+     * @throws \Doctrine\ORM\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     * @throws \Doctrine\ORM\TransactionRequiredException
+     */
+    public function saveMessage($message, $friendId = 0)
+    {
+        if (empty($message)) {
+            return false;
+        }
+
+        $em = Database::getManager();
+        $user = $em->find('ChamiloUserBundle:User', $this->userId);
+        $courseInfo = api_get_course_info_by_id($this->courseId);
+        $isMaster = (bool) api_is_course_admin();
+        $document_path = api_get_path(SYS_COURSE_PATH) . $courseInfo['path'] . '/document';
+        $basepath_chat = '/chat_files';
+
+        if (!$this->groupId) {
+            $group_info = GroupManager::get_group_properties($this->groupId);
+            $basepath_chat = $group_info['directory'] . '/chat_files';
+        }
+
+        $chat_path = $document_path . $basepath_chat . '/';
+
+        if (!is_dir($chat_path)) {
+            if (is_file($chat_path)) {
+                @unlink($chat_path);
+            }
+        }
+
+        $date_now = date('Y-m-d');
+        $timeNow = date('d/m/y H:i:s');
+        $basename_chat = 'messages-' . $date_now;
+
+        if ($this->groupId && !$friendId) {
+            $basename_chat = 'messages-' . $date_now . '_gid-' . $this->groupId;
+        } elseif ($this->sessionId && !$friendId) {
+            $basename_chat = 'messages-' . $date_now . '_sid-' . $this->sessionId;
+        } elseif ($friendId) {
+            if ($this->userId < $friendId) {
+                $basename_chat = 'messages-' . $date_now . '_uid-' . $this->userId . '-' . $friendId;
+            } else {
+                $basename_chat = 'messages-' . $date_now . '_uid-' . $friendId . '-' . $this->userId;
+            }
+        }
+
+        $message = self::prepareMessage($message);
+
+        $fileTitle = $basename_chat . '.log.html';
+        $filePath = $basepath_chat . '/' . $fileTitle;
+        $absoluteFilePath = $chat_path . $fileTitle;
+
+        if (!file_exists($absoluteFilePath)) {
+            $doc_id = add_document($courseInfo, $filePath, 'file', 0, $fileTitle);
+            $documentLogTypes = ['DocumentAdded', 'invisible'];
+
+            foreach ($documentLogTypes as $logType) {
+                api_item_property_update(
+                    $courseInfo,
+                    TOOL_DOCUMENT,
+                    $doc_id,
+                    $logType,
+                    $this->userId,
+                    $this->groupId,
+                    null,
+                    null,
+                    null,
+                    $this->sessionId
+                );
+            }
+
+            item_property_update_on_folder($courseInfo, $basepath_chat, $this->userId);
+        } else {
+            $doc_id = DocumentManager::get_document_id($courseInfo, $filePath);
+        }
+
+        $fp = fopen($absoluteFilePath, 'a');
+        $userPhoto = UserManager::getUserPicture($this->userId, USER_IMAGE_SIZE_MEDIUM);
+
+        if ($isMaster) {
+            $fileContent = '
+                <div class="message-teacher">
+                    <div class="content-message">
+                        <div class="chat-message-block-name">' . $user->getCompleteName() . '</div>
+                        <div class="chat-message-block-content">' . $message . '</div>
+                        <div class="message-date">' . $timeNow . '</div>
+                    </div>
+                    <div class="icon-message"></div>
+                    <img class="chat-image" src="' . $userPhoto . '">
+                </div>
+            ';
+        } else {
+            $fileContent = '
+                <div class="message-student">
+                    <img class="chat-image" src="' . $userPhoto . '">
+                    <div class="icon-message"></div>
+                    <div class="content-message">
+                        <div class="chat-message-block-name">' . $user->getCompleteName() . '</div>
+                        <div class="chat-message-block-content">' . $message . '</div>
+                        <div class="message-date">' . $timeNow . '</div>
+                    </div>
+                </div>
+            ';
+        }
+
+        fputs($fp, $fileContent);
+        fclose($fp);
+
+        $chat_size = filesize($absoluteFilePath);
+
+        update_existing_document($courseInfo, $doc_id, $chat_size);
+        item_property_update_on_folder($courseInfo, $basepath_chat, $this->userId);
+
+        return true;
+    }
+
+    /**
+     * Disconnect a user from course chats
+     * @param $userId
+     */
+    public static function exitChat($userId)
+    {
+        $listCourse = CourseManager::get_courses_list_by_user_id($userId);
+
+        foreach ($listCourse as $course) {
+            Database::getManager()
+                ->createQuery('
+                    DELETE FROM ChamiloCourseBundle:CChatConnected ccc
+                    WHERE ccc.cId = :course AND ccc.userId = :user
+                ')
+                ->execute([
+                    'course' => intval($course['real_id']),
+                    'user' => intval($userId)
+                ]);
+        }
+    }
+
+    /**
+     * Disconnect users who are more than 5 seconds inactive
+     */
+    public function disconnectInactiveUsers()
+    {
+        $em = Database::getManager();
+        $extraCondition = "AND ccc.toGroupId = {$this->groupId}";
+
+        if (empty($this->groupId)) {
+            $extraCondition = "AND ccc.sessionId = {$this->sessionId}";
+        }
+
+        $connectedUsers = $em
+            ->createQuery("
+                SELECT ccc FROM ChamiloCourseBundle:CChatConnected ccc
+                WHERE ccc.cId = :course $extraCondition
+            ")
+            ->setParameter('course', $this->courseId)
+            ->getResult();
+
+        $now = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
+        $cd_count_time_seconds = $now->getTimestamp();
+
+        foreach ($connectedUsers as $connection) {
+            $date_count_time_seconds = $connection->getLastConnection()->getTimestamp();
+
+            if (strcmp($now->format('Y-m-d'), $connection->getLastConnection()->format('Y-m-d')) !== 0) {
+                continue;
+            }
+
+            if (($cd_count_time_seconds - $date_count_time_seconds) <= 5) {
+                continue;
+            }
+
+            $em
+                ->createQuery('
+                    DELETE FROM ChamiloCourseBundle:CChatConnected ccc
+                    WHERE ccc.cId = :course AND ccc.userId = :user AND ccc.toGroupId = :group
+                ')
+                ->execute([
+                    'course' => $this->courseId,
+                    'user' => $connection->getUserId(),
+                    'group' => $this->groupId
+                ]);
+        }
+    }
+
+    /**
+     * Keep registered to a user as connected
+     * @throws \Doctrine\ORM\NonUniqueResultException
+     */
+    public function keepUserAsConnected()
+    {
+        $em = Database::getManager();
+        $extraCondition = null;
+
+        if ($this->groupId) {
+            $extraCondition = 'AND ccc.toGroupId = ' . intval($this->groupId);
+        } else {
+            $extraCondition = 'AND ccc.sessionId = ' . intval($this->sessionId);
+        }
+
+        $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
+
+        $connection = $em
+            ->createQuery("
+                SELECT ccc FROM ChamiloCourseBundle:CChatConnected ccc
+                WHERE ccc.userId = :user AND ccc.cId = :course $extraCondition 
+            ")
+            ->setParameters([
+                'user' => $this->userId,
+                'course' => $this->courseId
+            ])
+            ->getOneOrNullResult();
+
+        if ($connection) {
+            $connection->setLastConnection($currentTime);
+            $em->merge($connection);
+            $em->flush();
+
+            return;
+        }
+
+        $connection = new \Chamilo\CourseBundle\Entity\CChatConnected();
+        $connection
+            ->setCId($this->courseId)
+            ->setUserId($this->userId)
+            ->setLastConnection($currentTime)
+            ->setSessionId($this->sessionId)
+            ->setToGroupId($this->groupId);
+
+        $em->persist($connection);
+        $em->flush();
+    }
+
+    /**
+     * Check if the connection is denied for chat
+     * @return bool
+     * @throws \Doctrine\ORM\ORMException
+     * @throws \Doctrine\ORM\OptimisticLockException
+     * @throws \Doctrine\ORM\TransactionRequiredException
+     */
+    public function isChatDenied()
+    {
+        if (ChamiloSession::read('origin', null) !== 'whoisonline') {
+            return false;
+        }
+
+        $talkTo = ChamiloSession::read('target', 0);
+
+        if (!$talkTo) {
+            return true;
+        }
+
+        $em = Database::getManager();
+        $user = $em->find('ChamiloUserBundle:User', $talkTo);
+
+        if ($user->getChatcallText() === 'DENIED') {
+            $user
+                ->setChatcallDate(null)
+                ->setChatcallUserId(null)
+                ->setChatcallText(null);
+
+            $em->merge($user);
+            $em->flush();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the emoji allowed on course chat
+     * @return array
+     */
+    public static function getEmojiStrategry()
+    {
+        return [
+            'hearts' => ['unicode' => '2665', 'shortname' => ':hearts:', 'aliases' => '', 'keywords' => 'black heart suit cards poker'],
+            'joy' => ['unicode' => '1F602', 'shortname' => ':joy:', 'aliases' => '', 'keywords' => 'face with tears of joy cry face haha happy tears tears cry joy happy weep'],
+            'unamused' => ['unicode' => '1F612', 'shortname' => ':unamused:', 'aliases' => '', 'keywords' => 'unamused face bored face indifference serious straight face unamused not amused depressed unhappy disapprove lame'],
+            'heart_eyes' => ['unicode' => '1F60D', 'shortname' => ':heart_eyes:', 'aliases' => '', 'keywords' => 'smiling face with heart-shaped eyes affection crush face infatuation like love valentines smiling heart lovestruck love flirt smile heart-shaped'],
+            'heart' => ['unicode' => '2764', 'shortname' => ':heart:', 'aliases' => '', 'keywords' => 'heavy black heart like love red pink black heart love passion romance intense desire death evil cold valentines'],
+            'relaxed' => ['unicode' => '263A', 'shortname' => ':relaxed:', 'aliases' => '', 'keywords' => 'white smiling face blush face happiness massage smile'],
+            'ok_hand' => ['unicode' => '1F44C', 'shortname' => ':ok_hand:', 'aliases' => '', 'keywords' => 'ok hand sign fingers limbs perfect okay ok smoke smoking marijuana joint pot 420'],
+            'kissing_heart' => ['unicode' => '1F618', 'shortname' => ':kissing_heart:', 'aliases' => '', 'keywords' => 'face throwing a kiss affection face infatuation kiss blowing kiss heart love lips like love valentines'],
+            'blush' => ['unicode' => '1F60A', 'shortname' => ':blush:', 'aliases' => '', 'keywords' => 'smiling face with smiling eyes crush embarrassed face flushed happy shy smile smiling smile smiley'],
+            'weary' => ['unicode' => '1F629', 'shortname' => ':weary:', 'aliases' => '', 'keywords' => 'weary face face frustrated sad sleepy tired weary sleepy tired tiredness study finals school exhausted'],
+            'pensive' => ['unicode' => '1F614', 'shortname' => ':pensive:', 'aliases' => '', 'keywords' => 'pensive face face okay sad pensive thoughtful think reflective wistful meditate serious'],
+            'sob' => ['unicode' => '1F62D', 'shortname' => ':sob:', 'aliases' => '', 'keywords' => 'loudly crying face cry face sad tears upset cry sob tears sad melancholy morn somber hurt'],
+            'smirk' => ['unicode' => '1F60F', 'shortname' => ':smirk:', 'aliases' => '', 'keywords' => 'smirking face mean prank smile smug smirking smirk smug smile half-smile conceited'],
+            'two_hearts' => ['unicode' => '1F495', 'shortname' => ':two_hearts:', 'aliases' => '', 'keywords' => 'two hearts affection like love valentines heart hearts two love emotion'],
+            'grin' => ['unicode' => '1F601', 'shortname' => ':grin:', 'aliases' => '', 'keywords' => 'grinning face with smiling eyes face happy joy smile grin grinning smiling smile smiley'],
+            'flushed' => ['unicode' => '1F633', 'shortname' => ':flushed:', 'aliases' => '', 'keywords' => 'flushed face blush face flattered flush blush red pink cheeks shy'],
+            'thumbsup' => ['unicode' => '1F44D', 'shortname' => ':thumbsup:', 'aliases' => ':+1:', 'keywords' => 'thumbs up sign cool hand like yes'],
+            'raised_hands' => ['unicode' => '1F64C', 'shortname' => ':raised_hands:', 'aliases' => '', 'keywords' => 'person raising both hands in celebration gesture hooray winning woot yay banzai'],
+            'wink' => ['unicode' => '1F609', 'shortname' => ':wink:', 'aliases' => '', 'keywords' => 'winking face face happy mischievous secret wink winking friendly joke'],
+            'information_desk_person' => ['unicode' => '1F481', 'shortname' => ':information_desk_person:', 'aliases' => '', 'keywords' => 'information desk person female girl human woman information help question answer sassy unimpressed attitude snarky'],
+            'relieved' => ['unicode' => '1F60C', 'shortname' => ':relieved:', 'aliases' => '', 'keywords' => 'relieved face face happiness massage phew relaxed relieved satisfied phew relief'],
+            'see_no_evil' => ['unicode' => '1F648', 'shortname' => ':see_no_evil:', 'aliases' => '', 'keywords' => 'see-no-evil monkey animal monkey nature monkey see eyes vision sight mizaru'],
+            'v' => ['unicode' => '270C', 'shortname' => ':v:', 'aliases' => '', 'keywords' => 'victory hand fingers hand ohyeah peace two victory'],
+            'pray' => ['unicode' => '1F64F', 'shortname' => ':pray:', 'aliases' => '', 'keywords' => 'person with folded hands highfive hope namaste please wish pray high five hands sorrow regret sorry'],
+            'yum' => ['unicode' => '1F60B', 'shortname' => ':yum:', 'aliases' => '', 'keywords' => 'face savouring delicious food face happy joy smile tongue delicious savoring food eat yummy yum tasty savory'],
+            'stuck_out_tongue_winking_eye' => ['unicode' => '1F61C', 'shortname' => ':stuck_out_tongue_winking_eye:', 'aliases' => '', 'keywords' => 'face with stuck-out tongue and winking eye childish face mischievous playful prank tongue wink winking kidding silly playful crazy'],
+            'notes' => ['unicode' => '1F3B6', 'shortname' => ':notes:', 'aliases' => '', 'keywords' => 'multiple musical notes music score musical music notes music sound melody'],
+            'eyes' => ['unicode' => '1F440', 'shortname' => ':eyes:', 'aliases' => '', 'keywords' => 'eyes look peek stalk watch'],
+            'smile' => ['unicode' => '1F604', 'shortname' => ':smile:', 'aliases' => '', 'keywords' => 'smiling face with open mouth and smiling eyes face funny haha happy joy laugh smile smiley smiling'],
+            'disappointed' => ['unicode' => '1F61E', 'shortname' => ':disappointed:', 'aliases' => '', 'keywords' => 'disappointed face disappointed disappoint frown depressed discouraged face sad upset'],
+            'raised_hand' => ['unicode' => '270B', 'shortname' => ':raised_hand:', 'aliases' => '', 'keywords' => 'raised hand female girl woman'],
+            'clap' => ['unicode' => '1F44F', 'shortname' => ':clap:', 'aliases' => '', 'keywords' => 'clapping hands sign applause congrats hands praise clapping appreciation approval sound encouragement enthusiasm'],
+            'speak_no_evil' => ['unicode' => '1F64A', 'shortname' => ':speak_no_evil:', 'aliases' => '', 'keywords' => 'speak-no-evil monkey animal monkey monkey mouth talk say words verbal verbalize oral iwazaru'],
+            100 => ['unicode' => '1F4AF', 'shortname' => ':100:', 'aliases' => '', 'keywords' => 'hundred points symbol numbers perfect score 100 percent a plus perfect school quiz score test exam'],
+            'cry' => ['unicode' => '1F622', 'shortname' => ':cry:', 'aliases' => '', 'keywords' => 'crying face face sad sad cry tear weep tears'],
+            'rage' => ['unicode' => '1F621', 'shortname' => ':rage:', 'aliases' => '', 'keywords' => 'pouting face angry despise hate mad pout anger rage irate'],
+            'tired_face' => ['unicode' => '1F62B', 'shortname' => ':tired_face:', 'aliases' => '', 'keywords' => 'tired face face frustrated sick upset whine exhausted sleepy tired'],
+            'scream' => ['unicode' => '1F631', 'shortname' => ':scream:', 'aliases' => '', 'keywords' => 'face screaming in fear face munch scream painting artist alien'],
+            'purple_heart' => ['unicode' => '1F49C', 'shortname' => ':purple_heart:', 'aliases' => '', 'keywords' => 'purple heart affection like love valentines purple violet heart love sensitive understanding compassionate compassion duty honor royalty veteran sacrifice'],
+            'broken_heart' => ['unicode' => '1F494', 'shortname' => ':broken_heart:', 'aliases' => '', 'keywords' => 'broken heart sad sorry'],
+            'kiss' => ['unicode' => '1F48B', 'shortname' => ':kiss:', 'aliases' => '', 'keywords' => 'kiss mark affection face like lips love valentines'],
+            'blue_heart' => ['unicode' => '1F499', 'shortname' => ':blue_heart:', 'aliases' => '', 'keywords' => 'blue heart affection like love valentines blue heart love stability truth loyalty trust'],
+            'sleepy' => ['unicode' => '1F62A', 'shortname' => ':sleepy:', 'aliases' => '', 'keywords' => 'sleepy face face rest tired sleepy tired exhausted'],
+            'sweat_smile' => ['unicode' => '1F605', 'shortname' => ':sweat_smile:', 'aliases' => '', 'keywords' => 'smiling face with open mouth and cold sweat face happy hot smiling cold sweat perspiration'],
+            'stuck_out_tongue_closed_eyes' => ['unicode' => '1F61D', 'shortname' => ':stuck_out_tongue_closed_eyes:', 'aliases' => '', 'keywords' => 'face with stuck-out tongue and tightly-closed eyes face mischievous playful prank tongue kidding silly playful ecstatic'],
+            'punch' => ['unicode' => '1F44A', 'shortname' => ':punch:', 'aliases' => '', 'keywords' => 'fisted hand sign fist hand'],
+            'triumph' => ['unicode' => '1F624', 'shortname' => ':triumph:', 'aliases' => '', 'keywords' => 'face with look of triumph face gas phew triumph steam breath'],
+            'sparkling_heart' => ['unicode' => '1F496', 'shortname' => ':sparkling_heart:', 'aliases' => '', 'keywords' => 'sparkling heart affection like love valentines'],
+            'smiley' => ['unicode' => '1F603', 'shortname' => ':smiley:', 'aliases' => '', 'keywords' => 'smiling face with open mouth face haha happy joy smiling smile smiley'],
+            'sunny' => ['unicode' => '2600', 'shortname' => ':sunny:', 'aliases' => '', 'keywords' => 'black sun with rays brightness weather'],
+            'heartpulse' => ['unicode' => '1F497', 'shortname' => ':heartpulse:', 'aliases' => '', 'keywords' => 'growing heart affection like love valentines'],
+            'wave' => ['unicode' => '1F44B', 'shortname' => ':wave:', 'aliases' => '', 'keywords' => 'waving hand sign farewell gesture goodbye hands solong'],
+            'mask' => ['unicode' => '1F637', 'shortname' => ':mask:', 'aliases' => '', 'keywords' => 'face with medical mask face ill sick sick virus flu medical mask'],
+            'heavy_check_mark' => ['unicode' => '2714', 'shortname' => ':heavy_check_mark:', 'aliases' => '', 'keywords' => 'heavy check mark nike ok'],
+            'cherry_blossom' => ['unicode' => '1F338', 'shortname' => ':cherry_blossom:', 'aliases' => '', 'keywords' => 'cherry blossom flower nature plant cherry blossom tree flower'],
+            'rose' => ['unicode' => '1F339', 'shortname' => ':rose:', 'aliases' => '', 'keywords' => 'rose flowers love valentines rose fragrant flower thorns love petals romance'],
+            'persevere' => ['unicode' => '1F623', 'shortname' => ':persevere:', 'aliases' => '', 'keywords' => 'persevering face endure persevere face no sick upset'],
+            'revolving_hearts' => ['unicode' => '1F49E', 'shortname' => ':revolving_hearts:', 'aliases' => '', 'keywords' => 'revolving hearts affection like love valentines heart hearts revolving moving circle multiple lovers'],
+            'sparkles' => ['unicode' => '2728', 'shortname' => ':sparkles:', 'aliases' => '', 'keywords' => 'sparkles cool shine shiny stars'],
+            'confounded' => ['unicode' => '1F616', 'shortname' => ':confounded:', 'aliases' => '', 'keywords' => 'confounded face confused face sick unwell confound amaze perplex puzzle mystify'],
+            'tada' => ['unicode' => '1F389', 'shortname' => ':tada:', 'aliases' => '', 'keywords' => 'party popper contulations party party popper tada celebration victory announcement climax congratulations'],
+            'no_good' => ['unicode' => '1F645', 'shortname' => ':no_good:', 'aliases' => '', 'keywords' => 'face with no good gesture female girl woman no stop nope don\'t not'],
+            'muscle' => ['unicode' => '1F4AA', 'shortname' => ':muscle:', 'aliases' => '', 'keywords' => 'flexed biceps arm flex hand strong muscle bicep'],
+            'angry' => ['unicode' => '1F620', 'shortname' => ':angry:', 'aliases' => '', 'keywords' => 'angry face angry livid mad vexed irritated annoyed face frustrated mad'],
+            'gun' => ['unicode' => '1F52B', 'shortname' => ':gun:', 'aliases' => '', 'keywords' => 'pistol violence weapon'],
+            'cupid' => ['unicode' => '1F498', 'shortname' => ':cupid:', 'aliases' => '', 'keywords' => 'heart with arrow affection heart like love valentines'],
+            'sweat' => ['unicode' => '1F613', 'shortname' => ':sweat:', 'aliases' => '', 'keywords' => 'face with cold sweat cold sweat sick anxious worried clammy diaphoresis face hot'],
+            'laughing' => ['unicode' => '1F606', 'shortname' => ':laughing:', 'aliases' => ':satisfied:', 'keywords' => 'smiling face with open mouth and tightly-closed ey happy joy lol smiling laughing laugh'],
+            'yellow_heart' => ['unicode' => '1F49B', 'shortname' => ':yellow_heart:', 'aliases' => '', 'keywords' => 'yellow heart affection like love valentines yellow gold heart love friendship happy happiness trust compassionate respectful honest caring selfless'],
+            'kissing_closed_eyes' => ['unicode' => '1F61A', 'shortname' => ':kissing_closed_eyes:', 'aliases' => '', 'keywords' => 'kissing face with closed eyes affection face infatuation like love valentines kissing kiss passion puckered heart love smooch'],
+            'disappointed_relieved' => ['unicode' => '1F625', 'shortname' => ':disappointed_relieved:', 'aliases' => '', 'keywords' => 'disappointed but relieved face face nervous phew sweat disappoint relief'],
+            'raising_hand' => ['unicode' => '1F64B', 'shortname' => ':raising_hand:', 'aliases' => '', 'keywords' => 'happy person raising one hand female girl woman hand raise notice attention answer'],
+            'fist' => ['unicode' => '270A', 'shortname' => ':fist:', 'aliases' => '', 'keywords' => 'raised fist fingers grasp hand'],
+            'green_heart' => ['unicode' => '1F49A', 'shortname' => ':green_heart:', 'aliases' => '', 'keywords' => 'green heart affection like love valentines green heart love nature rebirth reborn jealous clingy envious possessive'],
+            'headphones' => ['unicode' => '1F3A7', 'shortname' => ':headphones:', 'aliases' => '', 'keywords' => 'headphone gadgets music score headphone sound music ears beats buds audio listen'],
+            'thumbsdown' => ['unicode' => '1F44E', 'shortname' => ':thumbsdown:', 'aliases' => ':-1:', 'keywords' => 'thumbs down sign hand no'],
+            'heart_eyes_cat' => ['unicode' => '1F63B', 'shortname' => ':heart_eyes_cat:', 'aliases' => '', 'keywords' => 'smiling cat face with heart-shaped eyes affection animal cats like love valentines lovestruck love heart'],
+            'dancer' => ['unicode' => '1F483', 'shortname' => ':dancer:', 'aliases' => '', 'keywords' => 'dancer female fun girl woman dance dancer dress fancy boogy party celebrate ballet tango cha cha music'],
+            'skull' => ['unicode' => '1F480', 'shortname' => ':skull:', 'aliases' => ':skeleton:', 'keywords' => 'skull dead skeleton dying'],
+            'poop' => ['unicode' => '1F4A9', 'shortname' => ':poop:', 'aliases' => ':shit: :hankey: :poo:', 'keywords' => 'pile of poo poop shit shitface turd poo'],
+            'fire' => ['unicode' => '1F525', 'shortname' => ':fire:', 'aliases' => ':flame:', 'keywords' => 'fire cook hot flame'],
+            'walking' => ['unicode' => '1F6B6', 'shortname' => ':walking:', 'aliases' => '', 'keywords' => 'pedestrian human man walk pedestrian stroll stride foot feet'],
+            'cold_sweat' => ['unicode' => '1F630', 'shortname' => ':cold_sweat:', 'aliases' => '', 'keywords' => 'face with open mouth and cold sweat face nervous sweat exasperated frustrated'],
+            'copyright' => ['unicode' => '00A9', 'shortname' => ':copyright:', 'aliases' => '', 'keywords' => 'copyright sign ip license'],
+            'penguin' => ['unicode' => '1F427', 'shortname' => ':penguin:', 'aliases' => '', 'keywords' => 'penguin animal nature'],
+            'crown' => ['unicode' => '1F451', 'shortname' => ':crown:', 'aliases' => '', 'keywords' => 'crown king kod leader royalty'],
+            'open_hands' => ['unicode' => '1F450', 'shortname' => ':open_hands:', 'aliases' => '', 'keywords' => 'open hands sign butterfly fingers'],
+            'point_right' => ['unicode' => '1F449', 'shortname' => ':point_right:', 'aliases' => '', 'keywords' => 'white right pointing backhand index direction fingers hand'],
+            'heartbeat' => ['unicode' => '1F493', 'shortname' => ':heartbeat:', 'aliases' => '', 'keywords' => 'beating heart affection like love valentines'],
+            'dancers' => ['unicode' => '1F46F', 'shortname' => ':dancers:', 'aliases' => '', 'keywords' => 'woman with bunny ears bunny female girls women dancing dancers showgirl playboy costume bunny cancan'],
+            'ok_woman' => ['unicode' => '1F646', 'shortname' => ':ok_woman:', 'aliases' => '', 'keywords' => 'face with ok gesture female girl human pink women yes ok okay accept'],
+            'pizza' => ['unicode' => '1F355', 'shortname' => ':pizza:', 'aliases' => '', 'keywords' => 'slice of pizza food party pizza pie new york italian italy slice peperoni'],
+            'ballot_box_with_check' => ['unicode' => '2611', 'shortname' => ':ballot_box_with_check:', 'aliases' => '', 'keywords' => 'ballot box with check agree ok'],
+            'zzz' => ['unicode' => '1F4A4', 'shortname' => ':zzz:', 'aliases' => '', 'keywords' => 'sleeping symbol sleepy tired'],
+            'point_left' => ['unicode' => '1F448', 'shortname' => ':point_left:', 'aliases' => '', 'keywords' => 'white left pointing backhand index direction fingers hand'],
+            'musical_note' => ['unicode' => '1F3B5', 'shortname' => ':musical_note:', 'aliases' => '', 'keywords' => 'musical note score musical music note music sound'],
+            'bow' => ['unicode' => '1F647', 'shortname' => ':bow:', 'aliases' => '', 'keywords' => 'person bowing deeply boy male man sorry bow respect curtsy bend'],
+            'fearful' => ['unicode' => '1F628', 'shortname' => ':fearful:', 'aliases' => '', 'keywords' => 'fearful face face nervous oops scared terrified fear fearful scared frightened'],
+            'ribbon' => ['unicode' => '1F380', 'shortname' => ':ribbon:', 'aliases' => '', 'keywords' => 'ribbon bowtie decoration girl pink ribbon lace wrap decorate'],
+            'joy_cat' => ['unicode' => '1F639', 'shortname' => ':joy_cat:', 'aliases' => '', 'keywords' => 'cat face with tears of joy animal cats haha happy tears happy tears cry joy'],
+            'arrow_forward' => ['unicode' => '25B6', 'shortname' => ':arrow_forward:', 'aliases' => '', 'keywords' => 'black right-pointing triangle arrow blue-square'],
+            'tongue' => ['unicode' => '1F445', 'shortname' => ':tongue:', 'aliases' => '', 'keywords' => 'tongue mouth playful tongue mouth taste buds food silly playful tease kiss french kiss lick tasty playfulness silliness intimacy'],
+            'runner' => ['unicode' => '1F3C3', 'shortname' => ':runner:', 'aliases' => '', 'keywords' => 'runner exercise man walking run runner jog exercise sprint race dash'],
+            'point_up' => ['unicode' => '261D', 'shortname' => ':point_up:', 'aliases' => '', 'keywords' => 'white up pointing index direction fingers hand'],
+            'airplane' => ['unicode' => '2708', 'shortname' => ':airplane:', 'aliases' => '', 'keywords' => 'airplane flight transportation vehicle airplane plane airport travel airlines fly jet jumbo boeing airbus'],
+            'gem' => ['unicode' => '1F48E', 'shortname' => ':gem:', 'aliases' => '', 'keywords' => 'gem stone blue ruby'],
+            'person_frowning' => ['unicode' => '1F64D', 'shortname' => ':person_frowning:', 'aliases' => '', 'keywords' => 'person frowning female girl woman dejected rejected sad frown'],
+            'hibiscus' => ['unicode' => '1F33A', 'shortname' => ':hibiscus:', 'aliases' => '', 'keywords' => 'hibiscus flowers plant vegetable hibiscus flower warm'],
+            'basketball' => ['unicode' => '1F3C0', 'shortname' => ':basketball:', 'aliases' => '', 'keywords' => 'basketball and hoop NBA balls sports basketball bball dribble hoop net swish rip city'],
+            'boom' => ['unicode' => '1F4A5', 'shortname' => ':boom:', 'aliases' => '', 'keywords' => 'collision symbol bomb explode explosion boom bang collision fire emphasis wow bam'],
+            'nail_care' => ['unicode' => '1F485', 'shortname' => ':nail_care:', 'aliases' => '', 'keywords' => 'nail polish beauty manicure'],
+            'dizzy_face' => ['unicode' => '1F635', 'shortname' => ':dizzy_face:', 'aliases' => '', 'keywords' => 'dizzy face dizzy drunk inebriated face spent unconscious xox'],
+            'balloon' => ['unicode' => '1F388', 'shortname' => ':balloon:', 'aliases' => '', 'keywords' => 'balloon celebration party balloon birthday celebration helium gas children float'],
+            'couple' => ['unicode' => '1F46B', 'shortname' => ':couple:', 'aliases' => '', 'keywords' => 'man and woman holding hands affection date dating human like love marriage people valentines'],
+            'dog' => ['unicode' => '1F436', 'shortname' => ':dog:', 'aliases' => '', 'keywords' => 'dog face animal friend nature woof'],
+            'sweat_drops' => ['unicode' => '1F4A6', 'shortname' => ':sweat_drops:', 'aliases' => '', 'keywords' => 'splashing sweat symbol water'],
+            'star2' => ['unicode' => '1F31F', 'shortname' => ':star2:', 'aliases' => '', 'keywords' => 'glowing star night sparkle glow glowing star five points classic'],
+            'hear_no_evil' => ['unicode' => '1F649', 'shortname' => ':hear_no_evil:', 'aliases' => '', 'keywords' => 'hear-no-evil monkey animal monkey monkey ears hear sound kikazaru'],
+            'moneybag' => ['unicode' => '1F4B0', 'shortname' => ':moneybag:', 'aliases' => '', 'keywords' => 'money bag coins dollar payment'],
+            'beers' => ['unicode' => '1F37B', 'shortname' => ':beers:', 'aliases' => '', 'keywords' => 'clinking beer mugs beverage drink drunk party pub relax beer beers cheers mug toast celebrate pub bar jolly hops clink'],
+            'couplekiss' => ['unicode' => '1F48F', 'shortname' => ':couplekiss:', 'aliases' => '', 'keywords' => 'kiss dating like love marriage valentines'],
+            'point_down' => ['unicode' => '1F447', 'shortname' => ':point_down:', 'aliases' => '', 'keywords' => 'white down pointing backhand index direction fingers hand'],
+            'cloud' => ['unicode' => '2601', 'shortname' => ':cloud:', 'aliases' => '', 'keywords' => 'cloud sky weather'],
+            'alien' => ['unicode' => '1F47D', 'shortname' => ':alien:', 'aliases' => '', 'keywords' => 'extraterrestrial alien UFO paul alien ufo'],
+            'dizzy' => ['unicode' => '1F4AB', 'shortname' => ':dizzy:', 'aliases' => '', 'keywords' => 'dizzy symbol shoot sparkle star dizzy drunk sick intoxicated squeans starburst star'],
+            'heavy_multiplication_x' => ['unicode' => '2716', 'shortname' => ':heavy_multiplication_x:', 'aliases' => '', 'keywords' => 'heavy multiplication x calculation math'],
+            'white_check_mark' => ['unicode' => '2705', 'shortname' => ':white_check_mark:', 'aliases' => '', 'keywords' => 'white heavy check mark agree green-square ok'],
+            'palm_tree' => ['unicode' => '1F334', 'shortname' => ':palm_tree:', 'aliases' => '', 'keywords' => 'palm tree nature plant vegetable palm tree coconuts fronds warm tropical'],
+            'dash' => ['unicode' => '1F4A8', 'shortname' => ':dash:', 'aliases' => '', 'keywords' => 'dash symbol air fast shoo wind'],
+            'exclamation' => ['unicode' => '2757', 'shortname' => ':exclamation:', 'aliases' => '', 'keywords' => 'heavy exclamation mark symbol surprise'],
+            'soccer' => ['unicode' => '26BD', 'shortname' => ':soccer:', 'aliases' => '', 'keywords' => 'soccer ball balls fifa football sports european football'],
+            'microphone' => ['unicode' => '1F3A4', 'shortname' => ':microphone:', 'aliases' => '', 'keywords' => 'microphone PA music sound microphone mic audio sound voice karaoke'],
+            'angel' => ['unicode' => '1F47C', 'shortname' => ':angel:', 'aliases' => '', 'keywords' => 'baby angel baby angel halo cupid wings halo heaven wings jesus'],
+            'point_up_2' => ['unicode' => '1F446', 'shortname' => ':point_up_2:', 'aliases' => '', 'keywords' => 'white up pointing backhand index direction fingers hand'],
+            'snowflake' => ['unicode' => '2744', 'shortname' => ':snowflake:', 'aliases' => '', 'keywords' => 'snowflake christmas cold season weather winter xmas snowflake snow frozen droplet ice crystal cold chilly winter unique special below zero elsa'],
+            'astonished' => ['unicode' => '1F632', 'shortname' => ':astonished:', 'aliases' => '', 'keywords' => 'astonished face face xox shocked surprise astonished'],
+            'four_leaf_clover' => ['unicode' => '1F340', 'shortname' => ':four_leaf_clover:', 'aliases' => '', 'keywords' => 'four leaf clover lucky nature plant vegetable clover four leaf luck irish saint patrick green'],
+            'ghost' => ['unicode' => '1F47B', 'shortname' => ':ghost:', 'aliases' => '', 'keywords' => 'ghost halloween'],
+            'princess' => ['unicode' => '1F478', 'shortname' => ':princess:', 'aliases' => '', 'keywords' => 'princess blond crown female girl woman princess royal royalty king queen daughter disney high-maintenance'],
+            'cat' => ['unicode' => '1F431', 'shortname' => ':cat:', 'aliases' => '', 'keywords' => 'cat face animal meow'],
+            'ring' => ['unicode' => '1F48D', 'shortname' => ':ring:', 'aliases' => '', 'keywords' => 'ring marriage propose valentines wedding'],
+            'sunflower' => ['unicode' => '1F33B', 'shortname' => ':sunflower:', 'aliases' => '', 'keywords' => 'sunflower nature plant sunflower sun flower seeds yellow'],
+            'o' => ['unicode' => '2B55', 'shortname' => ':o:', 'aliases' => '', 'keywords' => 'heavy large circle circle round'],
+            'crescent_moon' => ['unicode' => '1F319', 'shortname' => ':crescent_moon:', 'aliases' => '', 'keywords' => 'crescent moon night moon crescent waxing sky night cheese phase'],
+            'gift' => ['unicode' => '1F381', 'shortname' => ':gift:', 'aliases' => '', 'keywords' => 'wrapped present birthday christmas present xmas gift present wrap package birthday wedding'],
+            'crying_cat_face' => ['unicode' => '1F63F', 'shortname' => ':crying_cat_face:', 'aliases' => '', 'keywords' => 'crying cat face animal cats sad tears weep cry cat sob tears sad melancholy morn somber hurt'],
+            'bouquet' => ['unicode' => '1F490', 'shortname' => ':bouquet:', 'aliases' => '', 'keywords' => 'bouquet flowers nature'],
+            'star' => ['unicode' => '2B50', 'shortname' => ':star:', 'aliases' => '', 'keywords' => 'white medium star night yellow'],
+            'leaves' => ['unicode' => '1F343', 'shortname' => ':leaves:', 'aliases' => '', 'keywords' => 'leaf fluttering in wind grass lawn nature plant tree vegetable leaves leaf wind float fluttering'],
+            'cactus' => ['unicode' => '1F335', 'shortname' => ':cactus:', 'aliases' => '', 'keywords' => 'cactus nature plant vegetable cactus desert drought spike poke'],
+            'clubs' => ['unicode' => '2663', 'shortname' => ':clubs:', 'aliases' => '', 'keywords' => 'black club suit cards poker'],
+            'diamonds' => ['unicode' => '2666', 'shortname' => ':diamonds:', 'aliases' => '', 'keywords' => 'black diamond suit cards poker'],
+            'massage' => ['unicode' => '1F486', 'shortname' => ':massage:', 'aliases' => '', 'keywords' => 'face massage female girl woman'],
+            'imp' => ['unicode' => '1F47F', 'shortname' => ':imp:', 'aliases' => '', 'keywords' => 'imp angry devil evil horns cute devil'],
+            'red_circle' => ['unicode' => '1F534', 'shortname' => ':red_circle:', 'aliases' => '', 'keywords' => 'large red circle shape'],
+            'money_with_wings' => ['unicode' => '1F4B8', 'shortname' => ':money_with_wings:', 'aliases' => '', 'keywords' => 'money with wings bills dollar payment money wings easy spend work lost blown burned gift cash dollar'],
+            'football' => ['unicode' => '1F3C8', 'shortname' => ':football:', 'aliases' => '', 'keywords' => 'american football NFL balls sports football ball sport america american'],
+            'cyclone' => ['unicode' => '1F300', 'shortname' => ':cyclone:', 'aliases' => '', 'keywords' => 'cyclone blue cloud swirl weather cyclone hurricane typhoon storm ocean'],
+            'smirk_cat' => ['unicode' => '1F63C', 'shortname' => ':smirk_cat:', 'aliases' => '', 'keywords' => 'cat face with wry smile animal cats smirk smirking wry confident confidence'],
+            'snowman' => ['unicode' => '26C4', 'shortname' => ':snowman:', 'aliases' => '', 'keywords' => 'snowman without snow christmas cold season weather winter xmas'],
+            'birthday' => ['unicode' => '1F382', 'shortname' => ':birthday:', 'aliases' => '', 'keywords' => 'birthday cake cake party birthday birth cake dessert wish celebrate'],
+            'baby' => ['unicode' => '1F476', 'shortname' => ':baby:', 'aliases' => '', 'keywords' => 'baby boy child infant'],
+            'telephone' => ['unicode' => '260E', 'shortname' => ':telephone:', 'aliases' => '', 'keywords' => 'black telephone communication dial technology'],
+            'eggplant' => ['unicode' => '1F346', 'shortname' => ':eggplant:', 'aliases' => '', 'keywords' => 'aubergine aubergine food nature vegetable eggplant aubergine fruit purple penis'],
+            'gift_heart' => ['unicode' => '1F49D', 'shortname' => ':gift_heart:', 'aliases' => '', 'keywords' => 'heart with ribbon love valentines'],
+            'tulip' => ['unicode' => '1F337', 'shortname' => ':tulip:', 'aliases' => '', 'keywords' => 'tulip flowers nature plant tulip flower bulb spring easter'],
+            'confetti_ball' => ['unicode' => '1F38A', 'shortname' => ':confetti_ball:', 'aliases' => '', 'keywords' => 'confetti ball festival party party congratulations confetti ball celebrate win birthday new years wedding'],
+            'black_small_square' => ['unicode' => '25AA', 'shortname' => ':black_small_square:', 'aliases' => '', 'keywords' => 'black small square '],
+            'coffee' => ['unicode' => '2615', 'shortname' => ':coffee:', 'aliases' => '', 'keywords' => 'hot beverage beverage cafe drink espresso'],
+            'scream_cat' => ['unicode' => '1F640', 'shortname' => ':scream_cat:', 'aliases' => '', 'keywords' => 'weary cat face animal cats munch weary sleepy tired tiredness study finals school exhausted scream painting artist'],
+            'rocket' => ['unicode' => '1F680', 'shortname' => ':rocket:', 'aliases' => '', 'keywords' => 'rocket launch ship staffmode rocket space spacecraft astronaut cosmonaut'],
+            'christmas_tree' => ['unicode' => '1F384', 'shortname' => ':christmas_tree:', 'aliases' => '', 'keywords' => 'christmas tree celebration december festival vacation xmas christmas xmas santa holiday winter december santa evergreen ornaments jesus gifts presents'],
+            'x' => ['unicode' => '274C', 'shortname' => ':x:', 'aliases' => '', 'keywords' => 'cross mark delete no remove'],
+            'knife' => ['unicode' => '1F52A', 'shortname' => ':knife:', 'aliases' => '', 'keywords' => 'hocho '],
+            'bangbang' => ['unicode' => '203C', 'shortname' => ':bangbang:', 'aliases' => '', 'keywords' => 'double exclamation mark exclamation surprise'],
+            'smile_cat' => ['unicode' => '1F638', 'shortname' => ':smile_cat:', 'aliases' => '', 'keywords' => 'grinning cat face with smiling eyes animal cats cat smile grin grinning'],
+            'kissing_cat' => ['unicode' => '1F63D', 'shortname' => ':kissing_cat:', 'aliases' => '', 'keywords' => 'kissing cat face with closed eyes animal cats passion kiss puckered heart love'],
+            'doughnut' => ['unicode' => '1F369', 'shortname' => ':doughnut:', 'aliases' => '', 'keywords' => 'doughnut desert food snack sweet doughnut donut pastry fried dessert breakfast police homer sweet'],
+            'couple_with_heart' => ['unicode' => '1F491', 'shortname' => ':couple_with_heart:', 'aliases' => '', 'keywords' => 'couple with heart affection dating human like love marriage valentines'],
+            'spades' => ['unicode' => '2660', 'shortname' => ':spades:', 'aliases' => '', 'keywords' => 'black spade suit cards poker'],
+            'bomb' => ['unicode' => '1F4A3', 'shortname' => ':bomb:', 'aliases' => '', 'keywords' => 'bomb boom explode'],
+            'guitar' => ['unicode' => '1F3B8', 'shortname' => ':guitar:', 'aliases' => '', 'keywords' => 'guitar instrument music guitar string music instrument jam rock acoustic electric'],
+            'space_invader' => ['unicode' => '1F47E', 'shortname' => ':space_invader:', 'aliases' => '', 'keywords' => 'alien monster arcade game'],
+            'maple_leaf' => ['unicode' => '1F341', 'shortname' => ':maple_leaf:', 'aliases' => '', 'keywords' => 'maple leaf canada nature plant vegetable maple leaf syrup canada tree'],
+            'pig' => ['unicode' => '1F437', 'shortname' => ':pig:', 'aliases' => '', 'keywords' => 'pig face animal oink'],
+            'guardsman' => ['unicode' => '1F482', 'shortname' => ':guardsman:', 'aliases' => '', 'keywords' => 'guardsman british gb male man uk guardsman guard bearskin hat british queen ceremonial military'],
+            'fork_and_knife' => ['unicode' => '1F374', 'shortname' => ':fork_and_knife:', 'aliases' => '', 'keywords' => 'fork and knife cutlery kitchen fork knife restaurant meal food eat'],
+            'lips' => ['unicode' => '1F444', 'shortname' => ':lips:', 'aliases' => '', 'keywords' => 'mouth kiss mouth'],
+            'santa' => ['unicode' => '1F385', 'shortname' => ':santa:', 'aliases' => '', 'keywords' => 'father christmas christmas father christmas festival male man xmas santa saint nick jolly ho ho ho north pole presents gifts naughty nice sleigh father christmas holiday'],
+            'beer' => ['unicode' => '1F37A', 'shortname' => ':beer:', 'aliases' => '', 'keywords' => 'beer mug beverage drink drunk party pub relax beer hops mug barley malt yeast portland oregon brewery micro pint boot'],
+            'red_car' => ['unicode' => '1F697', 'shortname' => ':red_car:', 'aliases' => '', 'keywords' => 'automobile transportation vehicle'],
+            'zap' => ['unicode' => '26A1', 'shortname' => ':zap:', 'aliases' => '', 'keywords' => 'high voltage sign lightning bolt thunder weather'],
+            'ocean' => ['unicode' => '1F30A', 'shortname' => ':ocean:', 'aliases' => '', 'keywords' => 'water wave sea water wave ocean wave surf beach tide'],
+            'banana' => ['unicode' => '1F34C', 'shortname' => ':banana:', 'aliases' => '', 'keywords' => 'banana food fruit banana peel bunch'],
+            'tm' => ['unicode' => '1F1F9-1F1F2', 'shortname' => ':tm:', 'aliases' => '', 'keywords' => 'turkmenistan country nation'],
+            'turtle' => ['unicode' => '1F422', 'shortname' => ':turtle:', 'aliases' => '', 'keywords' => 'turtle animal slow turtle shell tortoise chelonian reptile slow snap steady'],
+            'movie_camera' => ['unicode' => '1F3A5', 'shortname' => ':movie_camera:', 'aliases' => '', 'keywords' => 'movie camera film record movie camera camcorder video motion picture'],
+            'video_game' => ['unicode' => '1F3AE', 'shortname' => ':video_game:', 'aliases' => '', 'keywords' => 'video game PS4 console controller play video game console controller nintendo xbox playstation'],
+            'trophy' => ['unicode' => '1F3C6', 'shortname' => ':trophy:', 'aliases' => '', 'keywords' => 'trophy award ceremony contest ftw place win trophy first show place win reward achievement medal'],
+            'man' => ['unicode' => '1F468', 'shortname' => ':man:', 'aliases' => '', 'keywords' => 'man classy dad father guy mustashe'],
+            'umbrella' => ['unicode' => '2614', 'shortname' => ':umbrella:', 'aliases' => '', 'keywords' => 'umbrella with rain drops rain weather'],
+            'tiger' => ['unicode' => '1F42F', 'shortname' => ':tiger:', 'aliases' => '', 'keywords' => 'tiger face animal'],
+            'smoking' => ['unicode' => '1F6AC', 'shortname' => ':smoking:', 'aliases' => '', 'keywords' => 'smoking symbol cigarette kills tobacco smoking cigarette smoke cancer lungs inhale tar nicotine'],
+            'watermelon' => ['unicode' => '1F349', 'shortname' => ':watermelon:', 'aliases' => '', 'keywords' => 'watermelon food fruit melon watermelon summer fruit large'],
+            'person_with_pouting_face' => ['unicode' => '1F64E', 'shortname' => ':person_with_pouting_face:', 'aliases' => '', 'keywords' => 'person with pouting face female girl woman pout sexy cute annoyed'],
+            'herb' => ['unicode' => '1F33F', 'shortname' => ':herb:', 'aliases' => '', 'keywords' => 'herb grass lawn medicine plant vegetable weed herb spice plant cook cooking'],
+            'footprints' => ['unicode' => '1F463', 'shortname' => ':footprints:', 'aliases' => '', 'keywords' => 'footprints feet'],
+            'camera' => ['unicode' => '1F4F7', 'shortname' => ':camera:', 'aliases' => '', 'keywords' => 'camera gadgets photo'],
+            'japanese_ogre' => ['unicode' => '1F479', 'shortname' => ':japanese_ogre:', 'aliases' => '', 'keywords' => 'japanese ogre monster japanese oni demon troll ogre folklore monster devil mask theater horns teeth'],
+            'cookie' => ['unicode' => '1F36A', 'shortname' => ':cookie:', 'aliases' => '', 'keywords' => 'cookie chocolate food oreo snack cookie dessert biscuit sweet chocolate'],
+            'recycle' => ['unicode' => '267B', 'shortname' => ':recycle:', 'aliases' => '', 'keywords' => 'black universal recycling symbol arrow environment garbage trash'],
+            'wine_glass' => ['unicode' => '1F377', 'shortname' => ':wine_glass:', 'aliases' => '', 'keywords' => 'wine glass alcohol beverage booze bottle drink drunk fermented glass grapes tasting wine winery'],
+            'arrow_right' => ['unicode' => '27A1', 'shortname' => ':arrow_right:', 'aliases' => '', 'keywords' => 'black rightwards arrow blue-square next'],
+            'panda_face' => ['unicode' => '1F43C', 'shortname' => ':panda_face:', 'aliases' => '', 'keywords' => 'panda face animal nature panda bear face cub cute endearment friendship love bamboo china black white'],
+            'dollar' => ['unicode' => '1F4B5', 'shortname' => ':dollar:', 'aliases' => '', 'keywords' => 'banknote with dollar sign bill currency money dollar united states canada australia banknote money currency paper cash bills'],
+            'hamburger' => ['unicode' => '1F354', 'shortname' => ':hamburger:', 'aliases' => '', 'keywords' => 'hamburger food meat hamburger burger meat cow beef'],
+            'icecream' => ['unicode' => '1F366', 'shortname' => ':icecream:', 'aliases' => '', 'keywords' => 'soft ice cream desert food hot icecream ice cream dairy dessert cold soft serve cone yogurt'],
+            'fries' => ['unicode' => '1F35F', 'shortname' => ':fries:', 'aliases' => '', 'keywords' => 'french fries chips food fries french potato fry russet idaho'],
+            'arrow_left' => ['unicode' => '2B05', 'shortname' => ':arrow_left:', 'aliases' => '', 'keywords' => 'leftwards black arrow arrow blue-square previous'],
+            'rainbow' => ['unicode' => '1F308', 'shortname' => ':rainbow:', 'aliases' => '', 'keywords' => 'rainbow happy nature photo sky unicorn rainbow color pride diversity spectrum refract leprechaun gold'],
+            'earth_asia' => ['unicode' => '1F30F', 'shortname' => ':earth_asia:', 'aliases' => '', 'keywords' => 'earth globe asia-australia east globe international world earth globe space planet asia australia home'],
+            'anger' => ['unicode' => '1F4A2', 'shortname' => ':anger:', 'aliases' => '', 'keywords' => 'anger symbol anger angry mad'],
+            'swimmer' => ['unicode' => '1F3CA', 'shortname' => ':swimmer:', 'aliases' => '', 'keywords' => 'swimmer sports swimmer swim water pool laps freestyle butterfly breaststroke backstroke'],
+            'blossom' => ['unicode' => '1F33C', 'shortname' => ':blossom:', 'aliases' => '', 'keywords' => 'blossom flowers nature yellow blossom daisy flower'],
+            'calling' => ['unicode' => '1F4F2', 'shortname' => ':calling:', 'aliases' => '', 'keywords' => 'mobile phone with rightwards arrow at left incoming iphone'],
+            'haircut' => ['unicode' => '1F487', 'shortname' => ':haircut:', 'aliases' => '', 'keywords' => 'haircut female girl woman'],
+            'heart_decoration' => ['unicode' => '1F49F', 'shortname' => ':heart_decoration:', 'aliases' => '', 'keywords' => 'heart decoration like love purple-square'],
+            'cake' => ['unicode' => '1F370', 'shortname' => ':cake:', 'aliases' => '', 'keywords' => 'shortcake desert food cake short dessert strawberry'],
+            'lollipop' => ['unicode' => '1F36D', 'shortname' => ':lollipop:', 'aliases' => '', 'keywords' => 'lollipop candy food snack sweet lollipop stick lick sweet sugar candy'],
+            'pouting_cat' => ['unicode' => '1F63E', 'shortname' => ':pouting_cat:', 'aliases' => '', 'keywords' => 'pouting cat face animal cats pout annoyed miffed glower frown'],
+            'syringe' => ['unicode' => '1F489', 'shortname' => ':syringe:', 'aliases' => '', 'keywords' => 'syringe blood drugs health hospital medicine needle'],
+            'registered' => ['unicode' => '00AE', 'shortname' => ':registered:', 'aliases' => '', 'keywords' => 'registered sign alphabet circle'],
+            'partly_sunny' => ['unicode' => '26C5', 'shortname' => ':partly_sunny:', 'aliases' => '', 'keywords' => 'sun behind cloud cloud morning nature weather'],
+            'iphone' => ['unicode' => '1F4F1', 'shortname' => ':iphone:', 'aliases' => '', 'keywords' => 'mobile phone apple dial gadgets technology'],
+            'arrow_backward' => ['unicode' => '25C0', 'shortname' => ':arrow_backward:', 'aliases' => '', 'keywords' => 'black left-pointing triangle arrow blue-square'],
+            'whale' => ['unicode' => '1F433', 'shortname' => ':whale:', 'aliases' => '', 'keywords' => 'spouting whale animal nature ocean sea'],
+            'envelope' => ['unicode' => '2709', 'shortname' => ':envelope:', 'aliases' => '', 'keywords' => 'envelope communication letter mail postal'],
+            'tropical_drink' => ['unicode' => '1F379', 'shortname' => ':tropical_drink:', 'aliases' => '', 'keywords' => 'tropical drink beverage tropical drink mixed pineapple coconut pina fruit umbrella'],
+            'cocktail' => ['unicode' => '1F378', 'shortname' => ':cocktail:', 'aliases' => '', 'keywords' => 'cocktail glass alcohol beverage drink drunk cocktail mixed drink alcohol glass martini bar'],
+            'hatching_chick' => ['unicode' => '1F423', 'shortname' => ':hatching_chick:', 'aliases' => '', 'keywords' => 'hatching chick born chicken egg chick egg baby bird chicken young woman cute'],
+            'smiley_cat' => ['unicode' => '1F63A', 'shortname' => ':smiley_cat:', 'aliases' => '', 'keywords' => 'smiling cat face with open mouth animal cats happy smile smiley cat happy'],
+            'fallen_leaf' => ['unicode' => '1F342', 'shortname' => ':fallen_leaf:', 'aliases' => '', 'keywords' => 'fallen leaf leaves nature plant vegetable leaf fall color deciduous autumn'],
+            'bear' => ['unicode' => '1F43B', 'shortname' => ':bear:', 'aliases' => '', 'keywords' => 'bear face animal nature'],
+            'man_with_turban' => ['unicode' => '1F473', 'shortname' => ':man_with_turban:', 'aliases' => '', 'keywords' => 'man with turban male turban headdress headwear pagri india indian mummy wisdom peace'],
+            'monkey' => ['unicode' => '1F412', 'shortname' => ':monkey:', 'aliases' => '', 'keywords' => 'monkey animal nature monkey primate banana silly'],
+            'full_moon' => ['unicode' => '1F315', 'shortname' => ':full_moon:', 'aliases' => '', 'keywords' => 'full moon symbol nature yellow moon full sky night cheese phase monster spooky werewolves twilight'],
+            'chocolate_bar' => ['unicode' => '1F36B', 'shortname' => ':chocolate_bar:', 'aliases' => '', 'keywords' => 'chocolate bar desert food snack chocolate bar candy coca hershey\'s'],
+            'rabbit' => ['unicode' => '1F430', 'shortname' => ':rabbit:', 'aliases' => '', 'keywords' => 'rabbit face animal nature'],
+            'musical_score' => ['unicode' => '1F3BC', 'shortname' => ':musical_score:', 'aliases' => '', 'keywords' => 'musical score clef treble music musical score clef g-clef stave staff'],
+            'snake' => ['unicode' => '1F40D', 'shortname' => ':snake:', 'aliases' => '', 'keywords' => 'snake animal evil'],
+            'bee' => ['unicode' => '1F41D', 'shortname' => ':bee:', 'aliases' => '', 'keywords' => 'honeybee animal insect bee queen buzz flower pollen sting honey hive bumble pollination'],
+            'mortar_board' => ['unicode' => '1F393', 'shortname' => ':mortar_board:', 'aliases' => '', 'keywords' => 'graduation cap cap college degree graduation hat school university graduation cap mortarboard academic education ceremony square tassel'],
+            'new_moon' => ['unicode' => '1F311', 'shortname' => ':new_moon:', 'aliases' => '', 'keywords' => 'new moon symbol nature moon new sky night cheese phase'],
+            'woman' => ['unicode' => '1F469', 'shortname' => ':woman:', 'aliases' => '', 'keywords' => 'woman female girls'],
+            'baseball' => ['unicode' => '26BE', 'shortname' => ':baseball:', 'aliases' => '', 'keywords' => 'baseball MLB balls sports'],
+            'older_woman' => ['unicode' => '1F475', 'shortname' => ':older_woman:', 'aliases' => ':grandma:', 'keywords' => 'older woman female girl women grandma grandmother'],
+            'no_entry_sign' => ['unicode' => '1F6AB', 'shortname' => ':no_entry_sign:', 'aliases' => '', 'keywords' => 'no entry sign denied disallow forbid limit stop no stop entry'],
+            'dolphin' => ['unicode' => '1F42C', 'shortname' => ':dolphin:', 'aliases' => '', 'keywords' => 'dolphin animal fins fish flipper nature ocean sea'],
+            'books' => ['unicode' => '1F4DA', 'shortname' => ':books:', 'aliases' => '', 'keywords' => 'books library literature'],
+            'bikini' => ['unicode' => '1F459', 'shortname' => ':bikini:', 'aliases' => '', 'keywords' => 'bikini beach fashion female girl swimming woman'],
+            'tv' => ['unicode' => '1F1F9-1F1FB', 'shortname' => ':tv:', 'aliases' => '', 'keywords' => 'tuvalu country nation'],
+            'strawberry' => ['unicode' => '1F353', 'shortname' => ':strawberry:', 'aliases' => '', 'keywords' => 'strawberry food fruit nature strawberry short cake berry'],
+            'feet' => ['unicode' => '1F43E', 'shortname' => ':feet:', 'aliases' => '', 'keywords' => 'paw prints animal cat dog footprints paw pet tracking paw prints mark imprints footsteps animal lion bear dog cat raccoon critter feet pawsteps'],
+            'family' => ['unicode' => '1F46A', 'shortname' => ':family:', 'aliases' => '', 'keywords' => 'family child dad father home mom mother parents family mother father child girl boy group unit'],
+            'hatched_chick' => ['unicode' => '1F425', 'shortname' => ':hatched_chick:', 'aliases' => '', 'keywords' => 'front-facing baby chick baby chicken chick baby bird chicken young woman cute'],
+            'nose' => ['unicode' => '1F443', 'shortname' => ':nose:', 'aliases' => '', 'keywords' => 'nose smell sniff'],
+            'cherries' => ['unicode' => '1F352', 'shortname' => ':cherries:', 'aliases' => '', 'keywords' => 'cherries food fruit cherry cherries tree fruit pit'],
+            'jack_o_lantern' => ['unicode' => '1F383', 'shortname' => ':jack_o_lantern:', 'aliases' => '', 'keywords' => 'jack-o-lantern halloween jack-o-lantern pumpkin halloween holiday carve autumn fall october saints costume spooky horror scary scared dead'],
+            'ear_of_rice' => ['unicode' => '1F33E', 'shortname' => ':ear_of_rice:', 'aliases' => '', 'keywords' => 'ear of rice nature plant ear rice food plant seed'],
+            'scissors' => ['unicode' => '2702', 'shortname' => ':scissors:', 'aliases' => '', 'keywords' => 'black scissors cut stationery'],
+            'frog' => ['unicode' => '1F438', 'shortname' => ':frog:', 'aliases' => '', 'keywords' => 'frog face animal nature'],
+            'octopus' => ['unicode' => '1F419', 'shortname' => ':octopus:', 'aliases' => '', 'keywords' => 'octopus animal creature ocean sea'],
+            'high_heel' => ['unicode' => '1F460', 'shortname' => ':high_heel:', 'aliases' => '', 'keywords' => 'high-heeled shoe fashion female shoes'],
+            'loud_sound' => ['unicode' => '1F50A', 'shortname' => ':loud_sound:', 'aliases' => '', 'keywords' => 'speaker with three sound waves '],
+            'top' => ['unicode' => '1F51D', 'shortname' => ':top:', 'aliases' => '', 'keywords' => 'top with upwards arrow above blue-square words'],
+            'house_with_garden' => ['unicode' => '1F3E1', 'shortname' => ':house_with_garden:', 'aliases' => '', 'keywords' => 'house with garden home nature plant'],
+            'rotating_light' => ['unicode' => '1F6A8', 'shortname' => ':rotating_light:', 'aliases' => '', 'keywords' => 'police cars revolving light 911 ambulance emergency police light police emergency'],
+            'lipstick' => ['unicode' => '1F484', 'shortname' => ':lipstick:', 'aliases' => '', 'keywords' => 'lipstick fashion female girl'],
+            'ear' => ['unicode' => '1F442', 'shortname' => ':ear:', 'aliases' => '', 'keywords' => 'ear face hear listen sound'],
+            'first_quarter_moon' => ['unicode' => '1F313', 'shortname' => ':first_quarter_moon:', 'aliases' => '', 'keywords' => 'first quarter moon symbol nature moon quarter first sky night cheese phase'],
+            'pineapple' => ['unicode' => '1F34D', 'shortname' => ':pineapple:', 'aliases' => '', 'keywords' => 'pineapple food fruit nature pineapple pina tropical flower'],
+            'elephant' => ['unicode' => '1F418', 'shortname' => ':elephant:', 'aliases' => '', 'keywords' => 'elephant animal nature nose thailand'],
+            'athletic_shoe' => ['unicode' => '1F45F', 'shortname' => ':athletic_shoe:', 'aliases' => '', 'keywords' => 'athletic shoe shoes sports'],
+            'crystal_ball' => ['unicode' => '1F52E', 'shortname' => ':crystal_ball:', 'aliases' => '', 'keywords' => 'crystal ball disco party'],
+            'love_letter' => ['unicode' => '1F48C', 'shortname' => ':love_letter:', 'aliases' => '', 'keywords' => 'love letter affection email envelope like valentines love letter kiss heart'],
+            'waxing_gibbous_moon' => ['unicode' => '1F314', 'shortname' => ':waxing_gibbous_moon:', 'aliases' => '', 'keywords' => 'waxing gibbous moon symbol nature'],
+            'girl' => ['unicode' => '1F467', 'shortname' => ':girl:', 'aliases' => '', 'keywords' => 'girl female woman'],
+            'cool' => ['unicode' => '1F192', 'shortname' => ':cool:', 'aliases' => '', 'keywords' => 'squared cool blue-square words'],
+            'white_circle' => ['unicode' => '26AA', 'shortname' => ':white_circle:', 'aliases' => '', 'keywords' => 'medium white circle shape'],
+            'poultry_leg' => ['unicode' => '1F357', 'shortname' => ':poultry_leg:', 'aliases' => '', 'keywords' => 'poultry leg food meat poultry leg chicken fried'],
+            'speech_balloon' => ['unicode' => '1F4AC', 'shortname' => ':speech_balloon:', 'aliases' => '', 'keywords' => 'speech balloon bubble words speech balloon talk conversation communication comic dialogue'],
+            'question' => ['unicode' => '2753', 'shortname' => ':question:', 'aliases' => '', 'keywords' => 'black question mark ornament confused doubt'],
+            'tropical_fish' => ['unicode' => '1F420', 'shortname' => ':tropical_fish:', 'aliases' => '', 'keywords' => 'tropical fish animal swim'],
+            'older_man' => ['unicode' => '1F474', 'shortname' => ':older_man:', 'aliases' => '', 'keywords' => 'older man human male men'],
+            'bride_with_veil' => ['unicode' => '1F470', 'shortname' => ':bride_with_veil:', 'aliases' => '', 'keywords' => 'bride with veil couple marriage wedding bride wedding planning veil gown dress engagement white'],
+            'peach' => ['unicode' => '1F351', 'shortname' => ':peach:', 'aliases' => '', 'keywords' => 'peach food fruit nature peach fruit juicy pit'],
+            'eyeglasses' => ['unicode' => '1F453', 'shortname' => ':eyeglasses:', 'aliases' => '', 'keywords' => 'eyeglasses accessories eyesight fashion eyeglasses spectacles eye sight nearsightedness myopia farsightedness hyperopia frames vision see blurry contacts'],
+            'pencil' => ['unicode' => '1F4DD', 'shortname' => ':pencil:', 'aliases' => '', 'keywords' => 'memo documents paper station write'],
+            'spaghetti' => ['unicode' => '1F35D', 'shortname' => ':spaghetti:', 'aliases' => '', 'keywords' => 'spaghetti food italian noodle spaghetti noodles tomato sauce italian'],
+            'boy' => ['unicode' => '1F466', 'shortname' => ':boy:', 'aliases' => '', 'keywords' => 'boy guy male man'],
+            'black_circle' => ['unicode' => '26AB', 'shortname' => ':black_circle:', 'aliases' => '', 'keywords' => 'medium black circle shape'],
+            'book' => ['unicode' => '1F4D6', 'shortname' => ':book:', 'aliases' => '', 'keywords' => 'open book library literature'],
+            'pill' => ['unicode' => '1F48A', 'shortname' => ':pill:', 'aliases' => '', 'keywords' => 'pill health medicine'],
+            'loudspeaker' => ['unicode' => '1F4E2', 'shortname' => ':loudspeaker:', 'aliases' => '', 'keywords' => 'public address loudspeaker sound volume'],
+            'horse' => ['unicode' => '1F434', 'shortname' => ':horse:', 'aliases' => '', 'keywords' => 'horse face animal brown'],
+            'milky_way' => ['unicode' => '1F30C', 'shortname' => ':milky_way:', 'aliases' => '', 'keywords' => 'milky way photo space milky galaxy star stars planets space sky'],
+            'fish' => ['unicode' => '1F41F', 'shortname' => ':fish:', 'aliases' => '', 'keywords' => 'fish animal food nature'],
+            'surfer' => ['unicode' => '1F3C4', 'shortname' => ':surfer:', 'aliases' => '', 'keywords' => 'surfer ocean sea sports surfer surf wave ocean ride swell'],
+            'closed_lock_with_key' => ['unicode' => '1F510', 'shortname' => ':closed_lock_with_key:', 'aliases' => '', 'keywords' => 'closed lock with key privacy security'],
+            'warning' => ['unicode' => '26A0', 'shortname' => ':warning:', 'aliases' => '', 'keywords' => 'warning sign exclamation wip'],
+            'apple' => ['unicode' => '1F34E', 'shortname' => ':apple:', 'aliases' => '', 'keywords' => 'red apple fruit mac apple fruit electronics red doctor teacher school core'],
+            'fishing_pole_and_fish' => ['unicode' => '1F3A3', 'shortname' => ':fishing_pole_and_fish:', 'aliases' => '', 'keywords' => 'fishing pole and fish food hobby fish fishing pole'],
+            'dress' => ['unicode' => '1F457', 'shortname' => ':dress:', 'aliases' => '', 'keywords' => 'dress clothes fashion'],
+            'clapper' => ['unicode' => '1F3AC', 'shortname' => ':clapper:', 'aliases' => '', 'keywords' => 'clapper board film movie record clapper board clapboard movie film take'],
+            'man_with_gua_pi_mao' => ['unicode' => '1F472', 'shortname' => ':man_with_gua_pi_mao:', 'aliases' => '', 'keywords' => 'man with gua pi mao boy male skullcap chinese asian qing'],
+            'sunrise' => ['unicode' => '1F305', 'shortname' => ':sunrise:', 'aliases' => '', 'keywords' => 'sunrise morning photo vacation view sunrise sun morning color sky'],
+            'grapes' => ['unicode' => '1F347', 'shortname' => ':grapes:', 'aliases' => '', 'keywords' => 'grapes food fruit grapes wine vinegar fruit cluster vine'],
+            'first_quarter_moon_with_face' => ['unicode' => '1F31B', 'shortname' => ':first_quarter_moon_with_face:', 'aliases' => '', 'keywords' => 'first quarter moon with face nature moon first quarter anthropomorphic face sky night cheese phase'],
+            'telephone_receiver' => ['unicode' => '1F4DE', 'shortname' => ':telephone_receiver:', 'aliases' => '', 'keywords' => 'telephone receiver communication dial technology'],
+            'eight_spoked_asterisk' => ['unicode' => '2733', 'shortname' => ':eight_spoked_asterisk:', 'aliases' => '', 'keywords' => 'eight spoked asterisk green-square sparkle star'],
+            'sos' => ['unicode' => '1F198', 'shortname' => ':sos:', 'aliases' => '', 'keywords' => 'squared sos emergency help red-square words'],
+            'koala' => ['unicode' => '1F428', 'shortname' => ':koala:', 'aliases' => '', 'keywords' => 'koala animal nature'],
+            'blue_car' => ['unicode' => '1F699', 'shortname' => ':blue_car:', 'aliases' => '', 'keywords' => 'recreational vehicle car suv car wagon automobile'],
+            'arrow_down' => ['unicode' => '2B07', 'shortname' => ':arrow_down:', 'aliases' => '', 'keywords' => 'downwards black arrow arrow blue-square'],
+            'ramen' => ['unicode' => '1F35C', 'shortname' => ':ramen:', 'aliases' => '', 'keywords' => 'steaming bowl chipsticks food japanese noodle ramen noodles bowl steaming soup'],
+            'house' => ['unicode' => '1F3E0', 'shortname' => ':house:', 'aliases' => '', 'keywords' => 'house building building home house home residence dwelling mansion bungalow ranch craftsman'],
+            'pig_nose' => ['unicode' => '1F43D', 'shortname' => ':pig_nose:', 'aliases' => '', 'keywords' => 'pig nose animal oink pig nose snout food eat cute oink pink smell truffle'],
+            'anchor' => ['unicode' => '2693', 'shortname' => ':anchor:', 'aliases' => '', 'keywords' => 'anchor ferry ship anchor ship boat ocean harbor marina shipyard sailor tattoo'],
+            'art' => ['unicode' => '1F3A8', 'shortname' => ':art:', 'aliases' => '', 'keywords' => 'artist palette design draw paint artist palette art colors paint draw brush pastels oils'],
+            'chicken' => ['unicode' => '1F414', 'shortname' => ':chicken:', 'aliases' => '', 'keywords' => 'chicken animal cluck chicken hen poultry livestock'],
+            'wavy_dash' => ['unicode' => '3030', 'shortname' => ':wavy_dash:', 'aliases' => '', 'keywords' => 'wavy dash draw line'],
+            'monkey_face' => ['unicode' => '1F435', 'shortname' => ':monkey_face:', 'aliases' => '', 'keywords' => 'monkey face animal nature'],
+            'ok' => ['unicode' => '1F197', 'shortname' => ':ok:', 'aliases' => '', 'keywords' => 'squared ok agree blue-square good yes'],
+            'candy' => ['unicode' => '1F36C', 'shortname' => ':candy:', 'aliases' => '', 'keywords' => 'candy desert snack candy sugar sweet hard'],
+            'tangerine' => ['unicode' => '1F34A', 'shortname' => ':tangerine:', 'aliases' => '', 'keywords' => 'tangerine food fruit nature tangerine citrus orange'],
+            'm' => ['unicode' => '24C2', 'shortname' => ':m:', 'aliases' => '', 'keywords' => 'circled latin capital letter m alphabet blue-circle letter'],
+            'bath' => ['unicode' => '1F6C0', 'shortname' => ':bath:', 'aliases' => '', 'keywords' => 'bath clean shower bath tub basin wash bubble soak bathroom soap water clean shampoo lather water'],
+            'cow' => ['unicode' => '1F42E', 'shortname' => ':cow:', 'aliases' => '', 'keywords' => 'cow face animal beef ox'],
+            'mushroom' => ['unicode' => '1F344', 'shortname' => ':mushroom:', 'aliases' => '', 'keywords' => 'mushroom plant vegetable mushroom fungi food fungus'],
+            'mouse' => ['unicode' => '1F42D', 'shortname' => ':mouse:', 'aliases' => '', 'keywords' => 'mouse face animal nature'],
+            'large_blue_circle' => ['unicode' => '1F535', 'shortname' => ':large_blue_circle:', 'aliases' => '', 'keywords' => 'large blue circle '],
+            'japanese_goblin' => ['unicode' => '1F47A', 'shortname' => ':japanese_goblin:', 'aliases' => '', 'keywords' => 'japanese goblin evil mask red japanese tengu supernatural avian demon goblin mask theater nose frown mustache anger frustration'],
+            'moyai' => ['unicode' => '1F5FF', 'shortname' => ':moyai:', 'aliases' => '', 'keywords' => 'moyai island stone'],
+            'egg' => ['unicode' => '1F373', 'shortname' => ':egg:', 'aliases' => '', 'keywords' => 'cooking breakfast food egg fry pan flat cook frying cooking utensil'],
+            'tennis' => ['unicode' => '1F3BE', 'shortname' => ':tennis:', 'aliases' => '', 'keywords' => 'tennis racquet and ball balls green sports tennis racket racquet ball game net court love'],
+            'fireworks' => ['unicode' => '1F386', 'shortname' => ':fireworks:', 'aliases' => '', 'keywords' => 'fireworks carnival congratulations festival photo fireworks independence celebration explosion july 4th rocket sky idea excitement'],
+            'racehorse' => ['unicode' => '1F40E', 'shortname' => ':racehorse:', 'aliases' => '', 'keywords' => 'horse animal gamble horse powerful draft calvary cowboy cowgirl mounted race ride gallop trot colt filly mare stallion gelding yearling thoroughbred pony'],
+            'bread' => ['unicode' => '1F35E', 'shortname' => ':bread:', 'aliases' => '', 'keywords' => 'bread breakfast food toast wheat bread loaf yeast'],
+            'bird' => ['unicode' => '1F426', 'shortname' => ':bird:', 'aliases' => '', 'keywords' => 'bird animal fly nature tweet'],
+            'droplet' => ['unicode' => '1F4A7', 'shortname' => ':droplet:', 'aliases' => '', 'keywords' => 'droplet drip faucet water drop droplet h20 water aqua tear sweat rain moisture wet moist spit'],
+            'fried_shrimp' => ['unicode' => '1F364', 'shortname' => ':fried_shrimp:', 'aliases' => '', 'keywords' => 'fried shrimp animal food shrimp fried seafood small fish'],
+            'key' => ['unicode' => '1F511', 'shortname' => ':key:', 'aliases' => '', 'keywords' => 'key door lock password'],
+            'back' => ['unicode' => '1F519', 'shortname' => ':back:', 'aliases' => '', 'keywords' => 'back with leftwards arrow above arrow'],
+            'bike' => ['unicode' => '1F6B2', 'shortname' => ':bike:', 'aliases' => '', 'keywords' => 'bicycle bicycle exercise hipster sports bike pedal bicycle transportation'],
+            'pencil2' => ['unicode' => '270F', 'shortname' => ':pencil2:', 'aliases' => '', 'keywords' => 'pencil paper stationery write'],
+            'shaved_ice' => ['unicode' => '1F367', 'shortname' => ':shaved_ice:', 'aliases' => '', 'keywords' => 'shaved ice desert hot shaved ice dessert treat syrup flavoring'],
+            'arrow_right_hook' => ['unicode' => '21AA', 'shortname' => ':arrow_right_hook:', 'aliases' => '', 'keywords' => 'rightwards arrow with hook blue-square'],
+            'bulb' => ['unicode' => '1F4A1', 'shortname' => ':bulb:', 'aliases' => '', 'keywords' => 'electric light bulb electricity light idea bulb light'],
+            'tophat' => ['unicode' => '1F3A9', 'shortname' => ':tophat:', 'aliases' => '', 'keywords' => 'top hat classy gentleman magic top hat cap beaver high tall stove pipe chimney topper london period piece magic magician'],
+            'wolf' => ['unicode' => '1F43A', 'shortname' => ':wolf:', 'aliases' => '', 'keywords' => 'wolf face animal nature'],
+            'night_with_stars' => ['unicode' => '1F303', 'shortname' => ':night_with_stars:', 'aliases' => '', 'keywords' => 'night with stars night star cloudless evening planets space sky'],
+            'grey_exclamation' => ['unicode' => '2755', 'shortname' => ':grey_exclamation:', 'aliases' => '', 'keywords' => 'white exclamation mark ornament surprise'],
+            'alarm_clock' => ['unicode' => '23F0', 'shortname' => ':alarm_clock:', 'aliases' => '', 'keywords' => 'alarm clock time wake'],
+            'cop' => ['unicode' => '1F46E', 'shortname' => ':cop:', 'aliases' => '', 'keywords' => 'police officer arrest enforcement law man police'],
+            'arrow_lower_left' => ['unicode' => '2199', 'shortname' => ':arrow_lower_left:', 'aliases' => '', 'keywords' => 'south west arrow arrow blue-square'],
+            'person_with_blond_hair' => ['unicode' => '1F471', 'shortname' => ':person_with_blond_hair:', 'aliases' => '', 'keywords' => 'person with blond hair male man blonde young western westerner occidental'],
+            'jeans' => ['unicode' => '1F456', 'shortname' => ':jeans:', 'aliases' => '', 'keywords' => 'jeans fashion shopping jeans pants blue denim levi\'s levi designer work skinny'],
+            'sheep' => ['unicode' => '1F411', 'shortname' => ':sheep:', 'aliases' => '', 'keywords' => 'sheep animal nature sheep wool flock follower ewe female lamb'],
+            'golf' => ['unicode' => '26F3', 'shortname' => ':golf:', 'aliases' => '', 'keywords' => 'flag in hole business sports'],
+            'arrow_upper_right' => ['unicode' => '2197', 'shortname' => ':arrow_upper_right:', 'aliases' => '', 'keywords' => 'north east arrow blue-square'],
+            'cd' => ['unicode' => '1F1E8-1F1E9', 'shortname' => ':cd:', 'aliases' => '', 'keywords' => 'the democratic republic of the congo country nation république démocratique du congo republique democratique du congo'],
+            'watch' => ['unicode' => '231A', 'shortname' => ':watch:', 'aliases' => '', 'keywords' => 'watch accessories time'],
+            'performing_arts' => ['unicode' => '1F3AD', 'shortname' => ':performing_arts:', 'aliases' => '', 'keywords' => 'performing arts acting drama theater performing arts performance entertainment acting story mask masks'],
+            'bug' => ['unicode' => '1F41B', 'shortname' => ':bug:', 'aliases' => '', 'keywords' => 'bug insect nature bug insect virus error'],
+            'sushi' => ['unicode' => '1F363', 'shortname' => ':sushi:', 'aliases' => '', 'keywords' => 'sushi food japanese sushi fish raw nigiri japanese'],
+            'baby_chick' => ['unicode' => '1F424', 'shortname' => ':baby_chick:', 'aliases' => '', 'keywords' => 'baby chick animal chicken chick baby bird chicken young woman cute'],
+            'small_blue_diamond' => ['unicode' => '1F539', 'shortname' => ':small_blue_diamond:', 'aliases' => '', 'keywords' => 'small blue diamond shape'],
+            'electric_plug' => ['unicode' => '1F50C', 'shortname' => ':electric_plug:', 'aliases' => '', 'keywords' => 'electric plug charger power'],
+            'lock' => ['unicode' => '1F512', 'shortname' => ':lock:', 'aliases' => '', 'keywords' => 'lock password security'],
+            'black_square_button' => ['unicode' => '1F532', 'shortname' => ':black_square_button:', 'aliases' => '', 'keywords' => 'black square button frame'],
+            'fish_cake' => ['unicode' => '1F365', 'shortname' => ':fish_cake:', 'aliases' => '', 'keywords' => 'fish cake with swirl design food fish cake kamboko swirl ramen noodles naruto'],
+            'seedling' => ['unicode' => '1F331', 'shortname' => ':seedling:', 'aliases' => '', 'keywords' => 'seedling grass lawn nature plant seedling plant new start grow'],
+            'corn' => ['unicode' => '1F33D', 'shortname' => ':corn:', 'aliases' => '', 'keywords' => 'ear of maize food plant vegetable corn maize food iowa kernel popcorn husk yellow stalk cob ear'],
+            'leftwards_arrow_with_hook' => ['unicode' => '21A9', 'shortname' => ':leftwards_arrow_with_hook:', 'aliases' => '', 'keywords' => 'leftwards arrow with hook '],
+            'arrow_heading_down' => ['unicode' => '2935', 'shortname' => ':arrow_heading_down:', 'aliases' => '', 'keywords' => 'arrow pointing rightwards then curving downwards arrow blue-square'],
+            'ant' => ['unicode' => '1F41C', 'shortname' => ':ant:', 'aliases' => '', 'keywords' => 'ant animal insect ant queen insect team'],
+            'checkered_flag' => ['unicode' => '1F3C1', 'shortname' => ':checkered_flag:', 'aliases' => '', 'keywords' => 'chequered flag contest finishline gokart rase checkered chequred race flag finish complete end'],
+            'tea' => ['unicode' => '1F375', 'shortname' => ':tea:', 'aliases' => '', 'keywords' => 'teacup without handle bowl breakfast british drink green tea leaf drink teacup hot beverage'],
+            'stew' => ['unicode' => '1F372', 'shortname' => ':stew:', 'aliases' => '', 'keywords' => 'pot of food food meat stew hearty soup thick hot pot'],
+            'arrow_up' => ['unicode' => '2B06', 'shortname' => ':arrow_up:', 'aliases' => '', 'keywords' => 'upwards black arrow blue-square'],
+            'underage' => ['unicode' => '1F51E', 'shortname' => ':underage:', 'aliases' => '', 'keywords' => 'no one under eighteen symbol 18 drink night pub'],
+            'snail' => ['unicode' => '1F40C', 'shortname' => ':snail:', 'aliases' => '', 'keywords' => 'snail animal shell slow snail slow escargot french appetizer'],
+            'meat_on_bone' => ['unicode' => '1F356', 'shortname' => ':meat_on_bone:', 'aliases' => '', 'keywords' => 'meat on bone food good meat bone animal cooked'],
+            'camel' => ['unicode' => '1F42B', 'shortname' => ':camel:', 'aliases' => '', 'keywords' => 'bactrian camel animal hot nature bactrian camel hump desert central asia heat hot water hump day wednesday sex'],
+            'necktie' => ['unicode' => '1F454', 'shortname' => ':necktie:', 'aliases' => '', 'keywords' => 'necktie cloth fashion formal shirt suitup'],
+            'toilet' => ['unicode' => '1F6BD', 'shortname' => ':toilet:', 'aliases' => '', 'keywords' => 'toilet restroom wc toilet bathroom throne porcelain waste flush plumbing'],
+            'a' => ['unicode' => '1F170', 'shortname' => ':a:', 'aliases' => '', 'keywords' => 'negative squared latin capital letter a alphabet letter red-square'],
+            'arrow_lower_right' => ['unicode' => '2198', 'shortname' => ':arrow_lower_right:', 'aliases' => '', 'keywords' => 'south east arrow arrow blue-square'],
+            'hamster' => ['unicode' => '1F439', 'shortname' => ':hamster:', 'aliases' => '', 'keywords' => 'hamster face animal nature'],
+            'fuelpump' => ['unicode' => '26FD', 'shortname' => ':fuelpump:', 'aliases' => '', 'keywords' => 'fuel pump gas station petroleum'],
+            'hammer' => ['unicode' => '1F528', 'shortname' => ':hammer:', 'aliases' => '', 'keywords' => 'hammer done judge law ruling tools verdict'],
+            'bust_in_silhouette' => ['unicode' => '1F464', 'shortname' => ':bust_in_silhouette:', 'aliases' => '', 'keywords' => 'bust in silhouette human man person user silhouette person user member account guest icon avatar profile me myself i'],
+            'up' => ['unicode' => '1F199', 'shortname' => ':up:', 'aliases' => '', 'keywords' => 'squared up with exclamation mark blue-square'],
+            'snowboarder' => ['unicode' => '1F3C2', 'shortname' => ':snowboarder:', 'aliases' => '', 'keywords' => 'snowboarder sports winter snow boarding sports freestyle halfpipe board mountain alpine winter'],
+            'curly_loop' => ['unicode' => '27B0', 'shortname' => ':curly_loop:', 'aliases' => '', 'keywords' => 'curly loop scribble'],
+            'handbag' => ['unicode' => '1F45C', 'shortname' => ':handbag:', 'aliases' => '', 'keywords' => 'handbag accessories accessory bag fashion'],
+            'dart' => ['unicode' => '1F3AF', 'shortname' => ':dart:', 'aliases' => '', 'keywords' => 'direct hit bar game direct hit bullseye dart archery game fletching arrow sport'],
+            'computer' => ['unicode' => '1F4BB', 'shortname' => ':computer:', 'aliases' => '', 'keywords' => 'personal computer laptop tech'],
+            'poodle' => ['unicode' => '1F429', 'shortname' => ':poodle:', 'aliases' => '', 'keywords' => 'poodle 101 animal dog nature poodle dog clip showy sophisticated vain'],
+            'cancer' => ['unicode' => '264B', 'shortname' => ':cancer:', 'aliases' => '', 'keywords' => 'cancer cancer crab astrology greek constellation stars zodiac sign sign zodiac horoscope'],
+            'rice' => ['unicode' => '1F35A', 'shortname' => ':rice:', 'aliases' => '', 'keywords' => 'cooked rice food rice white grain food bowl'],
+            'black_medium_small_square' => ['unicode' => '25FE', 'shortname' => ':black_medium_small_square:', 'aliases' => '', 'keywords' => 'black medium small square '],
+            'seat' => ['unicode' => '1F4BA', 'shortname' => ':seat:', 'aliases' => '', 'keywords' => 'seat sit'],
+            'shell' => ['unicode' => '1F41A', 'shortname' => ':shell:', 'aliases' => '', 'keywords' => 'spiral shell beach nature sea shell spiral beach sand crab nautilus'],
+            'trident' => ['unicode' => '1F531', 'shortname' => ':trident:', 'aliases' => '', 'keywords' => 'trident emblem spear weapon'],
+            'hotsprings' => ['unicode' => '2668', 'shortname' => ':hotsprings:', 'aliases' => '', 'keywords' => 'hot springs bath relax warm'],
+            'curry' => ['unicode' => '1F35B', 'shortname' => ':curry:', 'aliases' => '', 'keywords' => 'curry and rice food hot indian spicy curry spice flavor food meal'],
+            'ice_cream' => ['unicode' => '1F368', 'shortname' => ':ice_cream:', 'aliases' => '', 'keywords' => 'ice cream desert food hot icecream ice cream dairy dessert cold soft serve cone waffle'],
+            'diamond_shape_with_a_dot_inside' => ['unicode' => '1F4A0', 'shortname' => ':diamond_shape_with_a_dot_inside:', 'aliases' => '', 'keywords' => 'diamond shape with a dot inside diamond cute cuteness kawaii japanese glyph adorable'],
+            'green_apple' => ['unicode' => '1F34F', 'shortname' => ':green_apple:', 'aliases' => '', 'keywords' => 'green apple fruit nature apple fruit green pie granny smith core'],
+            'statue_of_liberty' => ['unicode' => '1F5FD', 'shortname' => ':statue_of_liberty:', 'aliases' => '', 'keywords' => 'statue of liberty american newyork'],
+            'bus' => ['unicode' => '1F68C', 'shortname' => ':bus:', 'aliases' => '', 'keywords' => 'bus car transportation vehicle bus school city transportation public'],
+            'bowling' => ['unicode' => '1F3B3', 'shortname' => ':bowling:', 'aliases' => '', 'keywords' => 'bowling fun play sports bowl bowling ball pin strike spare game'],
+            'dolls' => ['unicode' => '1F38E', 'shortname' => ':dolls:', 'aliases' => '', 'keywords' => 'japanese dolls japanese kimono toy dolls japan japanese day girls emperor empress pray blessing imperial family royal'],
+            'baby_symbol' => ['unicode' => '1F6BC', 'shortname' => ':baby_symbol:', 'aliases' => '', 'keywords' => 'baby symbol child orange-square baby crawl newborn human diaper small babe'],
+            'construction_worker' => ['unicode' => '1F477', 'shortname' => ':construction_worker:', 'aliases' => '', 'keywords' => 'construction worker human male man wip'],
+            'custard' => ['unicode' => '1F36E', 'shortname' => ':custard:', 'aliases' => '', 'keywords' => 'custard desert food custard cream rich butter dessert crème brûlée french'],
+            'unlock' => ['unicode' => '1F513', 'shortname' => ':unlock:', 'aliases' => '', 'keywords' => 'open lock privacy security'],
+            'shirt' => ['unicode' => '1F455', 'shortname' => ':shirt:', 'aliases' => '', 'keywords' => 't-shirt cloth fashion'],
+            'credit_card' => ['unicode' => '1F4B3', 'shortname' => ':credit_card:', 'aliases' => '', 'keywords' => 'credit card bill dollar money pay payment credit card loan purchase shopping mastercard visa american express wallet signature'],
+            'bento' => ['unicode' => '1F371', 'shortname' => ':bento:', 'aliases' => '', 'keywords' => 'bento box box food japanese bento japanese rice meal box obento convenient lunchbox'],
+            'beetle' => ['unicode' => '1F41E', 'shortname' => ':beetle:', 'aliases' => '', 'keywords' => 'lady beetle insect nature lady bug ladybug ladybird beetle cow lady cow insect endearment'],
+            'mans_shoe' => ['unicode' => '1F45E', 'shortname' => ':mans_shoe:', 'aliases' => '', 'keywords' => 'mans shoe fashion male'],
+            'chestnut' => ['unicode' => '1F330', 'shortname' => ':chestnut:', 'aliases' => '', 'keywords' => 'chestnut food squirrel chestnut roasted food tree'],
+            'interrobang' => ['unicode' => '2049', 'shortname' => ':interrobang:', 'aliases' => '', 'keywords' => 'exclamation question mark punctuation surprise wat'],
+            'small_red_triangle' => ['unicode' => '1F53A', 'shortname' => ':small_red_triangle:', 'aliases' => '', 'keywords' => 'up-pointing red triangle shape'],
+            'heavy_dollar_sign' => ['unicode' => '1F4B2', 'shortname' => ':heavy_dollar_sign:', 'aliases' => '', 'keywords' => 'heavy dollar sign currency money payment dollar currency money cash sale purchase value'],
+            'battery' => ['unicode' => '1F50B', 'shortname' => ':battery:', 'aliases' => '', 'keywords' => 'battery energy power sustain'],
+            'black_nib' => ['unicode' => '2712', 'shortname' => ':black_nib:', 'aliases' => '', 'keywords' => 'black nib pen stationery'],
+            'police_car' => ['unicode' => '1F693', 'shortname' => ':police_car:', 'aliases' => '', 'keywords' => 'police car cars enforcement law transportation vehicle police car emergency ticket citation crime help officer'],
+            'honey_pot' => ['unicode' => '1F36F', 'shortname' => ':honey_pot:', 'aliases' => '', 'keywords' => 'honey pot bees sweet honey pot bees pooh bear'],
+            'small_orange_diamond' => ['unicode' => '1F538', 'shortname' => ':small_orange_diamond:', 'aliases' => '', 'keywords' => 'small orange diamond shape'],
+            'b' => ['unicode' => '1F171', 'shortname' => ':b:', 'aliases' => '', 'keywords' => 'negative squared latin capital letter b alphabet letter red-square'],
+            'arrows_clockwise' => ['unicode' => '1F503', 'shortname' => ':arrows_clockwise:', 'aliases' => '', 'keywords' => 'clockwise downwards and upwards open circle arrows sync'],
+            'roller_coaster' => ['unicode' => '1F3A2', 'shortname' => ':roller_coaster:', 'aliases' => '', 'keywords' => 'roller coaster carnival fun photo play playground roller coaster amusement park fair ride entertainment'],
+            'door' => ['unicode' => '1F6AA', 'shortname' => ':door:', 'aliases' => '', 'keywords' => 'door entry exit house door doorway entrance enter exit entry'],
+            'sunrise_over_mountains' => ['unicode' => '1F304', 'shortname' => ':sunrise_over_mountains:', 'aliases' => '', 'keywords' => 'sunrise over mountains photo vacation view sunrise sun morning mountain rural color sky'],
+            '8ball' => ['unicode' => '1F3B1', 'shortname' => ':8ball:', 'aliases' => '', 'keywords' => 'billiards pool billiards eight ball pool pocket ball cue'],
+            'eight_pointed_black_star' => ['unicode' => '2734', 'shortname' => ':eight_pointed_black_star:', 'aliases' => '', 'keywords' => 'eight pointed black star '],
+            'musical_keyboard' => ['unicode' => '1F3B9', 'shortname' => ':musical_keyboard:', 'aliases' => '', 'keywords' => 'musical keyboard instrument piano music keyboard piano organ instrument electric'],
+            'sparkler' => ['unicode' => '1F387', 'shortname' => ':sparkler:', 'aliases' => '', 'keywords' => 'firework sparkler night shine stars'],
+            'small_red_triangle_down' => ['unicode' => '1F53B', 'shortname' => ':small_red_triangle_down:', 'aliases' => '', 'keywords' => 'down-pointing red triangle shape'],
+            'arrow_upper_left' => ['unicode' => '2196', 'shortname' => ':arrow_upper_left:', 'aliases' => '', 'keywords' => 'north west arrow blue-square'],
+            'left_right_arrow' => ['unicode' => '2194', 'shortname' => ':left_right_arrow:', 'aliases' => '', 'keywords' => 'left right arrow shape'],
+            'barber' => ['unicode' => '1F488', 'shortname' => ':barber:', 'aliases' => '', 'keywords' => 'barber pole hair salon style'],
+            'large_orange_diamond' => ['unicode' => '1F536', 'shortname' => ':large_orange_diamond:', 'aliases' => '', 'keywords' => 'large orange diamond shape'],
+            'hospital' => ['unicode' => '1F3E5', 'shortname' => ':hospital:', 'aliases' => '', 'keywords' => 'hospital building doctor health surgery'],
+            'city_dusk' => ['unicode' => '1F306', 'shortname' => ':city_dusk:', 'aliases' => '', 'keywords' => 'cityscape at dusk photo city scape sunset dusk lights evening metropolitan night dark'],
+            'scorpius' => ['unicode' => '264F', 'shortname' => ':scorpius:', 'aliases' => '', 'keywords' => 'scorpius scorpius scorpion scorpio astrology greek constellation stars zodiac sign sign zodiac horoscope'],
+            'sailboat' => ['unicode' => '26F5', 'shortname' => ':sailboat:', 'aliases' => '', 'keywords' => 'sailboat ship transportation'],
+            'tomato' => ['unicode' => '1F345', 'shortname' => ':tomato:', 'aliases' => '', 'keywords' => 'tomato food fruit nature vegetable tomato fruit sauce italian'],
+            'sparkle' => ['unicode' => '2747', 'shortname' => ':sparkle:', 'aliases' => '', 'keywords' => 'sparkle green-square stars'],
+            'closed_umbrella' => ['unicode' => '1F302', 'shortname' => ':closed_umbrella:', 'aliases' => '', 'keywords' => 'closed umbrella drizzle rain weather umbrella closed rain moisture protection sun ultraviolet uv'],
+            'heavy_plus_sign' => ['unicode' => '2795', 'shortname' => ':heavy_plus_sign:', 'aliases' => '', 'keywords' => 'heavy plus sign calculation math'],
+            'mega' => ['unicode' => '1F4E3', 'shortname' => ':mega:', 'aliases' => '', 'keywords' => 'cheering megaphone sound speaker volume'],
+            'large_blue_diamond' => ['unicode' => '1F537', 'shortname' => ':large_blue_diamond:', 'aliases' => '', 'keywords' => 'large blue diamond shape'],
+            'package' => ['unicode' => '1F4E6', 'shortname' => ':package:', 'aliases' => '', 'keywords' => 'package gift mail'],
+            'heavy_minus_sign' => ['unicode' => '2796', 'shortname' => ':heavy_minus_sign:', 'aliases' => '', 'keywords' => 'heavy minus sign calculation math'],
+            'city_sunset' => ['unicode' => '1F307', 'shortname' => ':city_sunset:', 'aliases' => ':city_sunrise:', 'keywords' => 'sunset over buildings photo city scape sunrise dawn light morning metropolitan rise sun'],
+            'soon' => ['unicode' => '1F51C', 'shortname' => ':soon:', 'aliases' => '', 'keywords' => 'soon with rightwards arrow above arrow words'],
+            'congratulations' => ['unicode' => '3297', 'shortname' => ':congratulations:', 'aliases' => '', 'keywords' => 'circled ideograph congratulation chinese japanese kanji'],
+            'secret' => ['unicode' => '3299', 'shortname' => ':secret:', 'aliases' => '', 'keywords' => 'circled ideograph secret privacy'],
+            'no_entry' => ['unicode' => '26D4', 'shortname' => ':no_entry:', 'aliases' => '', 'keywords' => 'no entry bad denied limit privacy security stop'],
+            'aries' => ['unicode' => '2648', 'shortname' => ':aries:', 'aliases' => '', 'keywords' => 'aries aries ram astrology greek constellation stars zodiac sign purple-square sign zodiac horoscope'],
+            'purse' => ['unicode' => '1F45B', 'shortname' => ':purse:', 'aliases' => '', 'keywords' => 'purse accessories fashion money purse clutch bag handbag coin bag accessory money ladies shopping'],
+            'dragon_face' => ['unicode' => '1F432', 'shortname' => ':dragon_face:', 'aliases' => '', 'keywords' => 'dragon face animal chinese green myth nature dragon head fire legendary myth'],
+            'leo' => ['unicode' => '264C', 'shortname' => ':leo:', 'aliases' => '', 'keywords' => 'leo leo lion astrology greek constellation stars zodiac sign purple-square sign zodiac horoscope'],
+            'ship' => ['unicode' => '1F6A2', 'shortname' => ':ship:', 'aliases' => '', 'keywords' => 'ship titanic transportation ferry ship boat'],
+            'white_flower' => ['unicode' => '1F4AE', 'shortname' => ':white_flower:', 'aliases' => '', 'keywords' => 'white flower japanese white flower teacher school grade score brilliance intelligence homework student assignment praise'],
+            'id' => ['unicode' => '1F1EE-1F1E9', 'shortname' => ':id:', 'aliases' => '', 'keywords' => 'indonesia country nation'],
+            'wedding' => ['unicode' => '1F492', 'shortname' => ':wedding:', 'aliases' => '', 'keywords' => 'wedding affection bride couple groom like love marriage'],
+            'boot' => ['unicode' => '1F462', 'shortname' => ':boot:', 'aliases' => '', 'keywords' => 'womans boots fashion shoes'],
+            'radio_button' => ['unicode' => '1F518', 'shortname' => ':radio_button:', 'aliases' => '', 'keywords' => 'radio button input'],
+            'notebook' => ['unicode' => '1F4D3', 'shortname' => ':notebook:', 'aliases' => '', 'keywords' => 'notebook notes paper record stationery'],
+            'gemini' => ['unicode' => '264A', 'shortname' => ':gemini:', 'aliases' => '', 'keywords' => 'gemini gemini twins astrology greek constellation stars zodiac sign sign zodiac horoscope'],
+            'bell' => ['unicode' => '1F514', 'shortname' => ':bell:', 'aliases' => '', 'keywords' => 'bell chime christmas notification sound xmas'],
+            'boar' => ['unicode' => '1F417', 'shortname' => ':boar:', 'aliases' => '', 'keywords' => 'boar animal nature'],
+            'ambulance' => ['unicode' => '1F691', 'shortname' => ':ambulance:', 'aliases' => '', 'keywords' => 'ambulance 911 health ambulance emergency medical help assistance'],
+            'mount_fuji' => ['unicode' => '1F5FB', 'shortname' => ':mount_fuji:', 'aliases' => '', 'keywords' => 'mount fuji japan mountain nature photo'],
+            'sandal' => ['unicode' => '1F461', 'shortname' => ':sandal:', 'aliases' => '', 'keywords' => 'womans sandal fashion shoes'],
+            'round_pushpin' => ['unicode' => '1F4CD', 'shortname' => ':round_pushpin:', 'aliases' => '', 'keywords' => 'round pushpin stationery'],
+            'keycap_ten' => ['unicode' => '1F51F', 'shortname' => ':keycap_ten:', 'aliases' => '', 'keywords' => 'keycap ten 10 blue-square numbers'],
+            'ledger' => ['unicode' => '1F4D2', 'shortname' => ':ledger:', 'aliases' => '', 'keywords' => 'ledger notes paper'],
+            'womans_hat' => ['unicode' => '1F452', 'shortname' => ':womans_hat:', 'aliases' => '', 'keywords' => 'womans hat accessories fashion female'],
+            'envelope_with_arrow' => ['unicode' => '1F4E9', 'shortname' => ':envelope_with_arrow:', 'aliases' => '', 'keywords' => 'envelope with downwards arrow above email'],
+            'black_joker' => ['unicode' => '1F0CF', 'shortname' => ':black_joker:', 'aliases' => '', 'keywords' => 'playing card black joker cards game poker'],
+            'part_alternation_mark' => ['unicode' => '303D', 'shortname' => ':part_alternation_mark:', 'aliases' => '', 'keywords' => 'part alternation mark graph sing song vocal music karaoke cue letter m japanese'],
+            'o2' => ['unicode' => '1F17E', 'shortname' => ':o2:', 'aliases' => '', 'keywords' => 'negative squared latin capital letter o alphabet letter red-square'],
+            'office' => ['unicode' => '1F3E2', 'shortname' => ':office:', 'aliases' => '', 'keywords' => 'office building building bureau work'],
+            'volcano' => ['unicode' => '1F30B', 'shortname' => ':volcano:', 'aliases' => '', 'keywords' => 'volcano nature photo volcano lava magma hot explode'],
+            'aquarius' => ['unicode' => '2652', 'shortname' => ':aquarius:', 'aliases' => '', 'keywords' => 'aquarius aquarius water bearer astrology greek constellation stars zodiac sign purple-square sign zodiac horoscope'],
+            'taurus' => ['unicode' => '2649', 'shortname' => ':taurus:', 'aliases' => '', 'keywords' => 'taurus purple-square sign taurus bull astrology greek constellation stars zodiac sign zodiac horoscope'],
+            'pushpin' => ['unicode' => '1F4CC', 'shortname' => ':pushpin:', 'aliases' => '', 'keywords' => 'pushpin stationery'],
+            'violin' => ['unicode' => '1F3BB', 'shortname' => ':violin:', 'aliases' => '', 'keywords' => 'violin instrument music violin fiddle music instrument'],
+            'virgo' => ['unicode' => '264D', 'shortname' => ':virgo:', 'aliases' => '', 'keywords' => 'virgo sign virgo maiden astrology greek constellation stars zodiac sign zodiac horoscope'],
+            'ski' => ['unicode' => '1F3BF', 'shortname' => ':ski:', 'aliases' => '', 'keywords' => 'ski and ski boot cold sports winter ski downhill cross-country poles snow winter mountain alpine powder slalom freestyle'],
+            'taxi' => ['unicode' => '1F695', 'shortname' => ':taxi:', 'aliases' => '', 'keywords' => 'taxi cars transportation uber vehicle taxi car automobile city transport service'],
+            'stars' => ['unicode' => '1F320', 'shortname' => ':stars:', 'aliases' => '', 'keywords' => 'shooting star night photo shooting shoot star sky night comet meteoroid'],
+            'speedboat' => ['unicode' => '1F6A4', 'shortname' => ':speedboat:', 'aliases' => '', 'keywords' => 'speedboat ship transportation vehicle motor speed ski power boat'],
+            'hourglass_flowing_sand' => ['unicode' => '23F3', 'shortname' => ':hourglass_flowing_sand:', 'aliases' => '', 'keywords' => 'hourglass with flowing sand countdown oldschool time'],
+            'ferris_wheel' => ['unicode' => '1F3A1', 'shortname' => ':ferris_wheel:', 'aliases' => '', 'keywords' => 'ferris wheel carnival londoneye photo farris wheel amusement park fair ride entertainment'],
+            'tent' => ['unicode' => '26FA', 'shortname' => ':tent:', 'aliases' => '', 'keywords' => 'tent camp outdoors photo'],
+            'love_hotel' => ['unicode' => '1F3E9', 'shortname' => ':love_hotel:', 'aliases' => '', 'keywords' => 'love hotel affection dating like love hotel love sex romance leisure adultery prostitution hospital birth happy'],
+            'church' => ['unicode' => '26EA', 'shortname' => ':church:', 'aliases' => '', 'keywords' => 'church building christ religion'],
+            'briefcase' => ['unicode' => '1F4BC', 'shortname' => ':briefcase:', 'aliases' => '', 'keywords' => 'briefcase business documents work'],
+            'womans_clothes' => ['unicode' => '1F45A', 'shortname' => ':womans_clothes:', 'aliases' => '', 'keywords' => 'womans clothes fashion woman clothing clothes blouse shirt wardrobe breasts cleavage shopping shop dressing dressed'],
+            'dvd' => ['unicode' => '1F4C0', 'shortname' => ':dvd:', 'aliases' => '', 'keywords' => 'dvd cd disc disk'],
+            'libra' => ['unicode' => '264E', 'shortname' => ':libra:', 'aliases' => '', 'keywords' => 'libra libra scales astrology greek constellation stars zodiac sign purple-square sign zodiac horoscope'],
+            'sagittarius' => ['unicode' => '2650', 'shortname' => ':sagittarius:', 'aliases' => '', 'keywords' => 'sagittarius sagittarius centaur archer astrology greek constellation stars zodiac sign sign zodiac horoscope'],
+            'oden' => ['unicode' => '1F362', 'shortname' => ':oden:', 'aliases' => '', 'keywords' => 'oden food japanese oden seafood casserole stew'],
+            'game_die' => ['unicode' => '1F3B2', 'shortname' => ':game_die:', 'aliases' => '', 'keywords' => 'game die dice game die dice craps gamble play'],
+            'grey_question' => ['unicode' => '2754', 'shortname' => ':grey_question:', 'aliases' => '', 'keywords' => 'white question mark ornament doubts'],
+            'fast_forward' => ['unicode' => '23E9', 'shortname' => ':fast_forward:', 'aliases' => '', 'keywords' => 'black right-pointing double triangle blue-square'],
+            'flashlight' => ['unicode' => '1F526', 'shortname' => ':flashlight:', 'aliases' => '', 'keywords' => 'electric torch dark'],
+            'triangular_flag_on_post' => ['unicode' => '1F6A9', 'shortname' => ':triangular_flag_on_post:', 'aliases' => '', 'keywords' => 'triangular flag on post triangle triangular flag golf post flagpole'],
+            'tanabata_tree' => ['unicode' => '1F38B', 'shortname' => ':tanabata_tree:', 'aliases' => '', 'keywords' => 'tanabata tree nature plant tanabata tree festival star wish holiday'],
+            'dango' => ['unicode' => '1F361', 'shortname' => ':dango:', 'aliases' => '', 'keywords' => 'dango food dango japanese dumpling mochi balls skewer'],
+            'signal_strength' => ['unicode' => '1F4F6', 'shortname' => ':signal_strength:', 'aliases' => '', 'keywords' => 'antenna with bars blue-square'],
+            'video_camera' => ['unicode' => '1F4F9', 'shortname' => ':video_camera:', 'aliases' => '', 'keywords' => 'video camera film record'],
+            'negative_squared_cross_mark' => ['unicode' => '274E', 'shortname' => ':negative_squared_cross_mark:', 'aliases' => '', 'keywords' => 'negative squared cross mark deny green-square no x'],
+            'black_medium_square' => ['unicode' => '25FC', 'shortname' => ':black_medium_square:', 'aliases' => '', 'keywords' => 'black medium square shape'],
+            'yen' => ['unicode' => '1F4B4', 'shortname' => ':yen:', 'aliases' => '', 'keywords' => 'banknote with yen sign currency dollar japanese money yen japan japanese banknote money currency paper cash bill'],
+            'blowfish' => ['unicode' => '1F421', 'shortname' => ':blowfish:', 'aliases' => '', 'keywords' => 'blowfish food nature ocean sea blowfish pufferfish puffer ballonfish toadfish fugu fish sushi'],
+            'white_large_square' => ['unicode' => '2B1C', 'shortname' => ':white_large_square:', 'aliases' => '', 'keywords' => 'white large square shape'],
+            'beginner' => ['unicode' => '1F530', 'shortname' => ':beginner:', 'aliases' => '', 'keywords' => 'japanese symbol for beginner badge shield'],
+            'school' => ['unicode' => '1F3EB', 'shortname' => ':school:', 'aliases' => '', 'keywords' => 'school building school university elementary middle high college teach education'],
+            'new' => ['unicode' => '1F195', 'shortname' => ':new:', 'aliases' => '', 'keywords' => 'squared new blue-square'],
+            'clock1' => ['unicode' => '1F550', 'shortname' => ':clock1:', 'aliases' => '', 'keywords' => 'clock face one oclock clock time'],
+            'womens' => ['unicode' => '1F6BA', 'shortname' => ':womens:', 'aliases' => '', 'keywords' => 'womens symbol purple-square woman bathroom restroom sign girl female avatar'],
+            'running_shirt_with_sash' => ['unicode' => '1F3BD', 'shortname' => ':running_shirt_with_sash:', 'aliases' => '', 'keywords' => 'running shirt with sash pageant play running run shirt cloths compete sports'],
+            'radio' => ['unicode' => '1F4FB', 'shortname' => ':radio:', 'aliases' => '', 'keywords' => 'radio communication music podcast program'],
+            'on' => ['unicode' => '1F51B', 'shortname' => ':on:', 'aliases' => '', 'keywords' => 'on with exclamation mark with left right arrow abo arrow words'],
+            'hourglass' => ['unicode' => '231B', 'shortname' => ':hourglass:', 'aliases' => '', 'keywords' => 'hourglass clock oldschool time'],
+            'pisces' => ['unicode' => '2653', 'shortname' => ':pisces:', 'aliases' => '', 'keywords' => 'pisces pisces fish astrology greek constellation stars zodiac sign purple-square sign zodiac horoscope'],
+            'nut_and_bolt' => ['unicode' => '1F529', 'shortname' => ':nut_and_bolt:', 'aliases' => '', 'keywords' => 'nut and bolt handy tools'],
+            'free' => ['unicode' => '1F193', 'shortname' => ':free:', 'aliases' => '', 'keywords' => 'squared free blue-square words'],
+            'bridge_at_night' => ['unicode' => '1F309', 'shortname' => ':bridge_at_night:', 'aliases' => '', 'keywords' => 'bridge at night photo sanfrancisco bridge night water road evening suspension golden gate'],
+            'saxophone' => ['unicode' => '1F3B7', 'shortname' => ':saxophone:', 'aliases' => '', 'keywords' => 'saxophone instrument music saxophone sax music instrument woodwind'],
+            'white_square_button' => ['unicode' => '1F533', 'shortname' => ':white_square_button:', 'aliases' => '', 'keywords' => 'white square button shape'],
+            'mobile_phone_off' => ['unicode' => '1F4F4', 'shortname' => ':mobile_phone_off:', 'aliases' => '', 'keywords' => 'mobile phone off mute'],
+            'closed_book' => ['unicode' => '1F4D5', 'shortname' => ':closed_book:', 'aliases' => '', 'keywords' => 'closed book knowledge library read'],
+            'european_castle' => ['unicode' => '1F3F0', 'shortname' => ':european_castle:', 'aliases' => '', 'keywords' => 'european castle building history royalty castle european residence royalty disneyland disney fort fortified moat tower princess prince lord king queen fortress nobel stronghold'],
+            'clock12' => ['unicode' => '1F55B', 'shortname' => ':clock12:', 'aliases' => '', 'keywords' => 'clock face twelve oclock clock time'],
+            'white_medium_square' => ['unicode' => '25FB', 'shortname' => ':white_medium_square:', 'aliases' => '', 'keywords' => 'white medium square shape'],
+            'foggy' => ['unicode' => '1F301', 'shortname' => ':foggy:', 'aliases' => '', 'keywords' => 'foggy mountain photo bridge weather fog foggy'],
+            'minidisc' => ['unicode' => '1F4BD', 'shortname' => ':minidisc:', 'aliases' => '', 'keywords' => 'minidisc data disc disk record technology'],
+            'fire_engine' => ['unicode' => '1F692', 'shortname' => ':fire_engine:', 'aliases' => '', 'keywords' => 'fire engine cars transportation vehicle fire fighter engine truck emergency medical'],
+            'clock2' => ['unicode' => '1F551', 'shortname' => ':clock2:', 'aliases' => '', 'keywords' => 'clock face two oclock clock time'],
+            'rice_ball' => ['unicode' => '1F359', 'shortname' => ':rice_ball:', 'aliases' => '', 'keywords' => 'rice ball food japanese rice ball white nori seaweed japanese'],
+            'wind_chime' => ['unicode' => '1F390', 'shortname' => ':wind_chime:', 'aliases' => '', 'keywords' => 'wind chime ding nature wind chime bell fūrin instrument music spirits soothing protective spiritual sound'],
+            'capricorn' => ['unicode' => '2651', 'shortname' => ':capricorn:', 'aliases' => '', 'keywords' => 'capricorn capricorn sea-goat goat-horned astrology greek constellation stars zodiac sign sign zodiac horoscope'],
+            'vs' => ['unicode' => '1F19A', 'shortname' => ':vs:', 'aliases' => '', 'keywords' => 'squared vs orange-square words'],
+            'melon' => ['unicode' => '1F348', 'shortname' => ':melon:', 'aliases' => '', 'keywords' => 'melon food fruit nature melon cantaloupe honeydew'],
+            'trumpet' => ['unicode' => '1F3BA', 'shortname' => ':trumpet:', 'aliases' => '', 'keywords' => 'trumpet brass music trumpet brass music instrument'],
+            'school_satchel' => ['unicode' => '1F392', 'shortname' => ':school_satchel:', 'aliases' => '', 'keywords' => 'school satchel bag education student school satchel backpack bag packing pack hike education adventure travel sightsee'],
+            'tokyo_tower' => ['unicode' => '1F5FC', 'shortname' => ':tokyo_tower:', 'aliases' => '', 'keywords' => 'tokyo tower japan photo'],
+            'station' => ['unicode' => '1F689', 'shortname' => ':station:', 'aliases' => '', 'keywords' => 'station public transportation vehicle station train subway'],
+            'end' => ['unicode' => '1F51A', 'shortname' => ':end:', 'aliases' => '', 'keywords' => 'end with leftwards arrow above arrow words'],
+            'bamboo' => ['unicode' => '1F38D', 'shortname' => ':bamboo:', 'aliases' => '', 'keywords' => 'pine decoration nature plant vegetable pine bamboo decoration new years spirits harvest prosperity longevity fortune luck welcome farming agriculture'],
+            'truck' => ['unicode' => '1F69A', 'shortname' => ':truck:', 'aliases' => '', 'keywords' => 'delivery truck cars transportation truck delivery package'],
+            'clock3' => ['unicode' => '1F552', 'shortname' => ':clock3:', 'aliases' => '', 'keywords' => 'clock face three oclock clock time'],
+            'six_pointed_star' => ['unicode' => '1F52F', 'shortname' => ':six_pointed_star:', 'aliases' => '', 'keywords' => 'six pointed star with middle dot purple-square'],
+            'mag_right' => ['unicode' => '1F50E', 'shortname' => ':mag_right:', 'aliases' => '', 'keywords' => 'right-pointing magnifying glass search zoom detective investigator detail details'],
+            'kimono' => ['unicode' => '1F458', 'shortname' => ':kimono:', 'aliases' => '', 'keywords' => 'kimono dress fashion female japanese women'],
+            'railway_car' => ['unicode' => '1F683', 'shortname' => ':railway_car:', 'aliases' => '', 'keywords' => 'railway car transportation vehicle railway rail car coach train'],
+            'crossed_flags' => ['unicode' => '1F38C', 'shortname' => ':crossed_flags:', 'aliases' => '', 'keywords' => 'crossed flags japan'],
+            'sweet_potato' => ['unicode' => '1F360', 'shortname' => ':sweet_potato:', 'aliases' => '', 'keywords' => 'roasted sweet potato food nature sweet potato potassium roasted roast'],
+            'white_small_square' => ['unicode' => '25AB', 'shortname' => ':white_small_square:', 'aliases' => '', 'keywords' => 'white small square shape'],
+            'date' => ['unicode' => '1F4C5', 'shortname' => ':date:', 'aliases' => '', 'keywords' => 'calendar calendar schedule'],
+            'newspaper' => ['unicode' => '1F4F0', 'shortname' => ':newspaper:', 'aliases' => '', 'keywords' => 'newspaper headline press'],
+            'no_smoking' => ['unicode' => '1F6AD', 'shortname' => ':no_smoking:', 'aliases' => '', 'keywords' => 'no smoking symbol cigarette no smoking cigarette smoke cancer lungs inhale tar nicotine'],
+            'scroll' => ['unicode' => '1F4DC', 'shortname' => ':scroll:', 'aliases' => '', 'keywords' => 'scroll documents'],
+            'flags' => ['unicode' => '1F38F', 'shortname' => ':flags:', 'aliases' => '', 'keywords' => 'carp streamer banner carp fish japanese koinobori children kids boys celebration happiness carp streamers japanese holiday flags'],
+            'mag' => ['unicode' => '1F50D', 'shortname' => ':mag:', 'aliases' => '', 'keywords' => 'left-pointing magnifying glass search zoom detective investigator detail details'],
+            'wheelchair' => ['unicode' => '267F', 'shortname' => ':wheelchair:', 'aliases' => '', 'keywords' => 'wheelchair symbol blue-square disabled'],
+            'sake' => ['unicode' => '1F376', 'shortname' => ':sake:', 'aliases' => '', 'keywords' => 'sake bottle and cup beverage drink drunk wine sake wine rice ferment alcohol japanese drink'],
+            'arrow_up_down' => ['unicode' => '2195', 'shortname' => ':arrow_up_down:', 'aliases' => '', 'keywords' => 'up down arrow blue-square'],
+            'black_large_square' => ['unicode' => '2B1B', 'shortname' => ':black_large_square:', 'aliases' => '', 'keywords' => 'black large square shape'],
+            'wrench' => ['unicode' => '1F527', 'shortname' => ':wrench:', 'aliases' => '', 'keywords' => 'wrench diy ikea tools'],
+            'construction' => ['unicode' => '1F6A7', 'shortname' => ':construction:', 'aliases' => '', 'keywords' => 'construction sign caution progress wip'],
+            'calendar' => ['unicode' => '1F4C6', 'shortname' => ':calendar:', 'aliases' => '', 'keywords' => 'tear-off calendar schedule'],
+            'hotel' => ['unicode' => '1F3E8', 'shortname' => ':hotel:', 'aliases' => '', 'keywords' => 'hotel accomodation building checkin whotel hotel motel holiday inn hospital'],
+            'satellite' => ['unicode' => '1F4E1', 'shortname' => ':satellite:', 'aliases' => '', 'keywords' => 'satellite antenna communication'],
+            'rewind' => ['unicode' => '23EA', 'shortname' => ':rewind:', 'aliases' => '', 'keywords' => 'black left-pointing double triangle blue-square play'],
+            'clock4' => ['unicode' => '1F553', 'shortname' => ':clock4:', 'aliases' => '', 'keywords' => 'clock face four oclock clock time'],
+            'circus_tent' => ['unicode' => '1F3AA', 'shortname' => ':circus_tent:', 'aliases' => '', 'keywords' => 'circus tent carnival festival party circus tent event carnival big top canvas'],
+            'link' => ['unicode' => '1F517', 'shortname' => ':link:', 'aliases' => '', 'keywords' => 'link symbol rings url'],
+            'bullettrain_side' => ['unicode' => '1F684', 'shortname' => ':bullettrain_side:', 'aliases' => '', 'keywords' => 'high-speed train transportation vehicle train bullet rail'],
+            'mens' => ['unicode' => '1F6B9', 'shortname' => ':mens:', 'aliases' => '', 'keywords' => 'mens symbol restroom toilet wc men bathroom restroom sign boy male avatar'],
+            'carousel_horse' => ['unicode' => '1F3A0', 'shortname' => ':carousel_horse:', 'aliases' => '', 'keywords' => 'carousel horse carnival horse photo carousel horse amusement park ride entertainment park fair'],
+            'ideograph_advantage' => ['unicode' => '1F250', 'shortname' => ':ideograph_advantage:', 'aliases' => '', 'keywords' => 'circled ideograph advantage chinese get kanji obtain'],
+            'atm' => ['unicode' => '1F3E7', 'shortname' => ':atm:', 'aliases' => '', 'keywords' => 'automated teller machine atm cash withdrawal money deposit financial bank adam payday bank blue-square cash money payment'],
+            'vhs' => ['unicode' => '1F4FC', 'shortname' => ':vhs:', 'aliases' => '', 'keywords' => 'videocassette oldschool record video'],
+            'arrow_double_down' => ['unicode' => '23EC', 'shortname' => ':arrow_double_down:', 'aliases' => '', 'keywords' => 'black down-pointing double triangle arrow blue-square'],
+            'clock9' => ['unicode' => '1F558', 'shortname' => ':clock9:', 'aliases' => '', 'keywords' => 'clock face nine oclock clock time'],
+            'blue_book' => ['unicode' => '1F4D8', 'shortname' => ':blue_book:', 'aliases' => '', 'keywords' => 'blue book knowledge library read'],
+            'arrow_heading_up' => ['unicode' => '2934', 'shortname' => ':arrow_heading_up:', 'aliases' => '', 'keywords' => 'arrow pointing rightwards then curving upwards arrow blue-square'],
+            'metro' => ['unicode' => '1F687', 'shortname' => ':metro:', 'aliases' => '', 'keywords' => 'metro blue-square mrt transportation tube underground metro subway underground train'],
+            'clock5' => ['unicode' => '1F554', 'shortname' => ':clock5:', 'aliases' => '', 'keywords' => 'clock face five oclock clock time'],
+            'wc' => ['unicode' => '1F6BE', 'shortname' => ':wc:', 'aliases' => '', 'keywords' => 'water closet blue-square restroom toilet water closet toilet bathroom throne porcelain waste flush plumbing'],
+            'chart_with_upwards_trend' => ['unicode' => '1F4C8', 'shortname' => ':chart_with_upwards_trend:', 'aliases' => '', 'keywords' => 'chart with upwards trend graph'],
+            'slot_machine' => ['unicode' => '1F3B0', 'shortname' => ':slot_machine:', 'aliases' => '', 'keywords' => 'slot machine bet gamble vegas slot machine gamble one-armed bandit slots luck'],
+            'rice_cracker' => ['unicode' => '1F358', 'shortname' => ':rice_cracker:', 'aliases' => '', 'keywords' => 'rice cracker food japanese rice cracker seaweed food japanese'],
+            'page_facing_up' => ['unicode' => '1F4C4', 'shortname' => ':page_facing_up:', 'aliases' => '', 'keywords' => 'page facing up documents'],
+            'arrow_up_small' => ['unicode' => '1F53C', 'shortname' => ':arrow_up_small:', 'aliases' => '', 'keywords' => 'up-pointing small red triangle blue-square'],
+            'green_book' => ['unicode' => '1F4D7', 'shortname' => ':green_book:', 'aliases' => '', 'keywords' => 'green book knowledge library read'],
+            'white_medium_small_square' => ['unicode' => '25FD', 'shortname' => ':white_medium_small_square:', 'aliases' => '', 'keywords' => 'white medium small square shape'],
+            'traffic_light' => ['unicode' => '1F6A5', 'shortname' => ':traffic_light:', 'aliases' => '', 'keywords' => 'horizontal traffic light traffic transportation traffic light stop go yield horizontal'],
+            'ng' => ['unicode' => '1F1F3-1F1EC', 'shortname' => ':ng:', 'aliases' => '', 'keywords' => 'nigeria country nation'],
+            'clock10' => ['unicode' => '1F559', 'shortname' => ':clock10:', 'aliases' => '', 'keywords' => 'clock face ten oclock clock time'], 1234 => ['unicode' => '1F522', 'shortname' => ':1234:', 'aliases' => '', 'keywords' => 'input symbol for numbers blue-square numbers'],
+            'convenience_store' => ['unicode' => '1F3EA', 'shortname' => ':convenience_store:', 'aliases' => '', 'keywords' => 'convenience store building'],
+            'paperclip' => ['unicode' => '1F4CE', 'shortname' => ':paperclip:', 'aliases' => '', 'keywords' => 'paperclip documents stationery'],
+            'name_badge' => ['unicode' => '1F4DB', 'shortname' => ':name_badge:', 'aliases' => '', 'keywords' => 'name badge fire forbid'],
+            'clock8' => ['unicode' => '1F557', 'shortname' => ':clock8:', 'aliases' => '', 'keywords' => 'clock face eight oclock clock time'],
+            'arrow_down_small' => ['unicode' => '1F53D', 'shortname' => ':arrow_down_small:', 'aliases' => '', 'keywords' => 'down-pointing small red triangle arrow blue-square'],
+            'clipboard' => ['unicode' => '1F4CB', 'shortname' => ':clipboard:', 'aliases' => '', 'keywords' => 'clipboard documents stationery'],
+            'page_with_curl' => ['unicode' => '1F4C3', 'shortname' => ':page_with_curl:', 'aliases' => '', 'keywords' => 'page with curl documents'],
+            'bookmark_tabs' => ['unicode' => '1F4D1', 'shortname' => ':bookmark_tabs:', 'aliases' => '', 'keywords' => 'bookmark tabs favorite'],
+            'bank' => ['unicode' => '1F3E6', 'shortname' => ':bank:', 'aliases' => '', 'keywords' => 'bank building'],
+            'clock11' => ['unicode' => '1F55A', 'shortname' => ':clock11:', 'aliases' => '', 'keywords' => 'clock face eleven oclock clock time'],
+            'e-mail' => ['unicode' => '1F4E7', 'shortname' => ':e-mail:', 'aliases' => ':email:', 'keywords' => 'e-mail symbol communication inbox'],
+            'chart_with_downwards_trend' => ['unicode' => '1F4C9', 'shortname' => ':chart_with_downwards_trend:', 'aliases' => '', 'keywords' => 'chart with downwards trend graph'],
+            'bullettrain_front' => ['unicode' => '1F685', 'shortname' => ':bullettrain_front:', 'aliases' => '', 'keywords' => 'high-speed train with bullet nose transportation train bullet rail'],
+            'bar_chart' => ['unicode' => '1F4CA', 'shortname' => ':bar_chart:', 'aliases' => '', 'keywords' => 'bar chart graph presentation stats'],
+            'notebook_with_decorative_cover' => ['unicode' => '1F4D4', 'shortname' => ':notebook_with_decorative_cover:', 'aliases' => '', 'keywords' => 'notebook with decorative cover classroom notes paper record'],
+            'ticket' => ['unicode' => '1F3AB', 'shortname' => ':ticket:', 'aliases' => '', 'keywords' => 'ticket concert event pass ticket show entertainment stub admission proof purchase'],
+            'information_source' => ['unicode' => '2139', 'shortname' => ':information_source:', 'aliases' => '', 'keywords' => 'information source alphabet blue-square letter'],
+            'pouch' => ['unicode' => '1F45D', 'shortname' => ':pouch:', 'aliases' => '', 'keywords' => 'pouch accessories bag pouch bag cosmetic packing grandma makeup'],
+            'chart' => ['unicode' => '1F4B9', 'shortname' => ':chart:', 'aliases' => '', 'keywords' => 'chart with upwards trend and yen sign graph green-square'],
+            'japanese_castle' => ['unicode' => '1F3EF', 'shortname' => ':japanese_castle:', 'aliases' => '', 'keywords' => 'japanese castle building photo castle japanese residence royalty fort fortified fortress'],
+            'cinema' => ['unicode' => '1F3A6', 'shortname' => ':cinema:', 'aliases' => '', 'keywords' => 'cinema blue-square film movie record cinema movie theater motion picture'],
+            'clock7' => ['unicode' => '1F556', 'shortname' => ':clock7:', 'aliases' => '', 'keywords' => 'clock face seven oclock clock time'],
+            'orange_book' => ['unicode' => '1F4D9', 'shortname' => ':orange_book:', 'aliases' => '', 'keywords' => 'orange book knowledge library read'],
+            'restroom' => ['unicode' => '1F6BB', 'shortname' => ':restroom:', 'aliases' => '', 'keywords' => 'restroom blue-square woman man unisex bathroom restroom sign shared toilet'],
+            'fountain' => ['unicode' => '26F2', 'shortname' => ':fountain:', 'aliases' => '', 'keywords' => 'fountain photo'],
+            'clock6' => ['unicode' => '1F555', 'shortname' => ':clock6:', 'aliases' => '', 'keywords' => 'clock face six oclock clock time'],
+            'vibration_mode' => ['unicode' => '1F4F3', 'shortname' => ':vibration_mode:', 'aliases' => '', 'keywords' => 'vibration mode orange-square phone'],
+            'ab' => ['unicode' => '1F18E', 'shortname' => ':ab:', 'aliases' => '', 'keywords' => 'negative squared ab alphabet red-square'],
+            'postbox' => ['unicode' => '1F4EE', 'shortname' => ':postbox:', 'aliases' => '', 'keywords' => 'postbox email envelope letter'],
+            'rice_scene' => ['unicode' => '1F391', 'shortname' => ':rice_scene:', 'aliases' => '', 'keywords' => 'moon viewing ceremony photo moon viewing observing otsukimi tsukimi rice scene festival autumn'],
+            'floppy_disk' => ['unicode' => '1F4BE', 'shortname' => ':floppy_disk:', 'aliases' => '', 'keywords' => 'floppy disk oldschool save technology floppy disk storage information computer drive megabyte'],
+            'parking' => ['unicode' => '1F17F', 'shortname' => ':parking:', 'aliases' => '', 'keywords' => 'negative squared latin capital letter p alphabet blue-square cars letter'],
+            'department_store' => ['unicode' => '1F3EC', 'shortname' => ':department_store:', 'aliases' => '', 'keywords' => 'department store building mall shopping department store retail sale merchandise'],
+            'pager' => ['unicode' => '1F4DF', 'shortname' => ':pager:', 'aliases' => '', 'keywords' => 'pager bbcall oldschool'],
+            'currency_exchange' => ['unicode' => '1F4B1', 'shortname' => ':currency_exchange:', 'aliases' => '', 'keywords' => 'currency exchange dollar money travel'],
+            'bookmark' => ['unicode' => '1F516', 'shortname' => ':bookmark:', 'aliases' => '', 'keywords' => 'bookmark favorite'],
+            'triangular_ruler' => ['unicode' => '1F4D0', 'shortname' => ':triangular_ruler:', 'aliases' => '', 'keywords' => 'triangular ruler architect math sketch stationery'],
+            'straight_ruler' => ['unicode' => '1F4CF', 'shortname' => ':straight_ruler:', 'aliases' => '', 'keywords' => 'straight ruler stationery'],
+            'japan' => ['unicode' => '1F5FE', 'shortname' => ':japan:', 'aliases' => '', 'keywords' => 'silhouette of japan nation'],
+            'flower_playing_cards' => ['unicode' => '1F3B4', 'shortname' => ':flower_playing_cards:', 'aliases' => '', 'keywords' => 'flower playing cards playing card flower game august moon special'],
+            'u5272' => ['unicode' => '1F239', 'shortname' => ':u5272:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-5272 chinese cut divide kanji pink'],
+            'fax' => ['unicode' => '1F4E0', 'shortname' => ':fax:', 'aliases' => '', 'keywords' => 'fax machine communication technology'],
+            'izakaya_lantern' => ['unicode' => '1F3EE', 'shortname' => ':izakaya_lantern:', 'aliases' => '', 'keywords' => 'izakaya lantern light izakaya lantern stay drink alcohol bar sake restaurant'],
+            'incoming_envelope' => ['unicode' => '1F4E8', 'shortname' => ':incoming_envelope:', 'aliases' => '', 'keywords' => 'incoming envelope email inbox'],
+            'mailbox' => ['unicode' => '1F4EB', 'shortname' => ':mailbox:', 'aliases' => '', 'keywords' => 'closed mailbox with raised flag communication email inbox'],
+            'lock_with_ink_pen' => ['unicode' => '1F50F', 'shortname' => ':lock_with_ink_pen:', 'aliases' => '', 'keywords' => 'lock with ink pen secret security'],
+            'inbox_tray' => ['unicode' => '1F4E5', 'shortname' => ':inbox_tray:', 'aliases' => '', 'keywords' => 'inbox tray documents email'],
+            'post_office' => ['unicode' => '1F3E3', 'shortname' => ':post_office:', 'aliases' => '', 'keywords' => 'japanese post office building communication email'],
+            'card_index' => ['unicode' => '1F4C7', 'shortname' => ':card_index:', 'aliases' => '', 'keywords' => 'card index business stationery'],
+            'cl' => ['unicode' => '1F1E8-1F1F1', 'shortname' => ':cl:', 'aliases' => '', 'keywords' => 'chile country nation'],
+            'open_file_folder' => ['unicode' => '1F4C2', 'shortname' => ':open_file_folder:', 'aliases' => '', 'keywords' => 'open file folder documents load'],
+            'mahjong' => ['unicode' => '1F004', 'shortname' => ':mahjong:', 'aliases' => '', 'keywords' => 'mahjong tile red dragon chinese game kanji'],
+            'ophiuchus' => ['unicode' => '26CE', 'shortname' => ':ophiuchus:', 'aliases' => '', 'keywords' => 'ophiuchus ophiuchus serpent snake astrology greek constellation stars zodiac purple-square sign horoscope'],
+            'busstop' => ['unicode' => '1F68F', 'shortname' => ':busstop:', 'aliases' => '', 'keywords' => 'bus stop transportation bus stop city transport transportation'],
+            'abc' => ['unicode' => '1F524', 'shortname' => ':abc:', 'aliases' => '', 'keywords' => 'input symbol for latin letters alphabet blue-square'],
+            'u7a7a' => ['unicode' => '1F233', 'shortname' => ':u7a7a:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-7a7a chinese empty japanese kanji'],
+            'capital_abcd' => ['unicode' => '1F520', 'shortname' => ':capital_abcd:', 'aliases' => '', 'keywords' => 'input symbol for latin capital letters alphabet blue-square words'],
+            'factory' => ['unicode' => '1F3ED', 'shortname' => ':factory:', 'aliases' => '', 'keywords' => 'factory building'],
+            'u7981' => ['unicode' => '1F232', 'shortname' => ':u7981:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-7981 chinese forbidden japanese kanji limit restricted'],
+            'u6e80' => ['unicode' => '1F235', 'shortname' => ':u6e80:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-6e80 chinese full japanese kanji red-square'],
+            'heavy_division_sign' => ['unicode' => '2797', 'shortname' => ':heavy_division_sign:', 'aliases' => '', 'keywords' => 'heavy division sign calculation divide math'],
+            'file_folder' => ['unicode' => '1F4C1', 'shortname' => ':file_folder:', 'aliases' => '', 'keywords' => 'file folder documents'],
+            'symbols' => ['unicode' => '1F523', 'shortname' => ':symbols:', 'aliases' => '', 'keywords' => 'input symbol for symbols blue-square'],
+            'arrow_double_up' => ['unicode' => '23EB', 'shortname' => ':arrow_double_up:', 'aliases' => '', 'keywords' => 'black up-pointing double triangle arrow blue-square'],
+            'u5408' => ['unicode' => '1F234', 'shortname' => ':u5408:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-5408 chinese japanese join kanji'],
+            'u6307' => ['unicode' => '1F22F', 'shortname' => ':u6307:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-6307 chinese green-square kanji point'],
+            'abcd' => ['unicode' => '1F521', 'shortname' => ':abcd:', 'aliases' => '', 'keywords' => 'input symbol for latin small letters alphabet blue-square'],
+            'mailbox_closed' => ['unicode' => '1F4EA', 'shortname' => ':mailbox_closed:', 'aliases' => '', 'keywords' => 'closed mailbox with lowered flag communication email inbox'],
+            'outbox_tray' => ['unicode' => '1F4E4', 'shortname' => ':outbox_tray:', 'aliases' => '', 'keywords' => 'outbox tray email inbox'],
+            'sa' => ['unicode' => '1F1F8-1F1E6', 'shortname' => ':sa:', 'aliases' => '', 'keywords' => 'saudi arabia country nation al arabiyah as suudiyah'],
+            'u55b6' => ['unicode' => '1F23A', 'shortname' => ':u55b6:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-55b6 japanese opening hours'],
+            'u6709' => ['unicode' => '1F236', 'shortname' => ':u6709:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-6709 chinese have kanji orange-square'],
+            'accept' => ['unicode' => '1F251', 'shortname' => ':accept:', 'aliases' => '', 'keywords' => 'circled ideograph accept agree chinese good kanji ok yes'],
+            'u7121' => ['unicode' => '1F21A', 'shortname' => ':u7121:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-7121 chinese japanese kanji no nothing orange-square'],
+            'koko' => ['unicode' => '1F201', 'shortname' => ':koko:', 'aliases' => '', 'keywords' => 'squared katakana koko blue-square destination here japanese katakana'],
+            'u7533' => ['unicode' => '1F238', 'shortname' => ':u7533:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-7533 chinese japanese kanji'],
+            'u6708' => ['unicode' => '1F237', 'shortname' => ':u6708:', 'aliases' => '', 'keywords' => 'squared cjk unified ideograph-6708 chinese japanese kanji moon orange-square'],
+            'hash' => ['unicode' => '0023-20E3', 'shortname' => ':hash:', 'aliases' => '', 'keywords' => 'number sign symbol'],
+            'zero' => ['unicode' => '0030-20E3', 'shortname' => ':zero:', 'aliases' => '', 'keywords' => 'digit zero blue-square null numbers'],
+            'one' => ['unicode' => '0031-20E3', 'shortname' => ':one:', 'aliases' => '', 'keywords' => 'digit one 1 blue-square numbers'],
+            'two' => ['unicode' => '0032-20E3', 'shortname' => ':two:', 'aliases' => '', 'keywords' => 'digit two 2 blue-square numbers prime'],
+            'three' => ['unicode' => '0033-20E3', 'shortname' => ':three:', 'aliases' => '', 'keywords' => 'digit three 3 blue-square numbers prime'],
+            'four' => ['unicode' => '0034-20E3', 'shortname' => ':four:', 'aliases' => '', 'keywords' => 'digit four 4 blue-square numbers'],
+            'five' => ['unicode' => '0035-20E3', 'shortname' => ':five:', 'aliases' => '', 'keywords' => 'digit five blue-square numbers prime'],
+            'speaker' => ['unicode' => '1F508', 'shortname' => ':speaker:', 'aliases' => '', 'keywords' => 'speaker sound listen hear noise'],
+            'six' => ['unicode' => '0036-20E3', 'shortname' => ':six:', 'aliases' => '', 'keywords' => 'digit six 6 blue-square numbers'],
+            'train' => ['unicode' => '1F68B', 'shortname' => ':train:', 'aliases' => '', 'keywords' => 'Tram Car tram rail'],
+            'loop' => ['unicode' => '27BF', 'shortname' => ':loop:', 'aliases' => '', 'keywords' => 'double curly loop curly'],
+            'seven' => ['unicode' => '0037-20E3', 'shortname' => ':seven:', 'aliases' => '', 'keywords' => 'digit seven 7 blue-square numbers prime'],
+            'af' => ['unicode' => '1F1E6-1F1EB', 'shortname' => ':af:', 'aliases' => '', 'keywords' => 'afghanistan country nation afghanestan'],
+            'al' => ['unicode' => '1F1E6-1F1F1', 'shortname' => ':al:', 'aliases' => '', 'keywords' => 'albania country nation shqiperia'],
+            'eight' => ['unicode' => '0038-20E3', 'shortname' => ':eight:', 'aliases' => '', 'keywords' => 'digit eight 8 blue-square numbers'],
+            'dz' => ['unicode' => '1F1E9-1F1FF', 'shortname' => ':dz:', 'aliases' => '', 'keywords' => 'algeria country nation al jaza\'ir al jazair'],
+            'ad' => ['unicode' => '1F1E6-1F1E9', 'shortname' => ':ad:', 'aliases' => '', 'keywords' => 'andorra country nation'],
+            'nine' => ['unicode' => '0039-20E3', 'shortname' => ':nine:', 'aliases' => '', 'keywords' => 'digit nine 9 blue-square numbers'],
+            'ao' => ['unicode' => '1F1E6-1F1F4', 'shortname' => ':ao:', 'aliases' => '', 'keywords' => 'angola country nation'],
+            'ag' => ['unicode' => '1F1E6-1F1EC', 'shortname' => ':ag:', 'aliases' => '', 'keywords' => 'antigua and barbuda country nation'],
+            'ar' => ['unicode' => '1F1E6-1F1F7', 'shortname' => ':ar:', 'aliases' => '', 'keywords' => 'argentina country nation'],
+            'am' => ['unicode' => '1F1E6-1F1F2', 'shortname' => ':am:', 'aliases' => '', 'keywords' => 'armenia country nation hayastan'],
+            'au' => ['unicode' => '1F1E6-1F1FA', 'shortname' => ':au:', 'aliases' => '', 'keywords' => 'australia country nation'],
+            'at' => ['unicode' => '1F1E6-1F1F9', 'shortname' => ':at:', 'aliases' => '', 'keywords' => 'austria country nation österreich osterreich'],
+            'az' => ['unicode' => '1F1E6-1F1FF', 'shortname' => ':az:', 'aliases' => '', 'keywords' => 'azerbaijan country nation azarbaycan'],
+            'bs' => ['unicode' => '1F1E7-1F1F8', 'shortname' => ':bs:', 'aliases' => '', 'keywords' => 'the bahamas country nation'],
+            'bh' => ['unicode' => '1F1E7-1F1ED', 'shortname' => ':bh:', 'aliases' => '', 'keywords' => 'bahrain country nation al bahrayn'],
+            'bd' => ['unicode' => '1F1E7-1F1E9', 'shortname' => ':bd:', 'aliases' => '', 'keywords' => 'bangladesh country nation'],
+            'bb' => ['unicode' => '1F1E7-1F1E7', 'shortname' => ':bb:', 'aliases' => '', 'keywords' => 'barbados country nation'],
+            'by' => ['unicode' => '1F1E7-1F1FE', 'shortname' => ':by:', 'aliases' => '', 'keywords' => 'belarus country nation byelarus'],
+            'be' => ['unicode' => '1F1E7-1F1EA', 'shortname' => ':be:', 'aliases' => '', 'keywords' => 'belgium country nation belgique belgie'],
+            'bz' => ['unicode' => '1F1E7-1F1FF', 'shortname' => ':bz:', 'aliases' => '', 'keywords' => 'belize country nation'],
+            'bj' => ['unicode' => '1F1E7-1F1EF', 'shortname' => ':bj:', 'aliases' => '', 'keywords' => 'benin country nation'],
+            'bt' => ['unicode' => '1F1E7-1F1F9', 'shortname' => ':bt:', 'aliases' => '', 'keywords' => 'bhutan country nation'],
+            'bo' => ['unicode' => '1F1E7-1F1F4', 'shortname' => ':bo:', 'aliases' => '', 'keywords' => 'bolivia country nation'],
+            'ba' => ['unicode' => '1F1E7-1F1E6', 'shortname' => ':ba:', 'aliases' => '', 'keywords' => 'bosnia and herzegovina country nation bosna i hercegovina'],
+            'bw' => ['unicode' => '1F1E7-1F1FC', 'shortname' => ':bw:', 'aliases' => '', 'keywords' => 'botswana country nation'],
+            'br' => ['unicode' => '1F1E7-1F1F7', 'shortname' => ':br:', 'aliases' => '', 'keywords' => 'brazil country nation brasil'],
+            'bn' => ['unicode' => '1F1E7-1F1F3', 'shortname' => ':bn:', 'aliases' => '', 'keywords' => 'brunei country nation'],
+            'bg' => ['unicode' => '1F1E7-1F1EC', 'shortname' => ':bg:', 'aliases' => '', 'keywords' => 'bulgaria country nation'],
+            'bf' => ['unicode' => '1F1E7-1F1EB', 'shortname' => ':bf:', 'aliases' => '', 'keywords' => 'burkina faso country nation'],
+            'bi' => ['unicode' => '1F1E7-1F1EE', 'shortname' => ':bi:', 'aliases' => '', 'keywords' => 'burundi country nation'],
+            'kh' => ['unicode' => '1F1F0-1F1ED', 'shortname' => ':kh:', 'aliases' => '', 'keywords' => 'cambodia country nation kampuchea'],
+            'cm' => ['unicode' => '1F1E8-1F1F2', 'shortname' => ':cm:', 'aliases' => '', 'keywords' => 'cameroon country nation'],
+            'ca' => ['unicode' => '1F1E8-1F1E6', 'shortname' => ':ca:', 'aliases' => '', 'keywords' => 'canada country nation'],
+            'cv' => ['unicode' => '1F1E8-1F1FB', 'shortname' => ':cv:', 'aliases' => '', 'keywords' => 'cape verde country nation cabo verde'],
+            'cf' => ['unicode' => '1F1E8-1F1EB', 'shortname' => ':cf:', 'aliases' => '', 'keywords' => 'central african republic country nation'],
+            'td' => ['unicode' => '1F1F9-1F1E9', 'shortname' => ':td:', 'aliases' => '', 'keywords' => 'chad country nation tchad'],
+            'co' => ['unicode' => '1F1E8-1F1F4', 'shortname' => ':co:', 'aliases' => '', 'keywords' => 'colombia country nation'],
+            'km' => ['unicode' => '1F1F0-1F1F2', 'shortname' => ':km:', 'aliases' => '', 'keywords' => 'the comoros country nation'],
+            'cr' => ['unicode' => '1F1E8-1F1F7', 'shortname' => ':cr:', 'aliases' => '', 'keywords' => 'costa rica country nation'],
+            'ci' => ['unicode' => '1F1E8-1F1EE', 'shortname' => ':ci:', 'aliases' => '', 'keywords' => 'cote d\'ivoire country nation'],
+            'hr' => ['unicode' => '1F1ED-1F1F7', 'shortname' => ':hr:', 'aliases' => '', 'keywords' => 'croatia country nation hrvatska'],
+            'cu' => ['unicode' => '1F1E8-1F1FA', 'shortname' => ':cu:', 'aliases' => '', 'keywords' => 'cuba country nation'],
+            'cy' => ['unicode' => '1F1E8-1F1FE', 'shortname' => ':cy:', 'aliases' => '', 'keywords' => 'cyprus country nation kibris kypros'],
+            'cz' => ['unicode' => '1F1E8-1F1FF', 'shortname' => ':cz:', 'aliases' => '', 'keywords' => 'the czech republic country nation ceska republika'],
+            'dk' => ['unicode' => '1F1E9-1F1F0', 'shortname' => ':dk:', 'aliases' => '', 'keywords' => 'denmark country nation danmark'],
+            'dj' => ['unicode' => '1F1E9-1F1EF', 'shortname' => ':dj:', 'aliases' => '', 'keywords' => 'djibouti country nation'],
+            'dm' => ['unicode' => '1F1E9-1F1F2', 'shortname' => ':dm:', 'aliases' => '', 'keywords' => 'dominica country nation'],
+            'do' => ['unicode' => '1F1E9-1F1F4', 'shortname' => ':do:', 'aliases' => '', 'keywords' => 'the dominican republic country nation'],
+            'tl' => ['unicode' => '1F1F9-1F1F1', 'shortname' => ':tl:', 'aliases' => '', 'keywords' => 'east timor country nation'],
+            'ec' => ['unicode' => '1F1EA-1F1E8', 'shortname' => ':ec:', 'aliases' => '', 'keywords' => 'ecuador country nation'],
+            'eg' => ['unicode' => '1F1EA-1F1EC', 'shortname' => ':eg:', 'aliases' => '', 'keywords' => 'egypt country nation misr'],
+            'sv' => ['unicode' => '1F1F8-1F1FB', 'shortname' => ':sv:', 'aliases' => '', 'keywords' => 'el salvador country nation'],
+            'gq' => ['unicode' => '1F1EC-1F1F6', 'shortname' => ':gq:', 'aliases' => '', 'keywords' => 'equatorial guinea country nation guinea ecuatorial'],
+            'er' => ['unicode' => '1F1EA-1F1F7', 'shortname' => ':er:', 'aliases' => '', 'keywords' => 'eritrea country nation hagere ertra'],
+            'ee' => ['unicode' => '1F1EA-1F1EA', 'shortname' => ':ee:', 'aliases' => '', 'keywords' => 'estonia country nation eesti vabariik'],
+            'et' => ['unicode' => '1F1EA-1F1F9', 'shortname' => ':et:', 'aliases' => '', 'keywords' => 'ethiopia country nation ityop\'iya ityopiya'],
+            'fj' => ['unicode' => '1F1EB-1F1EF', 'shortname' => ':fj:', 'aliases' => '', 'keywords' => 'fiji country nation'],
+            'fi' => ['unicode' => '1F1EB-1F1EE', 'shortname' => ':fi:', 'aliases' => '', 'keywords' => 'finland country nation suomen tasavalta'],
+            'ga' => ['unicode' => '1F1EC-1F1E6', 'shortname' => ':ga:', 'aliases' => '', 'keywords' => 'gabon country nation'],
+            'gm' => ['unicode' => '1F1EC-1F1F2', 'shortname' => ':gm:', 'aliases' => '', 'keywords' => 'the gambia country nation'],
+            'ge' => ['unicode' => '1F1EC-1F1EA', 'shortname' => ':ge:', 'aliases' => '', 'keywords' => 'georgia country nation sak\'art\'velo sakartvelo'],
+            'gh' => ['unicode' => '1F1EC-1F1ED', 'shortname' => ':gh:', 'aliases' => '', 'keywords' => 'ghana country nation'],
+            'gr' => ['unicode' => '1F1EC-1F1F7', 'shortname' => ':gr:', 'aliases' => '', 'keywords' => 'greece country nation ellas ellada'],
+            'gd' => ['unicode' => '1F1EC-1F1E9', 'shortname' => ':gd:', 'aliases' => '', 'keywords' => 'grenada country nation'],
+            'gt' => ['unicode' => '1F1EC-1F1F9', 'shortname' => ':gt:', 'aliases' => '', 'keywords' => 'guatemala country nation'],
+            'gn' => ['unicode' => '1F1EC-1F1F3', 'shortname' => ':gn:', 'aliases' => '', 'keywords' => 'guinea country nation guinee'],
+            'gw' => ['unicode' => '1F1EC-1F1FC', 'shortname' => ':gw:', 'aliases' => '', 'keywords' => 'guinea-bissau country nation guine-bissau guine bissau'],
+            'gy' => ['unicode' => '1F1EC-1F1FE', 'shortname' => ':gy:', 'aliases' => '', 'keywords' => 'guyana country nation'],
+            'ht' => ['unicode' => '1F1ED-1F1F9', 'shortname' => ':ht:', 'aliases' => '', 'keywords' => 'haiti country nation'],
+            'hn' => ['unicode' => '1F1ED-1F1F3', 'shortname' => ':hn:', 'aliases' => '', 'keywords' => 'honduras country nation'],
+            'hu' => ['unicode' => '1F1ED-1F1FA', 'shortname' => ':hu:', 'aliases' => '', 'keywords' => 'hungary country nation magyarorszag'],
+            'is' => ['unicode' => '1F1EE-1F1F8', 'shortname' => ':is:', 'aliases' => '', 'keywords' => 'iceland country nation lyoveldio island'],
+            'in' => ['unicode' => '1F1EE-1F1F3', 'shortname' => ':in:', 'aliases' => '', 'keywords' => 'india country nation bharat'],
+            'ir' => ['unicode' => '1F1EE-1F1F7', 'shortname' => ':ir:', 'aliases' => '', 'keywords' => 'iran country nation'],
+            'iq' => ['unicode' => '1F1EE-1F1F6', 'shortname' => ':iq:', 'aliases' => '', 'keywords' => 'iraq country nation'],
+            'ie' => ['unicode' => '1F1EE-1F1EA', 'shortname' => ':ie:', 'aliases' => '', 'keywords' => 'ireland country nation éire eire'],
+            'il' => ['unicode' => '1F1EE-1F1F1', 'shortname' => ':il:', 'aliases' => '', 'keywords' => 'israel country nation yisra\'el yisrael'],
+            'jm' => ['unicode' => '1F1EF-1F1F2', 'shortname' => ':jm:', 'aliases' => '', 'keywords' => 'jamaica country nation'],
+            'jo' => ['unicode' => '1F1EF-1F1F4', 'shortname' => ':jo:', 'aliases' => '', 'keywords' => 'jordan country nation al urdun'],
+            'kz' => ['unicode' => '1F1F0-1F1FF', 'shortname' => ':kz:', 'aliases' => '', 'keywords' => 'kazakhstan country nation qazaqstan'],
+            'ke' => ['unicode' => '1F1F0-1F1EA', 'shortname' => ':ke:', 'aliases' => '', 'keywords' => 'kenya country nation'],
+            'ki' => ['unicode' => '1F1F0-1F1EE', 'shortname' => ':ki:', 'aliases' => '', 'keywords' => 'kiribati country nation kiribati kiribas'],
+            'xk' => ['unicode' => '1F1FD-1F1F0', 'shortname' => ':xk:', 'aliases' => '', 'keywords' => 'kosovo country nation'],
+            'kw' => ['unicode' => '1F1F0-1F1FC', 'shortname' => ':kw:', 'aliases' => '', 'keywords' => 'kuwait country nation al kuwayt'],
+            'cn' => ['unicode' => '1F1E8-1F1F3', 'shortname' => ':cn:', 'aliases' => '', 'keywords' => 'china china chinese prc zhong guo'],
+            'kg' => ['unicode' => '1F1F0-1F1EC', 'shortname' => ':kg:', 'aliases' => '', 'keywords' => 'kyrgyzstan country nation kyrgyz respublikasy'],
+            'la' => ['unicode' => '1F1F1-1F1E6', 'shortname' => ':la:', 'aliases' => '', 'keywords' => 'laos country nation'],
+            'lv' => ['unicode' => '1F1F1-1F1FB', 'shortname' => ':lv:', 'aliases' => '', 'keywords' => 'latvia country nation latvija'],
+            'de' => ['unicode' => '1F1E9-1F1EA', 'shortname' => ':de:', 'aliases' => '', 'keywords' => 'germany flag german nation deutschland'],
+            'lb' => ['unicode' => '1F1F1-1F1E7', 'shortname' => ':lb:', 'aliases' => '', 'keywords' => 'lebanon country nation lubnan'],
+            'ls' => ['unicode' => '1F1F1-1F1F8', 'shortname' => ':ls:', 'aliases' => '', 'keywords' => 'lesotho country nation'],
+            'lr' => ['unicode' => '1F1F1-1F1F7', 'shortname' => ':lr:', 'aliases' => '', 'keywords' => 'liberia country nation'],
+            'es' => ['unicode' => '1F1EA-1F1F8', 'shortname' => ':es:', 'aliases' => '', 'keywords' => 'spain flag nation spain españa'],
+            'ly' => ['unicode' => '1F1F1-1F1FE', 'shortname' => ':ly:', 'aliases' => '', 'keywords' => 'libya country nation libiyah'],
+            'li' => ['unicode' => '1F1F1-1F1EE', 'shortname' => ':li:', 'aliases' => '', 'keywords' => 'liechtenstein country nation'],
+            'lt' => ['unicode' => '1F1F1-1F1F9', 'shortname' => ':lt:', 'aliases' => '', 'keywords' => 'lithuania country nation lietuva'],
+            'fr' => ['unicode' => '1F1EB-1F1F7', 'shortname' => ':fr:', 'aliases' => '', 'keywords' => 'france banner flag france french nation'],
+            'lu' => ['unicode' => '1F1F1-1F1FA', 'shortname' => ':lu:', 'aliases' => '', 'keywords' => 'luxembourg country nation luxembourg letzebuerg'],
+            'mk' => ['unicode' => '1F1F2-1F1F0', 'shortname' => ':mk:', 'aliases' => '', 'keywords' => 'macedonia country nation'],
+            'mg' => ['unicode' => '1F1F2-1F1EC', 'shortname' => ':mg:', 'aliases' => '', 'keywords' => 'madagascar country nation'],
+            'gb' => ['unicode' => '1F1EC-1F1E7', 'shortname' => ':gb:', 'aliases' => '', 'keywords' => 'great britain UK banner britsh flag nation united kingdom england'],
+            'mw' => ['unicode' => '1F1F2-1F1FC', 'shortname' => ':mw:', 'aliases' => '', 'keywords' => 'malawi country nation'],
+            'my' => ['unicode' => '1F1F2-1F1FE', 'shortname' => ':my:', 'aliases' => '', 'keywords' => 'malaysia country nation'],
+            'mv' => ['unicode' => '1F1F2-1F1FB', 'shortname' => ':mv:', 'aliases' => '', 'keywords' => 'maldives country nation dhivehi raajje'],
+            'it' => ['unicode' => '1F1EE-1F1F9', 'shortname' => ':it:', 'aliases' => '', 'keywords' => 'italy flag italy italia'],
+            'ml' => ['unicode' => '1F1F2-1F1F1', 'shortname' => ':ml:', 'aliases' => '', 'keywords' => 'mali country nation'],
+            'mt' => ['unicode' => '1F1F2-1F1F9', 'shortname' => ':mt:', 'aliases' => '', 'keywords' => 'malta country nation'],
+            'mh' => ['unicode' => '1F1F2-1F1ED', 'shortname' => ':mh:', 'aliases' => '', 'keywords' => 'the marshall islands country nation'],
+            'jp' => ['unicode' => '1F1EF-1F1F5', 'shortname' => ':jp:', 'aliases' => '', 'keywords' => 'japan flag japan nation nippon'],
+            'mr' => ['unicode' => '1F1F2-1F1F7', 'shortname' => ':mr:', 'aliases' => '', 'keywords' => 'mauritania country nation muritaniyah'],
+            'mu' => ['unicode' => '1F1F2-1F1FA', 'shortname' => ':mu:', 'aliases' => '', 'keywords' => 'mauritius country nation'],
+            'mx' => ['unicode' => '1F1F2-1F1FD', 'shortname' => ':mx:', 'aliases' => '', 'keywords' => 'mexico country nation'],
+            'kr' => ['unicode' => '1F1F0-1F1F7', 'shortname' => ':kr:', 'aliases' => '', 'keywords' => 'korea flag korea nation'],
+            'fm' => ['unicode' => '1F1EB-1F1F2', 'shortname' => ':fm:', 'aliases' => '', 'keywords' => 'micronesia country nation'],
+            'md' => ['unicode' => '1F1F2-1F1E9', 'shortname' => ':md:', 'aliases' => '', 'keywords' => 'moldova country nation'],
+            'mc' => ['unicode' => '1F1F2-1F1E8', 'shortname' => ':mc:', 'aliases' => '', 'keywords' => 'monaco country nation'],
+            'us' => ['unicode' => '1F1FA-1F1F8', 'shortname' => ':us:', 'aliases' => '', 'keywords' => 'united states american banner country flag nation usa united states of america america old glory'],
+            'grinning' => ['unicode' => '1F600', 'shortname' => ':grinning:', 'aliases' => '', 'keywords' => 'grinning face face happy joy smile grin grinning smiling smile smiley'],
+            'mn' => ['unicode' => '1F1F2-1F1F3', 'shortname' => ':mn:', 'aliases' => '', 'keywords' => 'mongolia country nation mongol uls'],
+            'innocent' => ['unicode' => '1F607', 'shortname' => ':innocent:', 'aliases' => '', 'keywords' => 'smiling face with halo angel face halo halo angel innocent ring circle heaven'],
+            'me' => ['unicode' => '1F1F2-1F1EA', 'shortname' => ':me:', 'aliases' => '', 'keywords' => 'montenegro country nation crna gora'],
+            'smiling_imp' => ['unicode' => '1F608', 'shortname' => ':smiling_imp:', 'aliases' => '', 'keywords' => 'smiling face with horns devil horns horns devil impish trouble'],
+            'ma' => ['unicode' => '1F1F2-1F1E6', 'shortname' => ':ma:', 'aliases' => '', 'keywords' => 'morocco country nation al maghrib'],
+            'ru' => ['unicode' => '1F1F7-1F1FA', 'shortname' => ':ru:', 'aliases' => '', 'keywords' => 'russia banner flag nation russian'],
+            'sunglasses' => ['unicode' => '1F60E', 'shortname' => ':sunglasses:', 'aliases' => '', 'keywords' => 'smiling face with sunglasses cool face smiling sunglasses sun glasses sunny cool smooth'],
+            'mz' => ['unicode' => '1F1F2-1F1FF', 'shortname' => ':mz:', 'aliases' => '', 'keywords' => 'mozambique country nation mocambique'],
+            'neutral_face' => ['unicode' => '1F610', 'shortname' => ':neutral_face:', 'aliases' => '', 'keywords' => 'neutral face face indifference neutral objective impartial blank'],
+            'mm' => ['unicode' => '1F1F2-1F1F2', 'shortname' => ':mm:', 'aliases' => '', 'keywords' => 'myanmar country nation myanma naingngandaw'],
+            'expressionless' => ['unicode' => '1F611', 'shortname' => ':expressionless:', 'aliases' => '', 'keywords' => 'expressionless face expressionless blank void vapid without expression face indifferent'],
+            'na' => ['unicode' => '1F1F3-1F1E6', 'shortname' => ':na:', 'aliases' => '', 'keywords' => 'namibia country nation'],
+            'confused' => ['unicode' => '1F615', 'shortname' => ':confused:', 'aliases' => '', 'keywords' => 'confused face confused confuse daze perplex puzzle indifference skeptical undecided uneasy hesitant'],
+            'nr' => ['unicode' => '1F1F3-1F1F7', 'shortname' => ':nr:', 'aliases' => '', 'keywords' => 'nauru country nation'],
+            'kissing' => ['unicode' => '1F617', 'shortname' => ':kissing:', 'aliases' => '', 'keywords' => 'kissing face 3 face infatuation like love valentines kissing kiss pucker lips smooch'],
+            'np' => ['unicode' => '1F1F3-1F1F5', 'shortname' => ':np:', 'aliases' => '', 'keywords' => 'nepal country nation'],
+            'kissing_smiling_eyes' => ['unicode' => '1F619', 'shortname' => ':kissing_smiling_eyes:', 'aliases' => '', 'keywords' => 'kissing face with smiling eyes affection face infatuation valentines kissing kiss smile pucker lips smooch'],
+            'nl' => ['unicode' => '1F1F3-1F1F1', 'shortname' => ':nl:', 'aliases' => '', 'keywords' => 'the netherlands country nation nederland holland'],
+            'stuck_out_tongue' => ['unicode' => '1F61B', 'shortname' => ':stuck_out_tongue:', 'aliases' => '', 'keywords' => 'face with stuck-out tongue childish face mischievous playful prank tongue silly playful cheeky'],
+            'nz' => ['unicode' => '1F1F3-1F1FF', 'shortname' => ':nz:', 'aliases' => '', 'keywords' => 'new zealand country nation aotearoa'],
+            'worried' => ['unicode' => '1F61F', 'shortname' => ':worried:', 'aliases' => '', 'keywords' => 'worried face concern face nervous worried anxious distressed nervous tense'],
+            'ni' => ['unicode' => '1F1F3-1F1EE', 'shortname' => ':ni:', 'aliases' => '', 'keywords' => 'nicaragua country nation'],
+            'frowning' => ['unicode' => '1F626', 'shortname' => ':frowning:', 'aliases' => ':anguished:', 'keywords' => 'frowning face with open mouth aw face frown sad pout sulk glower'],
+            'ne' => ['unicode' => '1F1F3-1F1EA', 'shortname' => ':ne:', 'aliases' => '', 'keywords' => 'niger country nation'],
+            'anguished' => ['unicode' => '1F627', 'shortname' => ':anguished:', 'aliases' => '', 'keywords' => 'anguished face face nervous stunned pain anguish ouch misery distress grief'],
+            'grimacing' => ['unicode' => '1F62C', 'shortname' => ':grimacing:', 'aliases' => '', 'keywords' => 'grimacing face face grimace teeth grimace disapprove pain'],
+            'kp' => ['unicode' => '1F1F0-1F1F5', 'shortname' => ':kp:', 'aliases' => '', 'keywords' => 'north korea country nation'],
+            'open_mouth' => ['unicode' => '1F62E', 'shortname' => ':open_mouth:', 'aliases' => '', 'keywords' => 'face with open mouth face impressed mouth open jaw gapping surprise wow'],
+            'no' => ['unicode' => '1F1F3-1F1F4', 'shortname' => ':no:', 'aliases' => '', 'keywords' => 'norway country nation norge'],
+            'hushed' => ['unicode' => '1F62F', 'shortname' => ':hushed:', 'aliases' => '', 'keywords' => 'hushed face face woo quiet hush whisper silent'],
+            'om' => ['unicode' => '1F1F4-1F1F2', 'shortname' => ':om:', 'aliases' => '', 'keywords' => 'oman country nation saltanat uman'],
+            'sleeping' => ['unicode' => '1F634', 'shortname' => ':sleeping:', 'aliases' => '', 'keywords' => 'sleeping face face sleepy tired sleep sleepy sleeping snore'],
+            'pk' => ['unicode' => '1F1F5-1F1F0', 'shortname' => ':pk:', 'aliases' => '', 'keywords' => 'pakistan country nation'],
+            'no_mouth' => ['unicode' => '1F636', 'shortname' => ':no_mouth:', 'aliases' => '', 'keywords' => 'face without mouth face hellokitty mouth silent vapid'],
+            'pw' => ['unicode' => '1F1F5-1F1FC', 'shortname' => ':pw:', 'aliases' => '', 'keywords' => 'palau country nation belau'],
+            'helicopter' => ['unicode' => '1F681', 'shortname' => ':helicopter:', 'aliases' => '', 'keywords' => 'helicopter transportation vehicle helicopter helo gyro gyrocopter'],
+            'pa' => ['unicode' => '1F1F5-1F1E6', 'shortname' => ':pa:', 'aliases' => '', 'keywords' => 'panama country nation'],
+            'steam_locomotive' => ['unicode' => '1F682', 'shortname' => ':steam_locomotive:', 'aliases' => '', 'keywords' => 'steam locomotive train transportation vehicle locomotive steam train engine'],
+            'pg' => ['unicode' => '1F1F5-1F1EC', 'shortname' => ':pg:', 'aliases' => '', 'keywords' => 'papua new guinea country nation papua niu gini'],
+            'train2' => ['unicode' => '1F686', 'shortname' => ':train2:', 'aliases' => '', 'keywords' => 'train transportation vehicle train locomotive rail'],
+            'py' => ['unicode' => '1F1F5-1F1FE', 'shortname' => ':py:', 'aliases' => '', 'keywords' => 'paraguay country nation'],
+            'light_rail' => ['unicode' => '1F688', 'shortname' => ':light_rail:', 'aliases' => '', 'keywords' => 'light rail transportation vehicle train rail light'],
+            'tram' => ['unicode' => '1F68A', 'shortname' => ':tram:', 'aliases' => '', 'keywords' => 'tram transportation vehicle tram transportation transport'],
+            'pe' => ['unicode' => '1F1F5-1F1EA', 'shortname' => ':pe:', 'aliases' => '', 'keywords' => 'peru country nation'],
+            'oncoming_bus' => ['unicode' => '1F68D', 'shortname' => ':oncoming_bus:', 'aliases' => '', 'keywords' => 'oncoming bus transportation vehicle bus school city transportation public'],
+            'ph' => ['unicode' => '1F1F5-1F1ED', 'shortname' => ':ph:', 'aliases' => '', 'keywords' => 'the philippines country nation pilipinas'],
+            'trolleybus' => ['unicode' => '1F68E', 'shortname' => ':trolleybus:', 'aliases' => '', 'keywords' => 'trolleybus bart transportation vehicle trolley bus city transport transportation'],
+            'pl' => ['unicode' => '1F1F5-1F1F1', 'shortname' => ':pl:', 'aliases' => '', 'keywords' => 'poland country nation polska'],
+            'minibus' => ['unicode' => '1F690', 'shortname' => ':minibus:', 'aliases' => '', 'keywords' => 'minibus car transportation vehicle bus city transport transportation'],
+            'pt' => ['unicode' => '1F1F5-1F1F9', 'shortname' => ':pt:', 'aliases' => '', 'keywords' => 'portugal country nation'],
+            'oncoming_police_car' => ['unicode' => '1F694', 'shortname' => ':oncoming_police_car:', 'aliases' => '', 'keywords' => 'oncoming police car enforcement law vehicle police car emergency ticket citation crime help officer'],
+            'qa' => ['unicode' => '1F1F6-1F1E6', 'shortname' => ':qa:', 'aliases' => '', 'keywords' => 'qatar country nation dawlat qatar'],
+            'oncoming_taxi' => ['unicode' => '1F696', 'shortname' => ':oncoming_taxi:', 'aliases' => '', 'keywords' => 'oncoming taxi cars uber vehicle taxi car automobile city transport service'],
+            'tw' => ['unicode' => '1F1F9-1F1FC', 'shortname' => ':tw:', 'aliases' => '', 'keywords' => 'the republic of china country nation'],
+            'oncoming_automobile' => ['unicode' => '1F698', 'shortname' => ':oncoming_automobile:', 'aliases' => '', 'keywords' => 'oncoming automobile car transportation vehicle sedan car automobile'],
+            'cg' => ['unicode' => '1F1E8-1F1EC', 'shortname' => ':cg:', 'aliases' => '', 'keywords' => 'the republic of the congo country nation'],
+            'articulated_lorry' => ['unicode' => '1F69B', 'shortname' => ':articulated_lorry:', 'aliases' => '', 'keywords' => 'articulated lorry cars transportation vehicle truck delivery semi lorry articulated'],
+            'ro' => ['unicode' => '1F1F7-1F1F4', 'shortname' => ':ro:', 'aliases' => '', 'keywords' => 'romania country nation'],
+            'tractor' => ['unicode' => '1F69C', 'shortname' => ':tractor:', 'aliases' => '', 'keywords' => 'tractor agriculture car farming vehicle tractor farm construction machine digger'],
+            'monorail' => ['unicode' => '1F69D', 'shortname' => ':monorail:', 'aliases' => '', 'keywords' => 'monorail transportation vehicle train mono rail transport'],
+            'rw' => ['unicode' => '1F1F7-1F1FC', 'shortname' => ':rw:', 'aliases' => '', 'keywords' => 'rwanda country nation'],
+            'mountain_railway' => ['unicode' => '1F69E', 'shortname' => ':mountain_railway:', 'aliases' => '', 'keywords' => 'mountain railway transportation mountain railway rail train transport'],
+            'kn' => ['unicode' => '1F1F0-1F1F3', 'shortname' => ':kn:', 'aliases' => '', 'keywords' => 'saint kitts and nevis country nation '],
+            'suspension_railway' => ['unicode' => '1F69F', 'shortname' => ':suspension_railway:', 'aliases' => '', 'keywords' => 'suspension railway transportation vehicle suspension railway rail train transportation'],
+            'lc' => ['unicode' => '1F1F1-1F1E8', 'shortname' => ':lc:', 'aliases' => '', 'keywords' => 'saint lucia country nation'],
+            'mountain_cableway' => ['unicode' => '1F6A0', 'shortname' => ':mountain_cableway:', 'aliases' => '', 'keywords' => 'mountain cableway transportation vehicle mountain cable rail train railway'],
+            'vc' => ['unicode' => '1F1FB-1F1E8', 'shortname' => ':vc:', 'aliases' => '', 'keywords' => 'saint vincent and the grenadines country nation '],
+            'aerial_tramway' => ['unicode' => '1F6A1', 'shortname' => ':aerial_tramway:', 'aliases' => '', 'keywords' => 'aerial tramway transportation vehicle aerial tram tramway cable transport'],
+            'ws' => ['unicode' => '1F1FC-1F1F8', 'shortname' => ':ws:', 'aliases' => '', 'keywords' => 'samoa country nation american samoa'],
+            'rowboat' => ['unicode' => '1F6A3', 'shortname' => ':rowboat:', 'aliases' => '', 'keywords' => 'rowboat hobby ship sports water boat row oar paddle'],
+            'sm' => ['unicode' => '1F1F8-1F1F2', 'shortname' => ':sm:', 'aliases' => '', 'keywords' => 'san marino country nation'],
+            'vertical_traffic_light' => ['unicode' => '1F6A6', 'shortname' => ':vertical_traffic_light:', 'aliases' => '', 'keywords' => 'vertical traffic light transportation traffic light stop go yield vertical'],
+            'st' => ['unicode' => '1F1F8-1F1F9', 'shortname' => ':st:', 'aliases' => '', 'keywords' => 'sao tome and principe country nation sao tome e principe'],
+            'put_litter_in_its_place' => ['unicode' => '1F6AE', 'shortname' => ':put_litter_in_its_place:', 'aliases' => '', 'keywords' => 'put litter in its place symbol blue-square litter waste trash garbage receptacle can'],
+            'do_not_litter' => ['unicode' => '1F6AF', 'shortname' => ':do_not_litter:', 'aliases' => '', 'keywords' => 'do not litter symbol bin garbage trash litter garbage waste no can trash'],
+            'sn' => ['unicode' => '1F1F8-1F1F3', 'shortname' => ':sn:', 'aliases' => '', 'keywords' => 'senegal country nation'],
+            'potable_water' => ['unicode' => '1F6B0', 'shortname' => ':potable_water:', 'aliases' => '', 'keywords' => 'potable water symbol blue-square cleaning faucet liquid restroom potable water drinkable pure clear clean aqua h20'],
+            'rs' => ['unicode' => '1F1F7-1F1F8', 'shortname' => ':rs:', 'aliases' => '', 'keywords' => 'serbia country nation srbija'],
+            'non-potable_water' => ['unicode' => '1F6B1', 'shortname' => ':non-potable_water:', 'aliases' => '', 'keywords' => 'non-potable water symbol drink faucet tap non-potable water not drinkable dirty gross aqua h20'],
+            'sc' => ['unicode' => '1F1F8-1F1E8', 'shortname' => ':sc:', 'aliases' => '', 'keywords' => 'the seychelles country nation seychelles'],
+            'no_bicycles' => ['unicode' => '1F6B3', 'shortname' => ':no_bicycles:', 'aliases' => '', 'keywords' => 'no bicycles cyclist prohibited bicycle bike pedal no'],
+            'sl' => ['unicode' => '1F1F8-1F1F1', 'shortname' => ':sl:', 'aliases' => '', 'keywords' => 'sierra leone country nation'],
+            'bicyclist' => ['unicode' => '1F6B4', 'shortname' => ':bicyclist:', 'aliases' => '', 'keywords' => 'bicyclist bike exercise hipster sports bicyclist road bike pedal bicycle transportation'],
+            'sg' => ['unicode' => '1F1F8-1F1EC', 'shortname' => ':sg:', 'aliases' => '', 'keywords' => 'singapore country nation'],
+            'mountain_bicyclist' => ['unicode' => '1F6B5', 'shortname' => ':mountain_bicyclist:', 'aliases' => '', 'keywords' => 'mountain bicyclist human sports transportation bicyclist mountain bike pedal bicycle transportation'],
+            'sk' => ['unicode' => '1F1F8-1F1F0', 'shortname' => ':sk:', 'aliases' => '', 'keywords' => 'slovakia country nation'],
+            'no_pedestrians' => ['unicode' => '1F6B7', 'shortname' => ':no_pedestrians:', 'aliases' => '', 'keywords' => 'no pedestrians crossing rules walking no walk pedestrian stroll stride foot feet'],
+            'si' => ['unicode' => '1F1F8-1F1EE', 'shortname' => ':si:', 'aliases' => '', 'keywords' => 'slovenia country nation slovenija'],
+            'children_crossing' => ['unicode' => '1F6B8', 'shortname' => ':children_crossing:', 'aliases' => '', 'keywords' => 'children crossing school children kids caution crossing street crosswalk slow'],
+            'sb' => ['unicode' => '1F1F8-1F1E7', 'shortname' => ':sb:', 'aliases' => '', 'keywords' => 'the solomon islands country nation'],
+            'shower' => ['unicode' => '1F6BF', 'shortname' => ':shower:', 'aliases' => '', 'keywords' => 'shower bath clean wash bathroom shower soap water clean shampoo lather'],
+            'so' => ['unicode' => '1F1F8-1F1F4', 'shortname' => ':so:', 'aliases' => '', 'keywords' => 'somalia country nation'],
+            'bathtub' => ['unicode' => '1F6C1', 'shortname' => ':bathtub:', 'aliases' => '', 'keywords' => 'bathtub clean shower bath tub basin wash bubble soak bathroom soap water clean shampoo lather water'],
+            'za' => ['unicode' => '1F1FF-1F1E6', 'shortname' => ':za:', 'aliases' => '', 'keywords' => 'south africa country nation'],
+            'passport_control' => ['unicode' => '1F6C2', 'shortname' => ':passport_control:', 'aliases' => '', 'keywords' => 'passport control blue-square custom passport official travel control foreign identification'],
+            'customs' => ['unicode' => '1F6C3', 'shortname' => ':customs:', 'aliases' => '', 'keywords' => 'customs border passport customs travel foreign goods check authority government'],
+            'baggage_claim' => ['unicode' => '1F6C4', 'shortname' => ':baggage_claim:', 'aliases' => '', 'keywords' => 'baggage claim airport blue-square transport bag baggage luggage travel'],
+            'lk' => ['unicode' => '1F1F1-1F1F0', 'shortname' => ':lk:', 'aliases' => '', 'keywords' => 'sri lanka country nation'],
+            'left_luggage' => ['unicode' => '1F6C5', 'shortname' => ':left_luggage:', 'aliases' => '', 'keywords' => 'left luggage blue-square travel bag baggage luggage travel'],
+            'sd' => ['unicode' => '1F1F8-1F1E9', 'shortname' => ':sd:', 'aliases' => '', 'keywords' => 'sudan country nation as-sudan'],
+            'earth_africa' => ['unicode' => '1F30D', 'shortname' => ':earth_africa:', 'aliases' => '', 'keywords' => 'earth globe europe-africa globe international world earth globe space planet africa europe home'],
+            'sr' => ['unicode' => '1F1F8-1F1F7', 'shortname' => ':sr:', 'aliases' => '', 'keywords' => 'suriname country nation'],
+            'earth_americas' => ['unicode' => '1F30E', 'shortname' => ':earth_americas:', 'aliases' => '', 'keywords' => 'earth globe americas USA globe international world earth globe space planet north south america americas home'],
+            'sz' => ['unicode' => '1F1F8-1F1FF', 'shortname' => ':sz:', 'aliases' => '', 'keywords' => 'swaziland country nation'],
+            'globe_with_meridians' => ['unicode' => '1F310', 'shortname' => ':globe_with_meridians:', 'aliases' => '', 'keywords' => 'globe with meridians earth international world earth meridian globe space planet home'],
+            'se' => ['unicode' => '1F1F8-1F1EA', 'shortname' => ':se:', 'aliases' => '', 'keywords' => 'sweden country nation sverige'],
+            'waxing_crescent_moon' => ['unicode' => '1F312', 'shortname' => ':waxing_crescent_moon:', 'aliases' => '', 'keywords' => 'waxing crescent moon symbol nature moon waxing sky night cheese phase'],
+            'ch' => ['unicode' => '1F1E8-1F1ED', 'shortname' => ':ch:', 'aliases' => '', 'keywords' => 'switzerland country nation'],
+            'waning_gibbous_moon' => ['unicode' => '1F316', 'shortname' => ':waning_gibbous_moon:', 'aliases' => '', 'keywords' => 'waning gibbous moon symbol nature moon waning gibbous sky night cheese phase'],
+            'sy' => ['unicode' => '1F1F8-1F1FE', 'shortname' => ':sy:', 'aliases' => '', 'keywords' => 'syria country nation'],
+            'last_quarter_moon' => ['unicode' => '1F317', 'shortname' => ':last_quarter_moon:', 'aliases' => '', 'keywords' => 'last quarter moon symbol nature moon last quarter sky night cheese phase'],
+            'tj' => ['unicode' => '1F1F9-1F1EF', 'shortname' => ':tj:', 'aliases' => '', 'keywords' => 'tajikistan country nation jumhurii tojikiston'],
+            'waning_crescent_moon' => ['unicode' => '1F318', 'shortname' => ':waning_crescent_moon:', 'aliases' => '', 'keywords' => 'waning crescent moon symbol nature moon crescent waning sky night cheese phase'],
+            'tz' => ['unicode' => '1F1F9-1F1FF', 'shortname' => ':tz:', 'aliases' => '', 'keywords' => 'tanzania country nation'],
+            'new_moon_with_face' => ['unicode' => '1F31A', 'shortname' => ':new_moon_with_face:', 'aliases' => '', 'keywords' => 'new moon with face nature moon new anthropomorphic face sky night cheese phase'],
+            'th' => ['unicode' => '1F1F9-1F1ED', 'shortname' => ':th:', 'aliases' => '', 'keywords' => 'thailand country nation prathet thai'],
+            'last_quarter_moon_with_face' => ['unicode' => '1F31C', 'shortname' => ':last_quarter_moon_with_face:', 'aliases' => '', 'keywords' => 'last quarter moon with face nature moon last quarter anthropomorphic face sky night cheese phase'],
+            'tg' => ['unicode' => '1F1F9-1F1EC', 'shortname' => ':tg:', 'aliases' => '', 'keywords' => 'togo country nation republique togolaise'],
+            'full_moon_with_face' => ['unicode' => '1F31D', 'shortname' => ':full_moon_with_face:', 'aliases' => '', 'keywords' => 'full moon with face night moon full anthropomorphic face sky night cheese phase spooky werewolves monsters'],
+            'to' => ['unicode' => '1F1F9-1F1F4', 'shortname' => ':to:', 'aliases' => '', 'keywords' => 'tonga country nation'],
+            'sun_with_face' => ['unicode' => '1F31E', 'shortname' => ':sun_with_face:', 'aliases' => '', 'keywords' => 'sun with face morning sun anthropomorphic face sky'],
+            'tt' => ['unicode' => '1F1F9-1F1F9', 'shortname' => ':tt:', 'aliases' => '', 'keywords' => 'trinidad and tobago country nation'],
+            'evergreen_tree' => ['unicode' => '1F332', 'shortname' => ':evergreen_tree:', 'aliases' => '', 'keywords' => 'evergreen tree nature plant evergreen tree needles christmas'],
+            'tn' => ['unicode' => '1F1F9-1F1F3', 'shortname' => ':tn:', 'aliases' => '', 'keywords' => 'tunisia country nation tunis'],
+            'deciduous_tree' => ['unicode' => '1F333', 'shortname' => ':deciduous_tree:', 'aliases' => '', 'keywords' => 'deciduous tree nature plant deciduous tree leaves fall color'],
+            'tr' => ['unicode' => '1F1F9-1F1F7', 'shortname' => ':tr:', 'aliases' => '', 'keywords' => 'turkey country nation turkiye'],
+            'lemon' => ['unicode' => '1F34B', 'shortname' => ':lemon:', 'aliases' => '', 'keywords' => 'lemon fruit nature lemon yellow citrus'],
+            'pear' => ['unicode' => '1F350', 'shortname' => ':pear:', 'aliases' => '', 'keywords' => 'pear fruit nature pear fruit shape'],
+            'baby_bottle' => ['unicode' => '1F37C', 'shortname' => ':baby_bottle:', 'aliases' => '', 'keywords' => 'baby bottle container food baby bottle milk mother nipple newborn formula'],
+            'ug' => ['unicode' => '1F1FA-1F1EC', 'shortname' => ':ug:', 'aliases' => '', 'keywords' => 'uganda country nation'],
+            'horse_racing' => ['unicode' => '1F3C7', 'shortname' => ':horse_racing:', 'aliases' => '', 'keywords' => 'horse racing animal betting competition horse race racing jockey triple crown'],
+            'ua' => ['unicode' => '1F1FA-1F1E6', 'shortname' => ':ua:', 'aliases' => '', 'keywords' => 'ukraine country nation ukrayina'],
+            'rugby_football' => ['unicode' => '1F3C9', 'shortname' => ':rugby_football:', 'aliases' => '', 'keywords' => 'rugby football sports rugby football ball sport team england'],
+            'ae' => ['unicode' => '1F1E6-1F1EA', 'shortname' => ':ae:', 'aliases' => '', 'keywords' => 'the united arab emirates country nation'],
+            'european_post_office' => ['unicode' => '1F3E4', 'shortname' => ':european_post_office:', 'aliases' => '', 'keywords' => 'european post office building'],
+            'rat' => ['unicode' => '1F400', 'shortname' => ':rat:', 'aliases' => '', 'keywords' => 'rat animal mouse rat rodent crooked snitch'],
+            'mouse2' => ['unicode' => '1F401', 'shortname' => ':mouse2:', 'aliases' => '', 'keywords' => 'mouse animal nature mouse mice rodent'],
+            'uy' => ['unicode' => '1F1FA-1F1FE', 'shortname' => ':uy:', 'aliases' => '', 'keywords' => 'uruguay country nation'],
+            'ox' => ['unicode' => '1F402', 'shortname' => ':ox:', 'aliases' => '', 'keywords' => 'ox animal beef cow'],
+            'uz' => ['unicode' => '1F1FA-1F1FF', 'shortname' => ':uz:', 'aliases' => '', 'keywords' => 'uzbekistan country nation uzbekiston respublikasi'],
+            'water_buffalo' => ['unicode' => '1F403', 'shortname' => ':water_buffalo:', 'aliases' => '', 'keywords' => 'water buffalo animal cow nature ox water buffalo asia bovine milk dairy'],
+            'vu' => ['unicode' => '1F1FB-1F1FA', 'shortname' => ':vu:', 'aliases' => '', 'keywords' => 'vanuatu country nation'],
+            'cow2' => ['unicode' => '1F404', 'shortname' => ':cow2:', 'aliases' => '', 'keywords' => 'cow animal beef nature ox cow milk dairy beef bessie moo'],
+            'va' => ['unicode' => '1F1FB-1F1E6', 'shortname' => ':va:', 'aliases' => '', 'keywords' => 'the vatican city country nation'],
+            'tiger2' => ['unicode' => '1F405', 'shortname' => ':tiger2:', 'aliases' => '', 'keywords' => 'tiger animal nature tiger cat striped tony tigger hobs'],
+            've' => ['unicode' => '1F1FB-1F1EA', 'shortname' => ':ve:', 'aliases' => '', 'keywords' => 'venezuela country nation'],
+            'leopard' => ['unicode' => '1F406', 'shortname' => ':leopard:', 'aliases' => '', 'keywords' => 'leopard animal nature leopard cat spot spotted sexy'],
+            'vn' => ['unicode' => '1F1FB-1F1F3', 'shortname' => ':vn:', 'aliases' => '', 'keywords' => 'vietnam country nation viet nam'],
+            'rabbit2' => ['unicode' => '1F407', 'shortname' => ':rabbit2:', 'aliases' => '', 'keywords' => 'rabbit animal nature rabbit bunny easter reproduction prolific'],
+            'eh' => ['unicode' => '1F1EA-1F1ED', 'shortname' => ':eh:', 'aliases' => '', 'keywords' => 'western sahara country nation aṣ-Ṣaḥrā’ al-gharbīyah sahra gharbiyah'],
+            'cat2' => ['unicode' => '1F408', 'shortname' => ':cat2:', 'aliases' => '', 'keywords' => 'cat animal meow pet cat kitten meow'],
+            'ye' => ['unicode' => '1F1FE-1F1EA', 'shortname' => ':ye:', 'aliases' => '', 'keywords' => 'yemen country nation al yaman'],
+            'dragon' => ['unicode' => '1F409', 'shortname' => ':dragon:', 'aliases' => '', 'keywords' => 'dragon animal chinese green myth nature dragon fire legendary myth'],
+            'zm' => ['unicode' => '1F1FF-1F1F2', 'shortname' => ':zm:', 'aliases' => '', 'keywords' => 'zambia country nation'],
+            'crocodile' => ['unicode' => '1F40A', 'shortname' => ':crocodile:', 'aliases' => '', 'keywords' => 'crocodile animal nature crocodile croc alligator gator cranky'],
+            'zw' => ['unicode' => '1F1FF-1F1FC', 'shortname' => ':zw:', 'aliases' => '', 'keywords' => 'zimbabwe country nation'],
+            'whale2' => ['unicode' => '1F40B', 'shortname' => ':whale2:', 'aliases' => '', 'keywords' => 'whale animal nature ocean sea whale blubber bloated fat large massive'],
+            'pr' => ['unicode' => '1F1F5-1F1F7', 'shortname' => ':pr:', 'aliases' => '', 'keywords' => 'puerto rico country nation'],
+            'ram' => ['unicode' => '1F40F', 'shortname' => ':ram:', 'aliases' => '', 'keywords' => 'ram animal nature sheep ram sheep male horn horns'],
+            'ky' => ['unicode' => '1F1F0-1F1FE', 'shortname' => ':ky:', 'aliases' => '', 'keywords' => 'cayman islands country nation'],
+            'goat' => ['unicode' => '1F410', 'shortname' => ':goat:', 'aliases' => '', 'keywords' => 'goat animal nature goat sheep kid billy livestock'],
+            'bm' => ['unicode' => '1F1E7-1F1F2', 'shortname' => ':bm:', 'aliases' => '', 'keywords' => 'bermuda country nation'],
+            'rooster' => ['unicode' => '1F413', 'shortname' => ':rooster:', 'aliases' => '', 'keywords' => 'rooster animal chicken nature rooster cockerel cock male cock-a-doodle-doo crowing'],
+            'pf' => ['unicode' => '1F1F5-1F1EB', 'shortname' => ':pf:', 'aliases' => '', 'keywords' => 'french polynesia country nation polynésie française polynesie francaise'],
+            'dog2' => ['unicode' => '1F415', 'shortname' => ':dog2:', 'aliases' => '', 'keywords' => 'dog animal doge friend nature pet dog puppy pet friend woof bark fido'],
+            'ps' => ['unicode' => '1F1F5-1F1F8', 'shortname' => ':ps:', 'aliases' => '', 'keywords' => 'palestinian authority country nation'],
+            'pig2' => ['unicode' => '1F416', 'shortname' => ':pig2:', 'aliases' => '', 'keywords' => 'pig animal nature pig piggy pork ham hog bacon oink slop livestock greed greedy'],
+            'nc' => ['unicode' => '1F1F3-1F1E8', 'shortname' => ':nc:', 'aliases' => '', 'keywords' => 'new caledonia country nation nouvelle calédonie caledonie'],
+            'dromedary_camel' => ['unicode' => '1F42A', 'shortname' => ':dromedary_camel:', 'aliases' => '', 'keywords' => 'dromedary camel animal desert hot dromedary camel hump desert middle east heat hot water hump day wednesday sex'],
+            'sh' => ['unicode' => '1F1F8-1F1ED', 'shortname' => ':sh:', 'aliases' => '', 'keywords' => 'saint helena country nation'],
+            'busts_in_silhouette' => ['unicode' => '1F465', 'shortname' => ':busts_in_silhouette:', 'aliases' => '', 'keywords' => 'busts in silhouette group human man person team user silhouette silhouettes people user members accounts relationship shadow'],
+            'aw' => ['unicode' => '1F1E6-1F1FC', 'shortname' => ':aw:', 'aliases' => '', 'keywords' => 'aruba country nation'],
+            'two_men_holding_hands' => ['unicode' => '1F46C', 'shortname' => ':two_men_holding_hands:', 'aliases' => '', 'keywords' => 'two men holding hands bromance couple friends like love men gay homosexual friends hands holding team unity'],
+            'vi' => ['unicode' => '1F1FB-1F1EE', 'shortname' => ':vi:', 'aliases' => '', 'keywords' => 'u.s. virgin islands country nation'],
+            'two_women_holding_hands' => ['unicode' => '1F46D', 'shortname' => ':two_women_holding_hands:', 'aliases' => '', 'keywords' => 'two women holding hands couple female friends like love women hands girlfriends friends sisters mother daughter gay homosexual couple unity'],
+            'hk' => ['unicode' => '1F1ED-1F1F0', 'shortname' => ':hk:', 'aliases' => '', 'keywords' => 'hong kong country nation xianggang'],
+            'thought_balloon' => ['unicode' => '1F4AD', 'shortname' => ':thought_balloon:', 'aliases' => '', 'keywords' => 'thought balloon bubble cloud speech thought balloon comic think day dream wonder'],
+            'ac' => ['unicode' => '1F1E6-1F1E8', 'shortname' => ':ac:', 'aliases' => '', 'keywords' => 'ascension country nation'],
+            'euro' => ['unicode' => '1F4B6', 'shortname' => ':euro:', 'aliases' => '', 'keywords' => 'banknote with euro sign currency dollar money euro europe banknote money currency paper cash bills'],
+            'ms' => ['unicode' => '1F1F2-1F1F8', 'shortname' => ':ms:', 'aliases' => '', 'keywords' => 'montserrat country nation'],
+            'pound' => ['unicode' => '1F4B7', 'shortname' => ':pound:', 'aliases' => '', 'keywords' => 'banknote with pound sign bills british currency england money sterling uk pound britain british banknote money currency paper cash bills'],
+            'gu' => ['unicode' => '1F1EC-1F1FA', 'shortname' => ':gu:', 'aliases' => '', 'keywords' => 'guam country nation'],
+            'mailbox_with_mail' => ['unicode' => '1F4EC', 'shortname' => ':mailbox_with_mail:', 'aliases' => '', 'keywords' => 'open mailbox with raised flag communication email inbox'],
+            'gl' => ['unicode' => '1F1EC-1F1F1', 'shortname' => ':gl:', 'aliases' => '', 'keywords' => 'greenland country nation kalaallit nunaat'],
+            'mailbox_with_no_mail' => ['unicode' => '1F4ED', 'shortname' => ':mailbox_with_no_mail:', 'aliases' => '', 'keywords' => 'open mailbox with lowered flag email inbox'],
+            'nu' => ['unicode' => '1F1F3-1F1FA', 'shortname' => ':nu:', 'aliases' => '', 'keywords' => 'niue country nation'],
+            'postal_horn' => ['unicode' => '1F4EF', 'shortname' => ':postal_horn:', 'aliases' => '', 'keywords' => 'postal horn instrument music'],
+            'wf' => ['unicode' => '1F1FC-1F1EB', 'shortname' => ':wf:', 'aliases' => '', 'keywords' => 'wallis and futuna country nation'],
+            'no_mobile_phones' => ['unicode' => '1F4F5', 'shortname' => ':no_mobile_phones:', 'aliases' => '', 'keywords' => 'no mobile phones iphone mute'],
+            'mo' => ['unicode' => '1F1F2-1F1F4', 'shortname' => ':mo:', 'aliases' => '', 'keywords' => 'macau country nation aomen'],
+            'twisted_rightwards_arrows' => ['unicode' => '1F500', 'shortname' => ':twisted_rightwards_arrows:', 'aliases' => '', 'keywords' => 'twisted rightwards arrows blue-square'],
+            'fo' => ['unicode' => '1F1EB-1F1F4', 'shortname' => ':fo:', 'aliases' => '', 'keywords' => 'faroe islands country nation foroyar'],
+            'repeat' => ['unicode' => '1F501', 'shortname' => ':repeat:', 'aliases' => '', 'keywords' => 'clockwise rightwards and leftwards open circle arr loop record'],
+            'fk' => ['unicode' => '1F1EB-1F1F0', 'shortname' => ':fk:', 'aliases' => '', 'keywords' => 'falkland islands country nation islas malvinas'],
+            'repeat_one' => ['unicode' => '1F502', 'shortname' => ':repeat_one:', 'aliases' => '', 'keywords' => 'clockwise rightwards and leftwards open circle arr blue-square loop'],
+            'je' => ['unicode' => '1F1EF-1F1EA', 'shortname' => ':je:', 'aliases' => '', 'keywords' => 'jersey country nation'],
+            'arrows_counterclockwise' => ['unicode' => '1F504', 'shortname' => ':arrows_counterclockwise:', 'aliases' => '', 'keywords' => 'anticlockwise downwards and upwards open circle ar blue-square sync'],
+            'ai' => ['unicode' => '1F1E6-1F1EE', 'shortname' => ':ai:', 'aliases' => '', 'keywords' => 'anguilla country nation'],
+            'low_brightness' => ['unicode' => '1F505', 'shortname' => ':low_brightness:', 'aliases' => '', 'keywords' => 'low brightness symbol summer sun'],
+            'gi' => ['unicode' => '1F1EC-1F1EE', 'shortname' => ':gi:', 'aliases' => '', 'keywords' => 'gibraltar country nation'],
+            'high_brightness' => ['unicode' => '1F506', 'shortname' => ':high_brightness:', 'aliases' => '', 'keywords' => 'high brightness symbol light summer sun'],
+            'mute' => ['unicode' => '1F507', 'shortname' => ':mute:', 'aliases' => '', 'keywords' => 'speaker with cancellation stroke sound volume'],
+            'sound' => ['unicode' => '1F509', 'shortname' => ':sound:', 'aliases' => '', 'keywords' => 'speaker with one sound wave speaker volume'],
+            'no_bell' => ['unicode' => '1F515', 'shortname' => ':no_bell:', 'aliases' => '', 'keywords' => 'bell with cancellation stroke mute sound volume'],
+            'microscope' => ['unicode' => '1F52C', 'shortname' => ':microscope:', 'aliases' => '', 'keywords' => 'microscope experiment laboratory zoomin'],
+            'telescope' => ['unicode' => '1F52D', 'shortname' => ':telescope:', 'aliases' => '', 'keywords' => 'telescope space stars'],
+            'clock130' => ['unicode' => '1F55C', 'shortname' => ':clock130:', 'aliases' => '', 'keywords' => 'clock face one-thirty clock time'],
+            'clock230' => ['unicode' => '1F55D', 'shortname' => ':clock230:', 'aliases' => '', 'keywords' => 'clock face two-thirty clock time'],
+            'clock330' => ['unicode' => '1F55E', 'shortname' => ':clock330:', 'aliases' => '', 'keywords' => 'clock face three-thirty clock time'],
+            'clock430' => ['unicode' => '1F55F', 'shortname' => ':clock430:', 'aliases' => '', 'keywords' => 'clock face four-thirty clock time'],
+            'clock530' => ['unicode' => '1F560', 'shortname' => ':clock530:', 'aliases' => '', 'keywords' => 'clock face five-thirty clock time'],
+            'clock630' => ['unicode' => '1F561', 'shortname' => ':clock630:', 'aliases' => '', 'keywords' => 'clock face six-thirty clock time'],
+            'clock730' => ['unicode' => '1F562', 'shortname' => ':clock730:', 'aliases' => '', 'keywords' => 'clock face seven-thirty clock time'],
+            'clock830' => ['unicode' => '1F563', 'shortname' => ':clock830:', 'aliases' => '', 'keywords' => 'clock face eight-thirty clock time'],
+            'clock930' => ['unicode' => '1F564', 'shortname' => ':clock930:', 'aliases' => '', 'keywords' => 'clock face nine-thirty clock time'],
+            'clock1030' => ['unicode' => '1F565', 'shortname' => ':clock1030:', 'aliases' => '', 'keywords' => 'clock face ten-thirty clock time'],
+            'clock1130' => ['unicode' => '1F566', 'shortname' => ':clock1130:', 'aliases' => '', 'keywords' => 'clock face eleven-thirty clock time'],
+            'clock1230' => ['unicode' => '1F567', 'shortname' => ':clock1230:', 'aliases' => '', 'keywords' => 'clock face twelve-thirty clock time']
+        ];
+    }
+
+    /**
+     * Get the emoji list to include in chat
+     * @return array
+     */
+    public static function getEmojisToInclude()
+    {
+        return [
+            ':bowtie:',
+            ':smile:'|
+            ':laughing:',
+            ':blush:',
+            ':smiley:',
+            ':relaxed:',
+            ':smirk:',
+            ':heart_eyes:',
+            ':kissing_heart:',
+            ':kissing_closed_eyes:',
+            ':flushed:',
+            ':relieved:',
+            ':satisfied:',
+            ':grin:',
+            ':wink:',
+            ':stuck_out_tongue_winking_eye:',
+            ':stuck_out_tongue_closed_eyes:',
+            ':grinning:',
+            ':kissing:',
+            ':kissing_smiling_eyes:',
+            ':stuck_out_tongue:',
+            ':sleeping:',
+            ':worried:',
+            ':frowning:',
+            ':anguished:',
+            ':open_mouth:',
+            ':grimacing:',
+            ':confused:',
+            ':hushed:',
+            ':expressionless:',
+            ':unamused:',
+            ':sweat_smile:',
+            ':sweat:',
+            ':disappointed_relieved:',
+            ':weary:',
+            ':pensive:',
+            ':disappointed:',
+            ':confounded:',
+            ':fearful:',
+            ':cold_sweat:',
+            ':persevere:',
+            ':cry:',
+            ':sob:',
+            ':joy:',
+            ':astonished:',
+            ':scream:',
+            ':neckbeard:',
+            ':tired_face:',
+            ':angry:',
+            ':rage:',
+            ':triumph:',
+            ':sleepy:',
+            ':yum:',
+            ':mask:',
+            ':sunglasses:',
+            ':dizzy_face:',
+            ':imp:',
+            ':smiling_imp:',
+            ':neutral_face:',
+            ':no_mouth:',
+            ':innocent:',
+            ':alien:'
+        ];
+    }
+
+    /**
+     * Get the chat history file name
+     * @param bool $absolute Optional. Whether get the base or the absolute file path
+     * @param int $friendId Optional.
+     * @return string
+     */
+    public function getFileName($absolute = false, $friendId = 0)
+    {
+        $date = date('Y-m-d');
+
+        $base = 'messages-' . $date . '.log.html';
+
+        if ($this->groupId && !$friendId) {
+            $base = 'messages-' . $date . '_gid-' . $this->groupId . '.log.html';
+        } elseif ($this->sessionId && !$friendId) {
+            $base = 'messages-' . $date . '_sid-' . $this->sessionId . '.log.html';
+        } elseif ($friendId) {
+            if ($this->userId < $friendId) {
+                $base = 'messages-' . $date . '_uid-' . $this->userId . '-' . $friendId . '.log.html';
+            } else {
+                $base = 'messages-' . $date . '_uid-' . $friendId . '-' . $this->userId . '.log.html';
+            }
+        }
+
+        if (!$absolute) {
+            return $base;
+        }
+
+        $courseInfo = api_get_course_info_by_id($this->courseId);
+        $document_path = api_get_path(SYS_COURSE_PATH) . $courseInfo['path'] . '/document';
+
+        $chatPath = $document_path . '/chat_files/';
+
+        if ($this->groupId) {
+            $group_info = GroupManager::get_group_properties($this->groupId);
+            $chatPath = $document_path . $group_info['directory'] . '/chat_files/';
+        }
+
+        return $chatPath . $base;
+    }
+
+    /**
+     * Get the chat history
+     * @param bool $reset
+     * @param int $friendId Optional.
+     * @return string
+     */
+    public function readMessages($reset = false, $friendId = 0)
+    {
+        $courseInfo = api_get_course_info_by_id($this->courseId);
+        $date_now = date('Y-m-d');
+        $isMaster = (bool)api_is_course_admin();
+        $basepath_chat = '/chat_files';
+        $document_path = api_get_path(SYS_COURSE_PATH) . $courseInfo['path'] . '/document';
+
+        if ($this->groupId) {
+            $group_info = GroupManager:: get_group_properties($this->groupId);
+            $basepath_chat = $group_info['directory'] . '/chat_files';
+        }
+
+        $chat_path = $document_path . $basepath_chat . '/';
+
+        if (!is_dir($chat_path)) {
+            if (is_file($chat_path)) {
+                @unlink($chat_path);
+            }
+
+            if (!api_is_anonymous()) {
+                @mkdir($chat_path, api_get_permissions_for_new_directories());
+                // Save chat files document for group into item property
+                if ($this->groupId) {
+                    $doc_id = add_document($courseInfo, $basepath_chat, 'folder', 0, 'chat_files');
+                    api_item_property_update(
+                        $courseInfo,
+                        TOOL_DOCUMENT,
+                        $doc_id,
+                        'FolderCreated',
+                        null,
+                        $this->groupId,
+                        null,
+                        null,
+                        null
+                    );
+                }
+            }
+        }
+
+        $filename_chat = 'messages-' . $date_now . '.log.html';
+
+        if ($this->groupId && !$friendId) {
+            $filename_chat = 'messages-' . $date_now . '_gid-' . $this->groupId . '.log.html';
+        } else if ($this->sessionId && !$friendId) {
+            $filename_chat = 'messages-' . $date_now . '_sid-' . $this->sessionId . '.log.html';
+        } elseif ($friendId) {
+            if ($this->userId < $friendId) {
+                $filename_chat = 'messages-' . $date_now . '_uid-' . $this->userId . '-' . $friendId . '.log.html';
+            } else {
+                $filename_chat = 'messages-' . $date_now . '_uid-' . $friendId . '-' . $this->userId . '.log.html';
+            }
+        }
+
+        if (!file_exists($chat_path . $filename_chat)) {
+            @fclose(fopen($chat_path . $filename_chat, 'w'));
+            if (!api_is_anonymous()) {
+                $doc_id = add_document($courseInfo, $basepath_chat . '/' . $filename_chat, 'file', 0, $filename_chat);
+                api_item_property_update(
+                    $courseInfo,
+                    TOOL_DOCUMENT,
+                    $doc_id,
+                    'DocumentAdded',
+                    $this->userId,
+                    $this->groupId,
+                    null,
+                    null,
+                    null,
+                    $this->sessionId
+                );
+                api_item_property_update(
+                    $courseInfo,
+                    TOOL_DOCUMENT,
+                    $doc_id,
+                    'invisible',
+                    $this->userId,
+                    $this->groupId,
+                    null,
+                    null,
+                    null,
+                    $this->sessionId
+                );
+                item_property_update_on_folder($courseInfo, $basepath_chat, $this->userId);
+            }
+        }
+
+        $basename_chat = 'messages-' . $date_now;
+
+        if ($this->groupId && !$friendId) {
+            $basename_chat = 'messages-' . $date_now . '_gid-' . $this->groupId;
+        } else if ($this->sessionId && !$friendId) {
+            $basename_chat = 'messages-' . $date_now . '_sid-' . $this->sessionId;
+        } elseif ($friendId) {
+            if ($this->userId < $friendId) {
+                $basename_chat = 'messages-' . $date_now . '_uid-' . $this->userId . '-' . $friendId;
+            } else {
+                $basename_chat = 'messages-' . $date_now . '_uid-' . $friendId . '-' . $this->userId;
+            }
+        }
+
+        if ($reset && $isMaster) {
+
+            $i = 1;
+
+            while (file_exists($chat_path . $basename_chat . '-' . $i . '.log.html')) {
+                $i++;
+            }
+
+            @rename($chat_path . $basename_chat . '.log.html', $chat_path . $basename_chat . '-' . $i . '.log.html');
+            @fclose(fopen($chat_path . $basename_chat . '.log.html', 'w'));
+
+            $doc_id = add_document(
+                $courseInfo,
+                $basepath_chat . '/' . $basename_chat . '-' . $i . '.log.html',
+                'file',
+                filesize($chat_path . $basename_chat . '-' . $i . '.log.html'),
+                $basename_chat . '-' . $i . '.log.html'
+            );
+
+            api_item_property_update(
+                $courseInfo,
+                TOOL_DOCUMENT,
+                $doc_id,
+                'DocumentAdded',
+                $this->userId,
+                $this->groupId,
+                null,
+                null,
+                null,
+                $this->sessionId
+            );
+            api_item_property_update(
+                $courseInfo,
+                TOOL_DOCUMENT,
+                $doc_id,
+                'invisible',
+                $this->userId,
+                $this->groupId,
+                null,
+                null,
+                null,
+                $this->sessionId
+            );
+            item_property_update_on_folder($courseInfo, $basepath_chat, $this->userId);
+
+            $doc_id = DocumentManager::get_document_id(
+                $courseInfo,
+                $basepath_chat . '/' . $basename_chat . '.log.html'
+            );
+
+            update_existing_document($courseInfo, $doc_id, 0);
+        }
+
+        $remove = 0;
+        $content = array();
+
+        if (file_exists($chat_path . $basename_chat . '.log.html')) {
+            $content = file($chat_path . $basename_chat . '.log.html');
+            $nbr_lines = sizeof($content);
+            $remove = $nbr_lines - 100;
+        }
+
+        if ($remove < 0) {
+            $remove = 0;
+        }
+
+        array_splice($content, 0, $remove);
+
+        if (isset($_GET['origin']) && $_GET['origin'] == 'whoisonline') {
+            //the caller
+            $content[0] = get_lang('CallSent') . '<br />' . $content[0];
+        }
+        if (isset($_GET['origin']) && $_GET['origin'] == 'whoisonlinejoin') {
+            //the joiner (we have to delete the chat request to him when he joins the chat)
+            Database::getManager()
+                ->createQuery('
+                    UPDATE ChamiloUserBundle:User u
+                    SET u.chatcallUserId = NULL, u.chatcallDate = NULL, u.chatcallText = NULL
+                    WHERE u.id = :user
+                ')
+                ->execute(['user' => $this->userId]);
+        }
+
+        $history = '<div id="content-chat">';
+
+        foreach ($content as $this_line) {
+            $history .= $this_line;
+        }
+
+        $history .= '</div>';
+
+        if ($isMaster || $GLOBALS['is_courseCoach']) {
+            $history .= '
+                <div id="clear-chat">
+                    <button type="button" id="chat-reset" class="btn btn-danger btn-sm">
+                        ' . get_lang('ClearList') . '
+                    </button>
+                </div>
+            ';
+        }
+
+        return $history;
+    }
+
+    /**
+     * Get the number of users connected in chat
+     * @return mixed
+     */
+    public function countUsersOnline()
+    {
+        $date = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
+        $date->modify('-5 seconds');
+
+        $extraCondition = null;
+
+        if ($this->groupId) {
+            $extraCondition = 'AND ccc.toGroupId = ' . intval($this->groupId);
+        } else {
+            $extraCondition = 'AND ccc.sessionId = ' . intval($this->sessionId);
+        }
+
+        $number = Database::getManager()
+            ->createQuery("
+                SELECT COUNT(ccc.userId) FROM ChamiloCourseBundle:CChatConnected ccc
+                WHERE ccc.lastConnection > :date AND ccc.cId = :course $extraCondition
+            ")
+            ->setParameters([
+                'date' => $date,
+                'course' => $this->courseId
+            ])
+            ->getSingleScalarResult();
+
+        return intval($number);
+    }
+
+    /**
+     * Check if a user is connected in course chat
+     * @param int $userId
+     * @return int
+     */
+    private function userIsConnected($userId)
+    {
+        $date = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
+        $date->modify('-5 seconds');
+
+        $extraCondition = null;
+
+        if ($this->groupId) {
+            $extraCondition = 'AND ccc.toGroupId = ' . intval($this->groupId);
+        } else {
+            $extraCondition = 'AND ccc.sessionId = ' . intval($this->sessionId);
+        }
+
+        $number = Database::getManager()
+            ->createQuery("
+                SELECT COUNT(ccc.userId) FROM ChamiloCourseBundle:CChatConnected ccc
+                WHERE ccc.lastConnection > :date AND ccc.cId = :course AND ccc.userId = :user $extraCondition
+            ")
+            ->setParameters([
+                'date' => $date,
+                'course' => $this->courseId,
+                'user' => $userId
+            ])
+            ->getSingleScalarResult();
+
+        return intval($number);
+    }
+
+    /**
+     * Get the users online data
+     * @return string
+     */
+    public function listUsersOnline()
+    {
+        $subscriptions = $this->getUsersSubscriptions();
+        $usersInfo = [];
+
+        foreach ($subscriptions as $subscription) {
+            $user = $subscription->getUser();
+
+            $usersInfo[] = [
+                'id' => $user->getId(),
+                'firstname' => $user->getFirstname(),
+                'lastname' => $user->getLastname(),
+                'status' => !$this->sessionId ? $subscription->getStatus() : $user->getStatus(),
+                'image_url' => UserManager::getUserPicture($user->getId(), USER_IMAGE_SIZE_MEDIUM),
+                'profile_url' => api_get_path(WEB_CODE_PATH) . 'social/profile.php?u=' . $user->getId(),
+                'complete_name' => $user->getCompleteName(),
+                'username' => $user->getUsername(),
+                'email' => $user->getEmail(),
+                'isConnected' => $this->userIsConnected($user->getId())
+            ];
+        }
+
+        return $usersInfo;
+    }
+}

+ 1 - 1
main/inc/lib/document.lib.php

@@ -515,7 +515,7 @@ class DocumentManager
         if (!empty($sessionId)) {
             // Chat folder filter
             if ($path == '/chat_files') {
-                $condition .= " AND (id_session = '$sessionId') ";
+                $condition .= " AND (docs.session_id = '$sessionId') ";
             }
             // share_folder filter
             $condition .= " AND docs.path != '/shared_folder' ";

+ 1449 - 519
main/inc/lib/javascript/jquery-textcomplete/jquery.textcomplete.js

@@ -1,553 +1,1483 @@
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof module === "object" && module.exports) {
+    var $ = require('jquery');
+    module.exports = factory($);
+  } else {
+    // Browser globals
+    factory(jQuery);
+  }
+}(function (jQuery) {
+
 /*!
- * jQuery.textcomplete.js
+ * jQuery.textcomplete
  *
- * Repositiory: https://github.com/yuku-t/jquery-textcomplete
- * License:     MIT
- * Author:      Yuku Takahashi
+ * Repository: https://github.com/yuku-t/jquery-textcomplete
+ * License:    MIT (https://github.com/yuku-t/jquery-textcomplete/blob/master/LICENSE)
+ * Author:     Yuku Takahashi
  */
 
-;(function ($) {
-
-    'use strict';
-
-    /**
-     * Exclusive execution control utility.
-     */
-    var lock = function (func) {
-        var free, locked;
-        free = function () { locked = false; };
-        return function () {
-            var args;
-            if (locked) return;
-            locked = true;
-            args = toArray(arguments);
-            args.unshift(free);
-            func.apply(this, args);
-        };
-    };
+if (typeof jQuery === 'undefined') {
+  throw new Error('jQuery.textcomplete requires jQuery');
+}
 
-    /**
-     * Convert arguments into a real array.
-     */
-    var toArray = function (args) {
-        var result;
-        result = Array.prototype.slice.call(args);
-        return result;
-    };
++function ($) {
+  'use strict';
 
-    /**
-     * Get the styles of any element from property names.
-     */
-    var getStyles = (function () {
-        var color;
-        color = $('<div></div>').css(['color']).color;
-        if (typeof color !== 'undefined') {
-            return function ($el, properties) {
-                return $el.css(properties);
-            };
-        } else {  // for jQuery 1.8 or below
-            return function ($el, properties) {
-                var styles;
-                styles = {};
-                $.each(properties, function (i, property) {
-                    styles[property] = $el.css(property);
-                });
-                return styles;
-            };
+  var warn = function (message) {
+    if (console.warn) { console.warn(message); }
+  };
+
+  var id = 1;
+
+  $.fn.textcomplete = function (strategies, option) {
+    var args = Array.prototype.slice.call(arguments);
+    return this.each(function () {
+      var self = this;
+      var $this = $(this);
+      var completer = $this.data('textComplete');
+      if (!completer) {
+        option || (option = {});
+        option._oid = id++;  // unique object id
+        completer = new $.fn.textcomplete.Completer(this, option);
+        $this.data('textComplete', completer);
+      }
+      if (typeof strategies === 'string') {
+        if (!completer) return;
+        args.shift()
+        completer[strategies].apply(completer, args);
+        if (strategies === 'destroy') {
+          $this.removeData('textComplete');
         }
-    })();
-
-    /**
-     * Default template function.
-     */
-    var identity = function (obj) { return obj; };
-
-    /**
-     * Memoize a search function.
-     */
-    var memoize = function (func) {
-        var memo = {};
-        return function (term, callback) {
-            if (memo[term]) {
-                callback(memo[term]);
-            } else {
-                func.call(this, term, function (data) {
-                    memo[term] = (memo[term] || []).concat(data);
-                    callback.apply(null, arguments);
-                });
+      } else {
+        // For backward compatibility.
+        // TODO: Remove at v0.4
+        $.each(strategies, function (obj) {
+          $.each(['header', 'footer', 'placement', 'maxCount'], function (name) {
+            if (obj[name]) {
+              completer.option[name] = obj[name];
+              warn(name + 'as a strategy param is deprecated. Use option.');
+              delete obj[name];
             }
-        };
-    };
+          });
+        });
+        completer.register($.fn.textcomplete.Strategy.parse(strategies, {
+          el: self,
+          $el: $this
+        }));
+      }
+    });
+  };
+
+}(jQuery);
+
++function ($) {
+  'use strict';
+
+  // Exclusive execution control utility.
+  //
+  // func - The function to be locked. It is executed with a function named
+  //        `free` as the first argument. Once it is called, additional
+  //        execution are ignored until the free is invoked. Then the last
+  //        ignored execution will be replayed immediately.
+  //
+  // Examples
+  //
+  //   var lockedFunc = lock(function (free) {
+  //     setTimeout(function { free(); }, 1000); // It will be free in 1 sec.
+  //     console.log('Hello, world');
+  //   });
+  //   lockedFunc();  // => 'Hello, world'
+  //   lockedFunc();  // none
+  //   lockedFunc();  // none
+  //   // 1 sec past then
+  //   // => 'Hello, world'
+  //   lockedFunc();  // => 'Hello, world'
+  //   lockedFunc();  // none
+  //
+  // Returns a wrapped function.
+  var lock = function (func) {
+    var locked, queuedArgsToReplay;
 
-    /**
-     * Determine if the array contains a given value.
-     */
-    var include = function (array, value) {
-        var i, l;
-        if (array.indexOf) return array.indexOf(value) != -1;
-        for (i = 0, l = array.length; i < l; i++) {
-            if (array[i] === value) return true;
+    return function () {
+      // Convert arguments into a real array.
+      var args = Array.prototype.slice.call(arguments);
+      if (locked) {
+        // Keep a copy of this argument list to replay later.
+        // OK to overwrite a previous value because we only replay
+        // the last one.
+        queuedArgsToReplay = args;
+        return;
+      }
+      locked = true;
+      var self = this;
+      args.unshift(function replayOrFree() {
+        if (queuedArgsToReplay) {
+          // Other request(s) arrived while we were locked.
+          // Now that the lock is becoming available, replay
+          // the latest such request, then call back here to
+          // unlock (or replay another request that arrived
+          // while this one was in flight).
+          var replayArgs = queuedArgsToReplay;
+          queuedArgsToReplay = undefined;
+          replayArgs.unshift(replayOrFree);
+          func.apply(self, replayArgs);
+        } else {
+          locked = false;
         }
-        return false;
+      });
+      func.apply(this, args);
     };
+  };
 
-    /**
-     * Textarea manager class.
-     */
-    var Completer = (function () {
-        var html, css, $baseWrapper, $baseList, _id;
+  var isString = function (obj) {
+    return Object.prototype.toString.call(obj) === '[object String]';
+  };
 
-        html = {
-            wrapper: '<div class="textcomplete-wrapper"></div>',
-            list: '<ul class="dropdown-menu"></ul>'
-        };
-        css = {
-            wrapper: {
-                position: 'relative'
-            },
-            list: {
-                position: 'absolute',
-                top: 0,
-                left: 0,
-                zIndex: '100',
-                display: 'none'
-            }
-        };
-        $baseWrapper = $(html.wrapper).css(css.wrapper);
-        $baseList = $(html.list).css(css.list);
-        _id = 0;
-
-        function Completer($el) {
-            var focus;
-            this.el = $el.get(0);  // textarea element
-            focus = this.el === document.activeElement;
-            // Cannot wrap $el at initialize method lazily due to Firefox's behavior.
-            this.$el = wrapElement($el); // Focus is lost
-            this.id = 'textComplete' + _id++;
-            this.strategies = [];
-            if (focus) {
-                this.initialize();
-                this.$el.focus();
-            } else {
-                this.$el.one('focus.textComplete', $.proxy(this.initialize, this));
+  var isFunction = function (obj) {
+    return Object.prototype.toString.call(obj) === '[object Function]';
+  };
+
+  var uniqueId = 0;
+
+  function Completer(element, option) {
+    this.$el        = $(element);
+    this.id         = 'textcomplete' + uniqueId++;
+    this.strategies = [];
+    this.views      = [];
+    this.option     = $.extend({}, Completer._getDefaults(), option);
+
+    if (!this.$el.is('input[type=text]') && !this.$el.is('input[type=search]') && !this.$el.is('textarea') && !element.isContentEditable && element.contentEditable != 'true') {
+      throw new Error('textcomplete must be called on a Textarea or a ContentEditable.');
+    }
+
+    // use ownerDocument to fix iframe / IE issues
+    if (element === element.ownerDocument.activeElement) {
+      // element has already been focused. Initialize view objects immediately.
+      this.initialize()
+    } else {
+      // Initialize view objects lazily.
+      var self = this;
+      this.$el.one('focus.' + this.id, function () { self.initialize(); });
+
+      // Special handling for CKEditor: lazy init on instance load
+      if ((!this.option.adapter || this.option.adapter == 'CKEditor') && typeof CKEDITOR != 'undefined' && (this.$el.is('textarea'))) {
+        CKEDITOR.on("instanceReady", function(event) {
+          event.editor.once("focus", function(event2) {
+            // replace the element with the Iframe element and flag it as CKEditor
+            self.$el = $(event.editor.editable().$);
+            if (!self.option.adapter) {
+              self.option.adapter = $.fn.textcomplete['CKEditor'];
             }
+            self.initialize();
+          });
+        });
+      }
+    }
+  }
+
+  Completer._getDefaults = function () {
+    if (!Completer.DEFAULTS) {
+      Completer.DEFAULTS = {
+        appendTo: $('body'),
+        className: '',  // deprecated option
+        dropdownClassName: 'dropdown-menu textcomplete-dropdown',
+        maxCount: 10,
+        zIndex: '100'
+      };
+    }
+
+    return Completer.DEFAULTS;
+  }
+
+  $.extend(Completer.prototype, {
+    // Public properties
+    // -----------------
+
+    id:         null,
+    option:     null,
+    strategies: null,
+    adapter:    null,
+    dropdown:   null,
+    $el:        null,
+    $iframe:    null,
+
+    // Public methods
+    // --------------
+
+    initialize: function () {
+      var element = this.$el.get(0);
+      
+      // check if we are in an iframe
+      // we need to alter positioning logic if using an iframe
+      if (this.$el.prop('ownerDocument') !== document && window.frames.length) {
+        for (var iframeIndex = 0; iframeIndex < window.frames.length; iframeIndex++) {
+          if (this.$el.prop('ownerDocument') === window.frames[iframeIndex].document) {
+            this.$iframe = $(window.frames[iframeIndex].frameElement);
+            break;
+          }
         }
+      }
+      
+      
+      // Initialize view objects.
+      this.dropdown = new $.fn.textcomplete.Dropdown(element, this, this.option);
+      var Adapter, viewName;
+      if (this.option.adapter) {
+        Adapter = this.option.adapter;
+      } else {
+        if (this.$el.is('textarea') || this.$el.is('input[type=text]') || this.$el.is('input[type=search]')) {
+          viewName = typeof element.selectionEnd === 'number' ? 'Textarea' : 'IETextarea';
+        } else {
+          viewName = 'ContentEditable';
+        }
+        Adapter = $.fn.textcomplete[viewName];
+      }
+      this.adapter = new Adapter(element, this, this.option);
+    },
 
-        /**
-         * Completer's public methods
-         */
-        $.extend(Completer.prototype, {
-
-            /**
-             * Prepare ListView and bind events.
-             */
-            initialize: function () {
-                var $list, globalEvents;
-                $list = $baseList.clone();
-                this.listView = new ListView($list, this);
-                this.$el
-                    .before($list)
-                    .on({
-                        'keyup.textComplete': $.proxy(this.onKeyup, this),
-                        'keydown.textComplete': $.proxy(this.listView.onKeydown,
-                            this.listView)
-                    });
-                globalEvents = {};
-                globalEvents['click.' + this.id] = $.proxy(this.onClickDocument, this);
-                globalEvents['keyup.' + this.id] = $.proxy(this.onKeyupDocument, this);
-                $(document).on(globalEvents);
-            },
-
-            /**
-             * Register strategies to the completer.
-             */
-            register: function (strategies) {
-                this.strategies = this.strategies.concat(strategies);
-            },
-
-            /**
-             * Show autocomplete list next to the caret.
-             */
-            renderList: function (data) {
-                if (this.clearAtNext) {
-                    this.listView.clear();
-                    this.clearAtNext = false;
-                }
-                if (data.length) {
-                    if (!this.listView.shown) {
-                        this.listView
-                            .setPosition(this.getCaretPosition())
-                            .clear()
-                            .activate();
-                        this.listView.strategy = this.strategy;
-                    }
-                    data = data.slice(0, this.strategy.maxCount);
-                    this.listView.render(data);
-                }
-
-                if (!this.listView.data.length && this.listView.shown) {
-                    this.listView.deactivate();
-                }
-            },
-
-            searchCallbackFactory: function (free) {
-                var self = this;
-                return function (data, keep) {
-                    self.renderList(data);
-                    if (!keep) {
-                        // This is the last callback for this search.
-                        free();
-                        self.clearAtNext = true;
-                    }
-                };
-            },
-
-            /**
-             * Keyup event handler.
-             */
-            onKeyup: function (e) {
-                var searchQuery, term;
-                if (this.skipSearch(e)) { return; }
-
-                searchQuery = this.extractSearchQuery(this.getTextFromHeadToCaret());
-                if (searchQuery.length) {
-                    term = searchQuery[1];
-                    if (this.term === term) return; // Ignore shift-key or something.
-                    this.term = term;
-                    this.search(searchQuery);
-                } else {
-                    this.term = null;
-                    this.listView.deactivate();
-                }
-            },
-
-            /**
-             * Suppress searching if it returns true.
-             */
-            skipSearch: function (e) {
-                if (this.skipNextKeyup) {
-                    this.skipNextKeyup = false;
-                    return true;
-                }
-                switch (e.keyCode) {
-                    case 40:
-                    case 38:
-                        return true;
-                }
-            },
-
-            onSelect: function (value) {
-                var pre, post, newSubStr;
-                pre = this.getTextFromHeadToCaret();
-                post = this.el.value.substring(this.el.selectionEnd);
-
-                newSubStr = this.strategy.replace(value);
-                if ($.isArray(newSubStr)) {
-                    post = newSubStr[1] + post;
-                    newSubStr = newSubStr[0];
-                }
-                pre = pre.replace(this.strategy.match, newSubStr);
-                this.$el.val(pre + post)
-                    .trigger('change')
-                    .trigger('textComplete:select', value);
-                this.el.focus();
-                this.el.selectionStart = this.el.selectionEnd = pre.length;
-                this.skipNextKeyup = true;
-            },
-
-            /**
-             * Global click event handler.
-             */
-            onClickDocument: function (e) {
-                if (e.originalEvent && !e.originalEvent.keepTextCompleteDropdown) {
-                    this.listView.deactivate();
-                }
-            },
-
-            /**
-             * Global keyup event handler.
-             */
-            onKeyupDocument: function (e) {
-                if (this.listView.shown && e.keyCode === 27) { // ESC
-                    this.listView.deactivate();
-                    this.$el.focus();
-                }
-            },
-
-            /**
-             * Remove all event handlers and the wrapper element.
-             */
-            destroy: function () {
-                var $wrapper;
-                this.$el.off('.textComplete');
-                $(document).off('.' + this.id);
-                if (this.listView) { this.listView.destroy(); }
-                $wrapper = this.$el.parent();
-                $wrapper.after(this.$el).remove();
-                this.$el.data('textComplete', void 0);
-                this.$el = null;
-            },
-
-            // Helper methods
-            // ==============
-
-            /**
-             * Returns caret's relative coordinates from textarea's left top corner.
-             */
-            getCaretPosition: function () {
-                // Browser native API does not provide the way to know the position of
-                // caret in pixels, so that here we use a kind of hack to accomplish
-                // the aim. First of all it puts a div element and completely copies
-                // the textarea's style to the element, then it inserts the text and a
-                // span element into the textarea.
-                // Consequently, the span element's position is the thing what we want.
-
-                if (this.el.selectionEnd === 0) return;
-                var properties, css, $div, $span, position, dir;
-
-                dir = this.$el.attr('dir') || this.$el.css('direction');
-                properties = ['border-width', 'font-family', 'font-size', 'font-style',
-                    'font-variant', 'font-weight', 'height', 'letter-spacing',
-                    'word-spacing', 'line-height', 'text-decoration', 'text-align',
-                    'width', 'padding-top', 'padding-right', 'padding-bottom',
-                    'padding-left', 'margin-top', 'margin-right', 'margin-bottom',
-                    'margin-left'
-                ];
-                css = $.extend({
-                    position: 'absolute',
-                    overflow: 'auto',
-                    'white-space': 'pre-wrap',
-                    top: 0,
-                    left: -9999,
-                    direction: dir
-                }, getStyles(this.$el, properties));
-
-                $div = $('<div></div>').css(css).text(this.getTextFromHeadToCaret());
-                $span = $('<span></span>').text('.').appendTo($div);
-                this.$el.before($div);
-                position = $span.position();
-                position.top += $span.height() - this.$el.scrollTop();
-                if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
-                $div.remove();
-                return position;
-            },
-
-            getTextFromHeadToCaret: function () {
-                var text, selectionEnd, range;
-                selectionEnd = this.el.selectionEnd;
-                if (typeof selectionEnd === 'number') {
-                    text = this.el.value.substring(0, selectionEnd);
-                } else if (document.selection) {
-                    range = this.el.createTextRange();
-                    range.moveStart('character', 0);
-                    range.moveEnd('textedit');
-                    text = range.text;
-                }
-                return text;
-            },
-
-            /**
-             * Parse the value of textarea and extract search query.
-             */
-            extractSearchQuery: function (text) {
-                // If a search query found, it returns used strategy and the query
-                // term. If the caret is currently in a code block or search query does
-                // not found, it returns an empty array.
-
-                var i, l, strategy, match;
-                for (i = 0, l = this.strategies.length; i < l; i++) {
-                    strategy = this.strategies[i];
-                    match = text.match(strategy.match);
-                    if (match) { return [strategy, match[strategy.index]]; }
-                }
-                return [];
-            },
-
-            search: lock(function (free, searchQuery) {
-                var term;
-                this.strategy = searchQuery[0];
-                term = searchQuery[1];
-                this.strategy.search(term, this.searchCallbackFactory(free));
-            })
-        });
+    destroy: function () {
+      this.$el.off('.' + this.id);
+      if (this.adapter) {
+        this.adapter.destroy();
+      }
+      if (this.dropdown) {
+        this.dropdown.destroy();
+      }
+      this.$el = this.adapter = this.dropdown = null;
+    },
 
-        /**
-         * Completer's private functions
-         */
-        var wrapElement = function ($el) {
-            return $el.wrap($baseWrapper.clone().css('display', $el.css('display')));
-        };
+    deactivate: function () {
+      if (this.dropdown) {
+        this.dropdown.deactivate();
+      }
+    },
+
+    // Invoke textcomplete.
+    trigger: function (text, skipUnchangedTerm) {
+      if (!this.dropdown) { this.initialize(); }
+      text != null || (text = this.adapter.getTextFromHeadToCaret());
+      var searchQuery = this._extractSearchQuery(text);
+      if (searchQuery.length) {
+        var term = searchQuery[1];
+        // Ignore shift-key, ctrl-key and so on.
+        if (skipUnchangedTerm && this._term === term && term !== "") { return; }
+        this._term = term;
+        this._search.apply(this, searchQuery);
+      } else {
+        this._term = null;
+        this.dropdown.deactivate();
+      }
+    },
+
+    fire: function (eventName) {
+      var args = Array.prototype.slice.call(arguments, 1);
+      this.$el.trigger(eventName, args);
+      return this;
+    },
 
-        return Completer;
-    })();
+    register: function (strategies) {
+      Array.prototype.push.apply(this.strategies, strategies);
+    },
 
-    /**
-     * Dropdown menu manager class.
-     */
-    var ListView = (function () {
+    // Insert the value into adapter view. It is called when the dropdown is clicked
+    // or selected.
+    //
+    // value    - The selected element of the array callbacked from search func.
+    // strategy - The Strategy object.
+    // e        - Click or keydown event object.
+    select: function (value, strategy, e) {
+      this._term = null;
+      this.adapter.select(value, strategy, e);
+      this.fire('change').fire('textComplete:select', value, strategy);
+      this.adapter.focus();
+    },
 
-        function ListView($el, completer) {
-            this.data = [];
-            this.$el = $el;
-            this.index = 0;
-            this.completer = completer;
+    // Private properties
+    // ------------------
 
-            this.$el.on('click.textComplete', 'li.textcomplete-item',
-                $.proxy(this.onClick, this));
+    _clearAtNext: true,
+    _term:        null,
+
+    // Private methods
+    // ---------------
+
+    // Parse the given text and extract the first matching strategy.
+    //
+    // Returns an array including the strategy, the query term and the match
+    // object if the text matches an strategy; otherwise returns an empty array.
+    _extractSearchQuery: function (text) {
+      for (var i = 0; i < this.strategies.length; i++) {
+        var strategy = this.strategies[i];
+        var context = strategy.context(text);
+        if (context || context === '') {
+          var matchRegexp = isFunction(strategy.match) ? strategy.match(text) : strategy.match;
+          if (isString(context)) { text = context; }
+          var match = text.match(matchRegexp);
+          if (match) { return [strategy, match[strategy.index], match]; }
         }
+      }
+      return []
+    },
 
-        $.extend(ListView.prototype, {
-            shown: false,
-
-            render: function (data) {
-                var html, i, l, index, val;
-
-                html = '';
-                for (i = 0, l = data.length; i < l; i++) {
-                    val = data[i];
-                    if (include(this.data, val)) continue;
-                    index = this.data.length;
-                    this.data.push(val);
-                    html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
-                    html +=   this.strategy.template(val);
-                    html += '</a></li>';
-                    if (this.data.length === this.strategy.maxCount) break;
-                }
-                this.$el.append(html);
-                if (!this.data.length) {
-                    this.deactivate();
-                } else {
-                    this.activateIndexedItem();
-                }
-            },
-
-            clear: function () {
-                this.data = [];
-                this.$el.html('');
-                this.index = 0;
-                return this;
-            },
-
-            activateIndexedItem: function () {
-                this.$el.find('.active').removeClass('active');
-                this.getActiveItem().addClass('active');
-            },
-
-            getActiveItem: function () {
-                return $(this.$el.children().get(this.index));
-            },
-
-            activate: function () {
-                if (!this.shown) {
-                    this.$el.show();
-                    this.completer.$el.trigger('textComplete:show');
-                    this.shown = true;
-                }
-                return this;
-            },
-
-            deactivate: function () {
-                if (this.shown) {
-                    this.$el.hide();
-                    this.completer.$el.trigger('textComplete:hide');
-                    this.shown = false;
-                    this.data = this.index = null;
-                }
-                return this;
-            },
-
-            setPosition: function (position) {
-                this.$el.css(position);
-                return this;
-            },
-
-            select: function (index) {
-                var self = this;
-                this.completer.onSelect(this.data[index]);
-                // Deactive at next tick to allow other event handlers to know whether
-                // the dropdown has been shown or not.
-                setTimeout(function () { self.deactivate(); }, 0);
-            },
-
-            onKeydown: function (e) {
-                if (!this.shown) return;
-                if (e.keyCode === 38) {         // UP
-                    e.preventDefault();
-                    if (this.index === 0) {
-                        this.index = this.data.length-1;
-                    } else {
-                        this.index -= 1;
-                    }
-                    this.activateIndexedItem();
-                } else if (e.keyCode === 40) {  // DOWN
-                    e.preventDefault();
-                    if (this.index === this.data.length - 1) {
-                        this.index = 0;
-                    } else {
-                        this.index += 1;
-                    }
-                    this.activateIndexedItem();
-                } else if (e.keyCode === 13 || e.keyCode === 9) {  // ENTER or TAB
-                    e.preventDefault();
-                    this.select(parseInt(this.getActiveItem().data('index'), 10));
-                }
-            },
-
-            onClick: function (e) {
-                var $e = $(e.target);
-                e.originalEvent.keepTextCompleteDropdown = true;
-                if (!$e.hasClass('textcomplete-item')) {
-                    $e = $e.parents('li.textcomplete-item');
-                }
-                this.select(parseInt($e.data('index'), 10));
-            },
-
-            destroy: function () {
-                this.deactivate();
-                this.$el.off('click.textComplete').remove();
-                this.$el = null;
-            }
-        });
+    // Call the search method of selected strategy..
+    _search: lock(function (free, strategy, term, match) {
+      var self = this;
+      strategy.search(term, function (data, stillSearching) {
+        if (!self.dropdown.shown) {
+          self.dropdown.activate();
+        }
+        if (self._clearAtNext) {
+          // The first callback in the current lock.
+          self.dropdown.clear();
+          self._clearAtNext = false;
+        }
+        self.dropdown.setPosition(self.adapter.getCaretPosition());
+        self.dropdown.render(self._zip(data, strategy, term));
+        if (!stillSearching) {
+          // The last callback in the current lock.
+          free();
+          self._clearAtNext = true; // Call dropdown.clear at the next time.
+        }
+      }, match);
+    }),
 
-        return ListView;
-    })();
+    // Build a parameter for Dropdown#render.
+    //
+    // Examples
+    //
+    //  this._zip(['a', 'b'], 's');
+    //  //=> [{ value: 'a', strategy: 's' }, { value: 'b', strategy: 's' }]
+    _zip: function (data, strategy, term) {
+      return $.map(data, function (value) {
+        return { value: value, strategy: strategy, term: term };
+      });
+    }
+  });
 
-    $.fn.textcomplete = function (strategies) {
-        var i, l, strategy, dataKey;
+  $.fn.textcomplete.Completer = Completer;
+}(jQuery);
 
-        dataKey = 'textComplete';
++function ($) {
+  'use strict';
 
-        if (strategies === 'destroy') {
-            return this.each(function () {
-                var completer = $(this).data(dataKey);
-                if (completer) { completer.destroy(); }
-            });
+  var $window = $(window);
+
+  var include = function (zippedData, datum) {
+    var i, elem;
+    var idProperty = datum.strategy.idProperty
+    for (i = 0; i < zippedData.length; i++) {
+      elem = zippedData[i];
+      if (elem.strategy !== datum.strategy) continue;
+      if (idProperty) {
+        if (elem.value[idProperty] === datum.value[idProperty]) return true;
+      } else {
+        if (elem.value === datum.value) return true;
+      }
+    }
+    return false;
+  };
+
+  var dropdownViews = {};
+  $(document).on('click', function (e) {
+    var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
+    $.each(dropdownViews, function (key, view) {
+      if (key !== id) { view.deactivate(); }
+    });
+  });
+
+  var commands = {
+    SKIP_DEFAULT: 0,
+    KEY_UP: 1,
+    KEY_DOWN: 2,
+    KEY_ENTER: 3,
+    KEY_PAGEUP: 4,
+    KEY_PAGEDOWN: 5,
+    KEY_ESCAPE: 6
+  };
+
+  // Dropdown view
+  // =============
+
+  // Construct Dropdown object.
+  //
+  // element - Textarea or contenteditable element.
+  function Dropdown(element, completer, option) {
+    this.$el       = Dropdown.createElement(option);
+    this.completer = completer;
+    this.id        = completer.id + 'dropdown';
+    this._data     = []; // zipped data.
+    this.$inputEl  = $(element);
+    this.option    = option;
+
+    // Override setPosition method.
+    if (option.listPosition) { this.setPosition = option.listPosition; }
+    if (option.height) { this.$el.height(option.height); }
+    var self = this;
+    $.each(['maxCount', 'placement', 'footer', 'header', 'noResultsMessage', 'className'], function (_i, name) {
+      if (option[name] != null) { self[name] = option[name]; }
+    });
+    this._bindEvents(element);
+    dropdownViews[this.id] = this;
+  }
+
+  $.extend(Dropdown, {
+    // Class methods
+    // -------------
+
+    createElement: function (option) {
+      var $parent = option.appendTo;
+      if (!($parent instanceof $)) { $parent = $($parent); }
+      var $el = $('<ul></ul>')
+        .addClass(option.dropdownClassName)
+        .attr('id', 'textcomplete-dropdown-' + option._oid)
+        .css({
+          display: 'none',
+          left: 0,
+          position: 'absolute',
+          zIndex: option.zIndex
+        })
+        .appendTo($parent);
+      return $el;
+    }
+  });
+
+  $.extend(Dropdown.prototype, {
+    // Public properties
+    // -----------------
+
+    $el:       null,  // jQuery object of ul.dropdown-menu element.
+    $inputEl:  null,  // jQuery object of target textarea.
+    completer: null,
+    footer:    null,
+    header:    null,
+    id:        null,
+    maxCount:  null,
+    placement: '',
+    shown:     false,
+    data:      [],     // Shown zipped data.
+    className: '',
+
+    // Public methods
+    // --------------
+
+    destroy: function () {
+      // Don't remove $el because it may be shared by several textcompletes.
+      this.deactivate();
+
+      this.$el.off('.' + this.id);
+      this.$inputEl.off('.' + this.id);
+      this.clear();
+      this.$el.remove();
+      this.$el = this.$inputEl = this.completer = null;
+      delete dropdownViews[this.id]
+    },
+
+    render: function (zippedData) {
+      var contentsHtml = this._buildContents(zippedData);
+      var unzippedData = $.map(this.data, function (d) { return d.value; });
+      if (this.data.length) {
+        var strategy = zippedData[0].strategy;
+        if (strategy.id) {
+          this.$el.attr('data-strategy', strategy.id);
+        } else {
+          this.$el.removeAttr('data-strategy');
         }
+        this._renderHeader(unzippedData);
+        this._renderFooter(unzippedData);
+        if (contentsHtml) {
+          this._renderContents(contentsHtml);
+          this._fitToBottom();
+          this._fitToRight();
+          this._activateIndexedItem();
+        }
+        this._setScroll();
+      } else if (this.noResultsMessage) {
+        this._renderNoResultsMessage(unzippedData);
+      } else if (this.shown) {
+        this.deactivate();
+      }
+    },
 
-        for (i = 0, l = strategies.length; i < l; i++) {
-            strategy = strategies[i];
-            if (!strategy.template) {
-                strategy.template = identity;
-            }
-            if (strategy.index == null) {
-                strategy.index = 2;
-            }
-            if (strategy.cache) {
-                strategy.search = memoize(strategy.search);
-            }
-            strategy.maxCount || (strategy.maxCount = 10);
+    setPosition: function (pos) {
+      // Make the dropdown fixed if the input is also fixed
+      // This can't be done during init, as textcomplete may be used on multiple elements on the same page
+      // Because the same dropdown is reused behind the scenes, we need to recheck every time the dropdown is showed
+      var position = 'absolute';
+      // Check if input or one of its parents has positioning we need to care about
+      this.$inputEl.add(this.$inputEl.parents()).each(function() {
+        if($(this).css('position') === 'absolute') // The element has absolute positioning, so it's all OK
+          return false;
+        if($(this).css('position') === 'fixed') {
+          pos.top -= $window.scrollTop();
+          pos.left -= $window.scrollLeft();					
+          position = 'fixed';
+          return false;
         }
+      });
+      this.$el.css(this._applyPlacement(pos));
+      this.$el.css({ position: position }); // Update positioning
 
-        return this.each(function () {
-            var $this, completer;
-            $this = $(this);
-            completer = $this.data(dataKey);
-            if (!completer) {
-                completer = new Completer($this);
-                $this.data(dataKey, completer);
-            }
-            completer.register(strategies);
+      return this;
+    },
+
+    clear: function () {
+      this.$el.html('');
+      this.data = [];
+      this._index = 0;
+      this._$header = this._$footer = this._$noResultsMessage = null;
+    },
+
+    activate: function () {
+      if (!this.shown) {
+        this.clear();
+        this.$el.show();
+        if (this.className) { this.$el.addClass(this.className); }
+        this.completer.fire('textComplete:show');
+        this.shown = true;
+      }
+      return this;
+    },
+
+    deactivate: function () {
+      if (this.shown) {
+        this.$el.hide();
+        if (this.className) { this.$el.removeClass(this.className); }
+        this.completer.fire('textComplete:hide');
+        this.shown = false;
+      }
+      return this;
+    },
+
+    isUp: function (e) {
+      return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80);  // UP, Ctrl-P
+    },
+
+    isDown: function (e) {
+      return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78);  // DOWN, Ctrl-N
+    },
+
+    isEnter: function (e) {
+      var modifiers = e.ctrlKey || e.altKey || e.metaKey || e.shiftKey;
+      return !modifiers && (e.keyCode === 13 || e.keyCode === 9 || (this.option.completeOnSpace === true && e.keyCode === 32))  // ENTER, TAB
+    },
+
+    isPageup: function (e) {
+      return e.keyCode === 33;  // PAGEUP
+    },
+
+    isPagedown: function (e) {
+      return e.keyCode === 34;  // PAGEDOWN
+    },
+
+    isEscape: function (e) {
+      return e.keyCode === 27;  // ESCAPE
+    },
+
+    // Private properties
+    // ------------------
+
+    _data:    null,  // Currently shown zipped data.
+    _index:   null,
+    _$header: null,
+    _$noResultsMessage: null,
+    _$footer: null,
+
+    // Private methods
+    // ---------------
+
+    _bindEvents: function () {
+      this.$el.on('mousedown.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
+      this.$el.on('touchstart.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
+      this.$el.on('mouseover.' + this.id, '.textcomplete-item', $.proxy(this._onMouseover, this));
+      this.$inputEl.on('keydown.' + this.id, $.proxy(this._onKeydown, this));
+    },
+
+    _onClick: function (e) {
+      var $el = $(e.target);
+      e.preventDefault();
+      e.originalEvent.keepTextCompleteDropdown = this.id;
+      if (!$el.hasClass('textcomplete-item')) {
+        $el = $el.closest('.textcomplete-item');
+      }
+      var datum = this.data[parseInt($el.data('index'), 10)];
+      this.completer.select(datum.value, datum.strategy, e);
+      var self = this;
+      // Deactive at next tick to allow other event handlers to know whether
+      // the dropdown has been shown or not.
+      setTimeout(function () {
+        self.deactivate();
+        if (e.type === 'touchstart') {
+          self.$inputEl.focus();
+        }
+      }, 0);
+    },
+
+    // Activate hovered item.
+    _onMouseover: function (e) {
+      var $el = $(e.target);
+      e.preventDefault();
+      if (!$el.hasClass('textcomplete-item')) {
+        $el = $el.closest('.textcomplete-item');
+      }
+      this._index = parseInt($el.data('index'), 10);
+      this._activateIndexedItem();
+    },
+
+    _onKeydown: function (e) {
+      if (!this.shown) { return; }
+
+      var command;
+
+      if ($.isFunction(this.option.onKeydown)) {
+        command = this.option.onKeydown(e, commands);
+      }
+
+      if (command == null) {
+        command = this._defaultKeydown(e);
+      }
+
+      switch (command) {
+        case commands.KEY_UP:
+          e.preventDefault();
+          this._up();
+          break;
+        case commands.KEY_DOWN:
+          e.preventDefault();
+          this._down();
+          break;
+        case commands.KEY_ENTER:
+          e.preventDefault();
+          this._enter(e);
+          break;
+        case commands.KEY_PAGEUP:
+          e.preventDefault();
+          this._pageup();
+          break;
+        case commands.KEY_PAGEDOWN:
+          e.preventDefault();
+          this._pagedown();
+          break;
+        case commands.KEY_ESCAPE:
+          e.preventDefault();
+          this.deactivate();
+          break;
+      }
+    },
+
+    _defaultKeydown: function (e) {
+      if (this.isUp(e)) {
+        return commands.KEY_UP;
+      } else if (this.isDown(e)) {
+        return commands.KEY_DOWN;
+      } else if (this.isEnter(e)) {
+        return commands.KEY_ENTER;
+      } else if (this.isPageup(e)) {
+        return commands.KEY_PAGEUP;
+      } else if (this.isPagedown(e)) {
+        return commands.KEY_PAGEDOWN;
+      } else if (this.isEscape(e)) {
+        return commands.KEY_ESCAPE;
+      }
+    },
+
+    _up: function () {
+      if (this._index === 0) {
+        this._index = this.data.length - 1;
+      } else {
+        this._index -= 1;
+      }
+      this._activateIndexedItem();
+      this._setScroll();
+    },
+
+    _down: function () {
+      if (this._index === this.data.length - 1) {
+        this._index = 0;
+      } else {
+        this._index += 1;
+      }
+      this._activateIndexedItem();
+      this._setScroll();
+    },
+
+    _enter: function (e) {
+      var datum = this.data[parseInt(this._getActiveElement().data('index'), 10)];
+      this.completer.select(datum.value, datum.strategy, e);
+      this.deactivate();
+    },
+
+    _pageup: function () {
+      var target = 0;
+      var threshold = this._getActiveElement().position().top - this.$el.innerHeight();
+      this.$el.children().each(function (i) {
+        if ($(this).position().top + $(this).outerHeight() > threshold) {
+          target = i;
+          return false;
+        }
+      });
+      this._index = target;
+      this._activateIndexedItem();
+      this._setScroll();
+    },
+
+    _pagedown: function () {
+      var target = this.data.length - 1;
+      var threshold = this._getActiveElement().position().top + this.$el.innerHeight();
+      this.$el.children().each(function (i) {
+        if ($(this).position().top > threshold) {
+          target = i;
+          return false
+        }
+      });
+      this._index = target;
+      this._activateIndexedItem();
+      this._setScroll();
+    },
+
+    _activateIndexedItem: function () {
+      this.$el.find('.textcomplete-item.active').removeClass('active');
+      this._getActiveElement().addClass('active');
+    },
+
+    _getActiveElement: function () {
+      return this.$el.children('.textcomplete-item:nth(' + this._index + ')');
+    },
+
+    _setScroll: function () {
+      var $activeEl = this._getActiveElement();
+      var itemTop = $activeEl.position().top;
+      var itemHeight = $activeEl.outerHeight();
+      var visibleHeight = this.$el.innerHeight();
+      var visibleTop = this.$el.scrollTop();
+      if (this._index === 0 || this._index == this.data.length - 1 || itemTop < 0) {
+        this.$el.scrollTop(itemTop + visibleTop);
+      } else if (itemTop + itemHeight > visibleHeight) {
+        this.$el.scrollTop(itemTop + itemHeight + visibleTop - visibleHeight);
+      }
+    },
+
+    _buildContents: function (zippedData) {
+      var datum, i, index;
+      var html = '';
+      for (i = 0; i < zippedData.length; i++) {
+        if (this.data.length === this.maxCount) break;
+        datum = zippedData[i];
+        if (include(this.data, datum)) { continue; }
+        index = this.data.length;
+        this.data.push(datum);
+        html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
+        html +=   datum.strategy.template(datum.value, datum.term);
+        html += '</a></li>';
+      }
+      return html;
+    },
+
+    _renderHeader: function (unzippedData) {
+      if (this.header) {
+        if (!this._$header) {
+          this._$header = $('<li class="textcomplete-header"></li>').prependTo(this.$el);
+        }
+        var html = $.isFunction(this.header) ? this.header(unzippedData) : this.header;
+        this._$header.html(html);
+      }
+    },
+
+    _renderFooter: function (unzippedData) {
+      if (this.footer) {
+        if (!this._$footer) {
+          this._$footer = $('<li class="textcomplete-footer"></li>').appendTo(this.$el);
+        }
+        var html = $.isFunction(this.footer) ? this.footer(unzippedData) : this.footer;
+        this._$footer.html(html);
+      }
+    },
+
+    _renderNoResultsMessage: function (unzippedData) {
+      if (this.noResultsMessage) {
+        if (!this._$noResultsMessage) {
+          this._$noResultsMessage = $('<li class="textcomplete-no-results-message"></li>').appendTo(this.$el);
+        }
+        var html = $.isFunction(this.noResultsMessage) ? this.noResultsMessage(unzippedData) : this.noResultsMessage;
+        this._$noResultsMessage.html(html);
+      }
+    },
+
+    _renderContents: function (html) {
+      if (this._$footer) {
+        this._$footer.before(html);
+      } else {
+        this.$el.append(html);
+      }
+    },
+
+    _fitToBottom: function() {
+      var windowScrollBottom = $window.scrollTop() + $window.height();
+      var height = this.$el.height();
+      if ((this.$el.position().top + height) > windowScrollBottom) {
+        // only do this if we are not in an iframe
+        if (!this.completer.$iframe) {
+          this.$el.offset({top: windowScrollBottom - height});
+        }
+      }
+    },
+
+    _fitToRight: function() {
+      // We don't know how wide our content is until the browser positions us, and at that point it clips us
+      // to the document width so we don't know if we would have overrun it. As a heuristic to avoid that clipping
+      // (which makes our elements wrap onto the next line and corrupt the next item), if we're close to the right
+      // edge, move left. We don't know how far to move left, so just keep nudging a bit.
+      var tolerance = 30; // pixels. Make wider than vertical scrollbar because we might not be able to use that space.
+      var lastOffset = this.$el.offset().left, offset;
+      var width = this.$el.width();
+      var maxLeft = $window.width() - tolerance;
+      while (lastOffset + width > maxLeft) {
+        this.$el.offset({left: lastOffset - tolerance});
+        offset = this.$el.offset().left;
+        if (offset >= lastOffset) { break; }
+        lastOffset = offset;
+      }
+    },
+
+    _applyPlacement: function (position) {
+      // If the 'placement' option set to 'top', move the position above the element.
+      if (this.placement.indexOf('top') !== -1) {
+        // Overwrite the position object to set the 'bottom' property instead of the top.
+        position = {
+          top: 'auto',
+          bottom: this.$el.parent().height() - position.top + position.lineHeight,
+          left: position.left
+        };
+      } else {
+        position.bottom = 'auto';
+        delete position.lineHeight;
+      }
+      if (this.placement.indexOf('absleft') !== -1) {
+        position.left = 0;
+      } else if (this.placement.indexOf('absright') !== -1) {
+        position.right = 0;
+        position.left = 'auto';
+      }
+      return position;
+    }
+  });
+
+  $.fn.textcomplete.Dropdown = Dropdown;
+  $.extend($.fn.textcomplete, commands);
+}(jQuery);
+
++function ($) {
+  'use strict';
+
+  // Memoize a search function.
+  var memoize = function (func) {
+    var memo = {};
+    return function (term, callback) {
+      if (memo[term]) {
+        callback(memo[term]);
+      } else {
+        func.call(this, term, function (data) {
+          memo[term] = (memo[term] || []).concat(data);
+          callback.apply(null, arguments);
         });
+      }
     };
+  };
+
+  function Strategy(options) {
+    $.extend(this, options);
+    if (this.cache) { this.search = memoize(this.search); }
+  }
+
+  Strategy.parse = function (strategiesArray, params) {
+    return $.map(strategiesArray, function (strategy) {
+      var strategyObj = new Strategy(strategy);
+      strategyObj.el = params.el;
+      strategyObj.$el = params.$el;
+      return strategyObj;
+    });
+  };
+
+  $.extend(Strategy.prototype, {
+    // Public properties
+    // -----------------
+
+    // Required
+    match:      null,
+    replace:    null,
+    search:     null,
+
+    // Optional
+    id:         null,
+    cache:      false,
+    context:    function () { return true; },
+    index:      2,
+    template:   function (obj) { return obj; },
+    idProperty: null
+  });
+
+  $.fn.textcomplete.Strategy = Strategy;
+
+}(jQuery);
+
++function ($) {
+  'use strict';
+
+  var now = Date.now || function () { return new Date().getTime(); };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // `wait` msec.
+  //
+  // This utility function was originally implemented at Underscore.js.
+  var debounce = function (func, wait) {
+    var timeout, args, context, timestamp, result;
+    var later = function () {
+      var last = now() - timestamp;
+      if (last < wait) {
+        timeout = setTimeout(later, wait - last);
+      } else {
+        timeout = null;
+        result = func.apply(context, args);
+        context = args = null;
+      }
+    };
+
+    return function () {
+      context = this;
+      args = arguments;
+      timestamp = now();
+      if (!timeout) {
+        timeout = setTimeout(later, wait);
+      }
+      return result;
+    };
+  };
+
+  function Adapter () {}
+
+  $.extend(Adapter.prototype, {
+    // Public properties
+    // -----------------
+
+    id:        null, // Identity.
+    completer: null, // Completer object which creates it.
+    el:        null, // Textarea element.
+    $el:       null, // jQuery object of the textarea.
+    option:    null,
+
+    // Public methods
+    // --------------
+
+    initialize: function (element, completer, option) {
+      this.el        = element;
+      this.$el       = $(element);
+      this.id        = completer.id + this.constructor.name;
+      this.completer = completer;
+      this.option    = option;
+
+      if (this.option.debounce) {
+        this._onKeyup = debounce(this._onKeyup, this.option.debounce);
+      }
+
+      this._bindEvents();
+    },
+
+    destroy: function () {
+      this.$el.off('.' + this.id); // Remove all event handlers.
+      this.$el = this.el = this.completer = null;
+    },
+
+    // Update the element with the given value and strategy.
+    //
+    // value    - The selected object. It is one of the item of the array
+    //            which was callbacked from the search function.
+    // strategy - The Strategy associated with the selected value.
+    select: function (/* value, strategy */) {
+      throw new Error('Not implemented');
+    },
+
+    // Returns the caret's relative coordinates from body's left top corner.
+    getCaretPosition: function () {
+      var position = this._getCaretRelativePosition();
+      var offset = this.$el.offset();
+
+      // Calculate the left top corner of `this.option.appendTo` element.
+      var $parent = this.option.appendTo;
+      if ($parent) {
+         if (!($parent instanceof $)) { $parent = $($parent); }
+         var parentOffset = $parent.offsetParent().offset();
+         offset.top -= parentOffset.top;
+         offset.left -= parentOffset.left;
+      }
+
+      position.top += offset.top;
+      position.left += offset.left;
+      return position;
+    },
+
+    // Focus on the element.
+    focus: function () {
+      this.$el.focus();
+    },
+
+    // Private methods
+    // ---------------
+
+    _bindEvents: function () {
+      this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
+    },
+
+    _onKeyup: function (e) {
+      if (this._skipSearch(e)) { return; }
+      this.completer.trigger(this.getTextFromHeadToCaret(), true);
+    },
+
+    // Suppress searching if it returns true.
+    _skipSearch: function (clickEvent) {
+      switch (clickEvent.keyCode) {
+        case 9:  // TAB
+        case 13: // ENTER
+        case 40: // DOWN
+        case 38: // UP
+          return true;
+      }
+      if (clickEvent.ctrlKey) switch (clickEvent.keyCode) {
+        case 78: // Ctrl-N
+        case 80: // Ctrl-P
+          return true;
+      }
+    }
+  });
+
+  $.fn.textcomplete.Adapter = Adapter;
+}(jQuery);
+
++function ($) {
+  'use strict';
+
+  // Textarea adapter
+  // ================
+  //
+  // Managing a textarea. It doesn't know a Dropdown.
+  function Textarea(element, completer, option) {
+    this.initialize(element, completer, option);
+  }
+
+  $.extend(Textarea.prototype, $.fn.textcomplete.Adapter.prototype, {
+    // Public methods
+    // --------------
+
+    // Update the textarea with the given value and strategy.
+    select: function (value, strategy, e) {
+      var pre = this.getTextFromHeadToCaret();
+      var post = this.el.value.substring(this.el.selectionEnd);
+      var newSubstr = strategy.replace(value, e);
+      if (typeof newSubstr !== 'undefined') {
+        if ($.isArray(newSubstr)) {
+          post = newSubstr[1] + post;
+          newSubstr = newSubstr[0];
+        }
+        pre = pre.replace(strategy.match, newSubstr);
+        this.$el.val(pre + post);
+        this.el.selectionStart = this.el.selectionEnd = pre.length;
+      }
+    },
+
+    getTextFromHeadToCaret: function () {
+      return this.el.value.substring(0, this.el.selectionEnd);
+    },
+
+    // Private methods
+    // ---------------
+
+    _getCaretRelativePosition: function () {
+      var p = $.fn.textcomplete.getCaretCoordinates(this.el, this.el.selectionStart);
+      return {
+        top: p.top + this._calculateLineHeight() - this.$el.scrollTop(),
+        left: p.left - this.$el.scrollLeft()
+      };
+    },
+
+    _calculateLineHeight: function () {
+      var lineHeight = parseInt(this.$el.css('line-height'), 10);
+      if (isNaN(lineHeight)) {
+        // http://stackoverflow.com/a/4515470/1297336
+        var parentNode = this.el.parentNode;
+        var temp = document.createElement(this.el.nodeName);
+        var style = this.el.style;
+        temp.setAttribute(
+          'style',
+          'margin:0px;padding:0px;font-family:' + style.fontFamily + ';font-size:' + style.fontSize
+        );
+        temp.innerHTML = 'test';
+        parentNode.appendChild(temp);
+        lineHeight = temp.clientHeight;
+        parentNode.removeChild(temp);
+      }
+      return lineHeight;
+    }
+  });
+
+  $.fn.textcomplete.Textarea = Textarea;
+}(jQuery);
+
++function ($) {
+  'use strict';
+
+  var sentinelChar = '吶';
+
+  function IETextarea(element, completer, option) {
+    this.initialize(element, completer, option);
+    $('<span>' + sentinelChar + '</span>').css({
+      position: 'absolute',
+      top: -9999,
+      left: -9999
+    }).insertBefore(element);
+  }
+
+  $.extend(IETextarea.prototype, $.fn.textcomplete.Textarea.prototype, {
+    // Public methods
+    // --------------
+
+    select: function (value, strategy, e) {
+      var pre = this.getTextFromHeadToCaret();
+      var post = this.el.value.substring(pre.length);
+      var newSubstr = strategy.replace(value, e);
+      if (typeof newSubstr !== 'undefined') {
+        if ($.isArray(newSubstr)) {
+          post = newSubstr[1] + post;
+          newSubstr = newSubstr[0];
+        }
+        pre = pre.replace(strategy.match, newSubstr);
+        this.$el.val(pre + post);
+        this.el.focus();
+        var range = this.el.createTextRange();
+        range.collapse(true);
+        range.moveEnd('character', pre.length);
+        range.moveStart('character', pre.length);
+        range.select();
+      }
+    },
+
+    getTextFromHeadToCaret: function () {
+      this.el.focus();
+      var range = document.selection.createRange();
+      range.moveStart('character', -this.el.value.length);
+      var arr = range.text.split(sentinelChar)
+      return arr.length === 1 ? arr[0] : arr[1];
+    }
+  });
+
+  $.fn.textcomplete.IETextarea = IETextarea;
+}(jQuery);
+
+// NOTE: TextComplete plugin has contenteditable support but it does not work
+//       fine especially on old IEs.
+//       Any pull requests are REALLY welcome.
+
++function ($) {
+  'use strict';
+
+  // ContentEditable adapter
+  // =======================
+  //
+  // Adapter for contenteditable elements.
+  function ContentEditable (element, completer, option) {
+    this.initialize(element, completer, option);
+  }
+
+  $.extend(ContentEditable.prototype, $.fn.textcomplete.Adapter.prototype, {
+    // Public methods
+    // --------------
+
+    // Update the content with the given value and strategy.
+    // When an dropdown item is selected, it is executed.
+    select: function (value, strategy, e) {
+      var pre = this.getTextFromHeadToCaret();
+      // use ownerDocument instead of window to support iframes
+      var sel = this.el.ownerDocument.getSelection();
+      
+      var range = sel.getRangeAt(0);
+      var selection = range.cloneRange();
+      selection.selectNodeContents(range.startContainer);
+      var content = selection.toString();
+      var post = content.substring(range.startOffset);
+      var newSubstr = strategy.replace(value, e);
+      if (typeof newSubstr !== 'undefined') {
+        if ($.isArray(newSubstr)) {
+          post = newSubstr[1] + post;
+          newSubstr = newSubstr[0];
+        }
+        pre = pre.replace(strategy.match, newSubstr)
+            .replace(/ $/, "&nbsp"); // &nbsp necessary at least for CKeditor to not eat spaces
+        range.selectNodeContents(range.startContainer);
+        range.deleteContents();
+        
+        // create temporary elements
+        var preWrapper = this.el.ownerDocument.createElement("div");
+        preWrapper.innerHTML = pre;
+        var postWrapper = this.el.ownerDocument.createElement("div");
+        postWrapper.innerHTML = post;
+        
+        // create the fragment thats inserted
+        var fragment = this.el.ownerDocument.createDocumentFragment();
+        var childNode;
+        var lastOfPre;
+        while (childNode = preWrapper.firstChild) {
+        	lastOfPre = fragment.appendChild(childNode);
+        }
+        while (childNode = postWrapper.firstChild) {
+        	fragment.appendChild(childNode);
+        }
+        
+        // insert the fragment & jump behind the last node in "pre"
+        range.insertNode(fragment);
+        range.setStartAfter(lastOfPre);
+        
+        range.collapse(true);
+        sel.removeAllRanges();
+        sel.addRange(range);
+      }
+    },
+
+    // Private methods
+    // ---------------
+
+    // Returns the caret's relative position from the contenteditable's
+    // left top corner.
+    //
+    // Examples
+    //
+    //   this._getCaretRelativePosition()
+    //   //=> { top: 18, left: 200, lineHeight: 16 }
+    //
+    // Dropdown's position will be decided using the result.
+    _getCaretRelativePosition: function () {
+      var range = this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange();
+      var node = this.el.ownerDocument.createElement('span');
+      range.insertNode(node);
+      range.selectNodeContents(node);
+      range.deleteContents();
+      var $node = $(node);
+      var position = $node.offset();
+      position.left -= this.$el.offset().left;
+      position.top += $node.height() - this.$el.offset().top;
+      position.lineHeight = $node.height();
+      
+      // special positioning logic for iframes
+      // this is typically used for contenteditables such as tinymce or ckeditor
+      if (this.completer.$iframe) {
+        var iframePosition = this.completer.$iframe.offset();
+        position.top += iframePosition.top;
+        position.left += iframePosition.left;
+        //subtract scrollTop from element in iframe
+        position.top -= this.$el.scrollTop(); 
+      }
+      
+      $node.remove();
+      return position;
+    },
+
+    // Returns the string between the first character and the caret.
+    // Completer will be triggered with the result for start autocompleting.
+    //
+    // Example
+    //
+    //   // Suppose the html is '<b>hello</b> wor|ld' and | is the caret.
+    //   this.getTextFromHeadToCaret()
+    //   // => ' wor'  // not '<b>hello</b> wor'
+    getTextFromHeadToCaret: function () {
+      var range = this.el.ownerDocument.getSelection().getRangeAt(0);
+      var selection = range.cloneRange();
+      selection.selectNodeContents(range.startContainer);
+      return selection.toString().substring(0, range.startOffset);
+    }
+  });
+
+  $.fn.textcomplete.ContentEditable = ContentEditable;
+}(jQuery);
+
+// NOTE: TextComplete plugin has contenteditable support but it does not work
+//       fine especially on old IEs.
+//       Any pull requests are REALLY welcome.
+
++function ($) {
+  'use strict';
+
+  // CKEditor adapter
+  // =======================
+  //
+  // Adapter for CKEditor, based on contenteditable elements.
+  function CKEditor (element, completer, option) {
+    this.initialize(element, completer, option);
+  }
+
+  $.extend(CKEditor.prototype, $.fn.textcomplete.ContentEditable.prototype, {
+    _bindEvents: function () {
+      var $this = this;
+      CKEDITOR.instances["issue_notes"].on('key', function(event) {
+        var domEvent = event.data;
+        $this._onKeyup(domEvent);
+        if ($this.completer.dropdown.shown && $this._skipSearch(domEvent)) {
+          return false;
+        }
+      }, null, null, 1); // 1 = Priority = Important!
+      // we actually also need the native event, as the CKEditor one is happening to late
+      this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
+    },
+});
+
+  $.fn.textcomplete.CKEditor = CKEditor;
+}(jQuery);
+
+// The MIT License (MIT)
+// 
+// Copyright (c) 2015 Jonathan Ong me@jongleberry.com
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+// associated documentation files (the "Software"), to deal in the Software without restriction,
+// including without limitation the rights to use, copy, modify, merge, publish, distribute,
+// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// https://github.com/component/textarea-caret-position
+
+(function ($) {
+
+// The properties that we copy into a mirrored div.
+// Note that some browsers, such as Firefox,
+// do not concatenate properties, i.e. padding-top, bottom etc. -> padding,
+// so we have to do every single property specifically.
+var properties = [
+  'direction',  // RTL support
+  'boxSizing',
+  'width',  // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
+  'height',
+  'overflowX',
+  'overflowY',  // copy the scrollbar for IE
+
+  'borderTopWidth',
+  'borderRightWidth',
+  'borderBottomWidth',
+  'borderLeftWidth',
+  'borderStyle',
+
+  'paddingTop',
+  'paddingRight',
+  'paddingBottom',
+  'paddingLeft',
+
+  // https://developer.mozilla.org/en-US/docs/Web/CSS/font
+  'fontStyle',
+  'fontVariant',
+  'fontWeight',
+  'fontStretch',
+  'fontSize',
+  'fontSizeAdjust',
+  'lineHeight',
+  'fontFamily',
+
+  'textAlign',
+  'textTransform',
+  'textIndent',
+  'textDecoration',  // might not make a difference, but better be safe
+
+  'letterSpacing',
+  'wordSpacing',
+
+  'tabSize',
+  'MozTabSize'
+
+];
+
+var isBrowser = (typeof window !== 'undefined');
+var isFirefox = (isBrowser && window.mozInnerScreenX != null);
+
+function getCaretCoordinates(element, position, options) {
+  if(!isBrowser) {
+    throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
+  }
+
+  var debug = options && options.debug || false;
+  if (debug) {
+    var el = document.querySelector('#input-textarea-caret-position-mirror-div');
+    if ( el ) { el.parentNode.removeChild(el); }
+  }
+
+  // mirrored div
+  var div = document.createElement('div');
+  div.id = 'input-textarea-caret-position-mirror-div';
+  document.body.appendChild(div);
+
+  var style = div.style;
+  var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle;  // currentStyle for IE < 9
+
+  // default textarea styles
+  style.whiteSpace = 'pre-wrap';
+  if (element.nodeName !== 'INPUT')
+    style.wordWrap = 'break-word';  // only for textarea-s
+
+  // position off-screen
+  style.position = 'absolute';  // required to return coordinates properly
+  if (!debug)
+    style.visibility = 'hidden';  // not 'display: none' because we want rendering
+
+  // transfer the element's properties to the div
+  properties.forEach(function (prop) {
+    style[prop] = computed[prop];
+  });
+
+  if (isFirefox) {
+    // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
+    if (element.scrollHeight > parseInt(computed.height))
+      style.overflowY = 'scroll';
+  } else {
+    style.overflow = 'hidden';  // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
+  }
+
+  div.textContent = element.value.substring(0, position);
+  // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
+  if (element.nodeName === 'INPUT')
+    div.textContent = div.textContent.replace(/\s/g, '\u00a0');
+
+  var span = document.createElement('span');
+  // Wrapping must be replicated *exactly*, including when a long word gets
+  // onto the next line, with whitespace at the end of the line before (#7).
+  // The  *only* reliable way to do that is to copy the *entire* rest of the
+  // textarea's content into the <span> created at the caret position.
+  // for inputs, just '.' would be enough, but why bother?
+  span.textContent = element.value.substring(position) || '.';  // || because a completely empty faux span doesn't render at all
+  div.appendChild(span);
+
+  var coordinates = {
+    top: span.offsetTop + parseInt(computed['borderTopWidth']),
+    left: span.offsetLeft + parseInt(computed['borderLeftWidth'])
+  };
+
+  if (debug) {
+    span.style.backgroundColor = '#aaa';
+  } else {
+    document.body.removeChild(div);
+  }
+
+  return coordinates;
+}
+
+$.fn.textcomplete.getCaretCoordinates = getCaretCoordinates;
+
+}(jQuery));
 
-})(window.jQuery || window.Zepto);
+return jQuery;
+}));

File diff suppressed because it is too large
+ 0 - 0
main/inc/lib/javascript/jquery-textcomplete/jquery.textcomplete.min.js


File diff suppressed because it is too large
+ 0 - 0
main/inc/lib/javascript/jquery-textcomplete/jquery.textcomplete.min.map


+ 1 - 2
main/inc/lib/online.inc.php

@@ -149,8 +149,7 @@ function online_logout($user_id = null, $logout_redirect = false)
         }
     }
 
-    require_once api_get_path(SYS_PATH) . 'main/chat/chat_functions.lib.php';
-    exit_of_chat($user_id);
+    CourseChatUtils::exitChat($user_id);
     session_regenerate_id();
     Session::destroy();
     if ($logout_redirect) {

+ 342 - 0
main/template/default/chat/chat.tpl

@@ -0,0 +1,342 @@
+<div class="page-chat">
+    <div class="row">
+        <div class="col-sm-4 col-md-5 col-lg-4">
+            <ul class="row list-unstyled" id="chat-users"></ul>
+        </div>
+        <div class="col-sm-8 col-md-7 col-lg-8">
+            <div id="chat-tabs">
+                <ul class="nav nav-tabs" role="tablist">
+                    <li role="presentation" class="active">
+                        <a href="#all" aria-controls="all" role="tab" data-toggle="tab">{{ 'All'|get_lang }}</a>
+                    </li>
+                </ul>
+                <div class="tab-content">
+                    <div role="tabpanel" class="tab-pane active" id="all">
+                        <div class="course-chat chat-history" id="chat-history"></div>
+                    </div>
+                </div>
+            </div>
+            <div class="profile row">
+                <div class="col-xs-12">
+                    <div class="message-form-chat">
+                        <div class="tabbable">
+                            <ul class="nav nav-tabs">
+                                <li class="active">
+                                    <a href="#tab1" data-toggle="tab">{{ 'Write'|get_lang }}</a>
+                                </li>
+                                <li>
+                                    <a href="#tab2" id="preview" data-toggle="tab">{{ 'Preview'|get_lang }}</a>
+                                </li>
+                                <li>
+                                    <button id="emojis" class="btn btn-link" type="button">
+                                        <span class="sr-only">{{ 'Emoji'|get_lang }}</span>{{ emoji_smile }}
+                                    </button>
+                                </li>
+                            </ul>
+                            <div class="tab-content">
+                                <div class="tab-pane active" id="tab1">
+                                    <div class="row">
+                                        <div class="col-sm-9">
+                                            <span class="sr-only">{{ 'Message'|get_lang }}</span>
+                                            <textarea id="chat-writer" name="message"></textarea>
+                                        </div>
+                                        <div class="col-sm-3">
+                                            <button id="chat-send-message" type="button" class="btn btn-primary">{{ 'Send'|get_lang }}</button>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="tab-pane" id="tab2">
+                                    <div id="html-preview" class="emoji-wysiwyg-editor-preview"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<audio id="chat-alert">
+    <source src="{{ _p.web_main }}/chat/sound/notification.wav" type="audio/wav"></source>
+    <source src="{{ _p.web_main }}chat/sound/notification.ogg" type="audio/ogg"></source>
+    <source src="{{ _p.web_main }}chat/sound/notification.mp3" type="audio/mpeg"></source>
+</audio>
+<script>
+    $(document).on('ready', function () {
+        var ChChat = {
+            _ajaxUrl: '{{ _p.web_ajax }}course_chat.ajax.php?{{ _p.web_cid_query }}',
+            _historySize: -1,
+            usersOnline: 0,
+            currentFriend: 0,
+            call: false,
+            track: function () {
+                return $
+                        .get(ChChat._ajaxUrl, {
+                            action: 'track',
+                            size: ChChat._historySize,
+                            users_online: ChChat.usersOnline,
+                            friend: ChChat.currentFriend
+                        })
+                        .done(function (response) {
+                            if (response.data.chatIsDenied) {
+                                alert("{{ 'ChatDenied'|get_lang }}");
+
+                                return;
+                            }
+
+                            if (response.data.history) {
+                                ChChat._historySize = response.data.oldFileSize;
+                                ChChat.setHistory(response.data.history);
+                            }
+
+                            if (response.data.userList) {
+                                ChChat.usersOnline = response.data.usersOnline;
+                                ChChat.setConnectedUsers(response.data.userList);
+                            }
+                        });
+            },
+            setHistory: function (messageList) {
+                var chatHistoryContainer = ChChat.currentFriend ? ('#chat-history-' + ChChat.currentFriend) : '#chat-history';
+
+                $(chatHistoryContainer)
+                        .html(messageList)
+                        .prop('scrollTop', function () {
+                            return this.scrollHeight;
+                        });
+
+                $('#chat-alert').get(0).play();
+            },
+            setConnectedUsers: function (userList) {
+                var html = '';
+
+                userList.forEach(function (user) {
+                    var buttonStatus = user.isConnected ? 'success' : 'muted',
+                            buttonTitle = user.isConnected ? '{{ 'StartAChat'|get_lang }}' : '{{ 'LeaveAMessage'|get_lang }}';
+
+                    html += '<li class="col-xs-12 chat-user">' +
+                            '   <div>' +
+                            '       <img src="'+ user.image_url + '" alt="' + user.complete_name + '" class="img-circle user-image-chat"/>' +
+                            '       <ul class="list-unstyled">' +
+                            '           <li>' + user.complete_name;
+
+                    if (user.id != {{ _u.user_id }}) {
+                        html += '           <button type="button" class="btn btn-link btn-xs" title="' + buttonTitle + '" data-name="' + user.complete_name + '" data-user="' + user.id + '">' +
+                                '               <i class="fa fa-comments text-' + buttonStatus + '"></i><span class="sr-only">' + buttonTitle + '</span>' +
+                                '           </button>';
+                    }
+
+                    html += '           </li>' +
+                            '           <li><small>' + user.username + '</small></li>' +
+                            '       </ul>' +
+                            '   </div>' +
+                            '</li>';
+                });
+
+                $('#chat-users').html(html);
+            },
+            onPreviewListener: function () {
+                $
+                        .post(ChChat._ajaxUrl, {
+                            action: 'preview',
+                            'message': $('textarea#chat-writer').val()
+                        })
+                        .done(function (response) {
+                            if (!response.status) {
+                                return;
+                            }
+
+                            $('#html-preview').html(response.data.message);
+                        });
+            },
+            onSendMessageListener: function (e) {
+                e.preventDefault();
+
+                if (!$('textarea#chat-writer').val().trim().length) {
+                    return;
+                }
+
+                var self = this;
+                self.disabled = true;
+
+                $
+                        .post(ChChat._ajaxUrl, {
+                            action: 'write',
+                            message: $('textarea#chat-writer').val(),
+                            friend: ChChat.currentFriend
+                        })
+                        .done(function (response) {
+                            self.disabled = false;
+
+                            if (!response.status) {
+                                return;
+                            }
+
+                            $('textarea#chat-writer').val('');
+                            $(".emoji-wysiwyg-editor").html('');
+                        });
+            },
+            onResetListener: function (e) {
+                if (!confirm("{{ 'ConfirmReset'|get_lang }}")) {
+                    e.preventDefault();
+
+                    return;
+                }
+
+                $
+                        .get(ChChat._ajaxUrl, {
+                            action: 'reset',
+                            friend: ChChat.currentFriend
+                        })
+                        .done(function (response) {
+                            if (!response.status) {
+                                return;
+                            }
+
+                            ChChat.setHistory(response.data);
+                        });
+            },
+            init: function () {
+                ChChat.track().done(function () {
+                    ChChat.init();
+                });
+            }
+        };
+
+        hljs.initHighlightingOnLoad();
+
+        emojione.ascii = true;
+        emojione.imagePathPNG = '{{ _p.web_lib }}javascript/emojione/png/';
+        emojione.imagePathSVG = '{{ _p.web_lib }}javascript/emojione/svg/';
+        emojione.imagePathSVGSprites = '{{ _p.web_lib }}javascript/emojione/sprites/';
+
+        var emojiStrategy = {{ emoji_strategy|json_encode }};
+
+        $.emojiarea.path = '{{ _p.web_lib }}javascript/emojione/png/';
+        $.emojiarea.icons = {{ icons|json_encode }};
+
+        $('body').on('click', '#chat-reset', ChChat.onResetListener);
+
+        $('#preview').on('click', ChChat.onPreviewListener);
+
+        $('#emojis').on('click', function () {
+            $('[data-toggle="tab"][href="#tab1"]')
+                    .show()
+                    .tab('show');
+        });
+
+        $('textarea#chat-writer').emojiarea({
+            button: '#emojis'
+        });
+
+        $('body').delay(1500).find('.emoji-wysiwyg-editor').textcomplete([
+            {
+                match: /\B:([\-+\w]*)$/,
+                search: function (term, callback) {
+                    var results = [];
+                    var results2 = [];
+                    var results3 = [];
+                    $.each(emojiStrategy, function (shortname, data) {
+                        if (shortname.indexOf(term) > -1) {
+                            results.push(shortname);
+                        } else {
+                            if ((data.aliases !== null) && (data.aliases.indexOf(term) > -1)) {
+                                results2.push(shortname);
+                            } else if ((data.keywords !== null) && (data.keywords.indexOf(term) > -1)) {
+                                results3.push(shortname);
+                            }
+                        }
+                    });
+
+                    if (term.length >= 3) {
+                        results.sort(function (a, b) {
+                            return (a.length > b.length);
+                        });
+                        results2.sort(function (a, b) {
+                            return (a.length > b.length);
+                        });
+                        results3.sort();
+                    }
+
+                    var newResults = results.concat(results2).concat(results3);
+
+                    callback(newResults);
+                },
+                template: function (shortname) {
+                    return '<img class="emojione" src="{{ _p.web_lib }}javascript/emojione/png/'
+                            + emojiStrategy[shortname].unicode
+                            + '.png"> :' + shortname + ':';
+                },
+                replace: function (shortname) {
+                    return ':' + shortname + ': ';
+                },
+                index: 1,
+                maxCount: 10
+            }
+        ], {});
+
+        $('button#chat-send-message').on('click', ChChat.onSendMessageListener);
+
+        $('#chat-users').on('click', 'button.btn', function (e) {
+            e.preventDefault();
+
+            var jSelf = $(this),
+                    userId = parseInt(jSelf.data('user')) || 0;
+
+            if (!userId) {
+                return;
+            }
+
+            var exists = false;
+
+            $('#chat-tabs ul.nav li').each(function (i, el) {
+                if ($(el).data('user') == userId) {
+                    exists = true;
+                }
+            });
+
+            if (exists) {
+                $('#chat-tab-' + userId).tab('show');
+
+                return;
+            }
+
+            $('#chat-tabs ul.nav-tabs').append('\
+                <li role="presentation" data-user="' + userId + '">\
+                    <a id="chat-tab-' + userId + '" href="#chat-' + userId + '" aria-controls="chat-' + userId + '" role="tab" data-toggle="tab">' + jSelf.data('name') + '</a>\
+                </li>\
+            ');
+
+            $('#chat-tabs .tab-content').append('\
+                <div role="tabpanel" class="tab-pane" id="chat-' + userId + '">\
+                    <div class="course-chat chat-history" id="chat-history-' + userId + '"></div>\
+                </div>\
+            ');
+
+            $('#chat-tab-' + userId).tab('show');
+        });
+
+        $('#chat-tabs ul.nav-tabs').on('shown.bs.tab', 'li a', function (e) {
+            var jSelf = $(this);
+
+            var userId = parseInt(jSelf.parent().data('user')) || 0;
+
+            if (!userId) {
+                ChChat.currentFriend = 0;
+
+                return;
+            }
+
+            ChChat.currentFriend = userId;
+
+            $(this).tab('show');
+        });
+
+        $('.emoji-wysiwyg-editor').on('keyup', function (e) {
+            if (e.ctrlKey && e.keyCode === 13) {
+                $('button#chat-send-message').trigger('click');
+            }
+        });
+
+        ChChat.init();
+    });
+</script>

+ 0 - 44
tests/main/chat/chat_functions.lib.test.php

@@ -1,5 +1,4 @@
 <?php
-require_once(api_get_path(SYS_CODE_PATH).'chat/chat_functions.lib.php');
 require_once(api_get_path(LIBRARY_PATH).'course.lib.php');
 
 class TestChatFunctions extends UnitTestCase {
@@ -16,25 +15,6 @@ class TestChatFunctions extends UnitTestCase {
 		$this->tcourse = null;
 	}
 
-
-
-	function testuser_connected_in_chat () {
-		$course_code = 'COURSETEST';
-		$user_id = 1;
-		$res = user_connected_in_chat($user_id);
- 		$this->assertTrue(is_bool($res));
-	}
-
-
-	function testUsersListInChat () {
-		$course_code = 'COURSETEST';
-		$course_info = api_get_course_info($course_code);
-		$database_name = $course_info['dbName'];
-		$res = users_list_in_chat($database_name);
- 		$this->assertTrue(is_array($res));
- 		//var_dump($res);
-	}
-
 	function CreateChatConnection($database_name) {
 		$session_id = 1;
 		$tbl_chat_connected = Database::get_main_table(TABLE_MAIN_CHAT);
@@ -51,29 +31,5 @@ class TestChatFunctions extends UnitTestCase {
 		}
 		Database::query($query);
 	}
-
-
-	function testExitOfChat () {
-		$course_code = 'COURSETEST';
-		$course_info = api_get_course_info($course_code);
-		$database_name = $course_info['dbName'];
-		$this->CreateChatConnection($database_name);
-		$user_id = 1;
-		$res = exit_of_chat($user_id);
-		//$resu = $this->tcourse->delete_course($course_code);
- 		$this->assertTrue(is_null($res));
- 		//var_dump($res);
-	}
-
-
-		function testDisconnectUserOfChat() {
-		$_SESSION['is_courseAdmin'] = 1;
-		$course_code = 'COURSETEST';
-		$course_info = api_get_course_info($course_code);
-		$database_name = $course_info['dbName'];
-		$this->CreateChatConnection($database_name);
-		$res = disconnect_user_of_chat($database_name);
-		$this->assertTrue(is_null($res));
-	}
 }
 ?>

Some files were not shown because too many files changed in this diff