From 262c5f22c887e01d6c3ca29fa4bae0e051613c35 Mon Sep 17 00:00:00 2001 From: Lephe Date: Tue, 26 Apr 2022 15:14:51 +0100 Subject: [PATCH 01/92] =?UTF-8?q?navbar:=20fix=20links=20to=20news=20forum?= =?UTF-8?q?s=20under=20"Actualit=C3=A9s"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/base/navbar/news.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/templates/base/navbar/news.html b/app/templates/base/navbar/news.html index 1bff05e..0bea391 100644 --- a/app/templates/base/navbar/news.html +++ b/app/templates/base/navbar/news.html @@ -5,14 +5,14 @@ Actualités - Toutes les nouveautés + Toutes les nouveautés
- Nouveautés Casio - Projets communutaires - Événements de Planète Casio - Autres nouveautés + Nouveautés Casio + Projets communutaires + Événements de Planète Casio + Autres nouveautés
From faf5bd184d2792a95ba8e8b199f9f51442149515 Mon Sep 17 00:00:00 2001 From: Darks Date: Tue, 26 Apr 2022 20:40:56 +0200 Subject: [PATCH 02/92] navbar: properly generate links to recent topics --- app/processors/menu.py | 15 ++++++++++++++- app/templates/base/navbar/forum.html | 8 +++----- app/templates/base/navbar/news.html | 8 +++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/processors/menu.py b/app/processors/menu.py index b45bbf6..a2879ff 100644 --- a/app/processors/menu.py +++ b/app/processors/menu.py @@ -32,5 +32,18 @@ def menu_processor(): last_active_topics = list(filter(f, last_active_topics))[:10] + # Constructing last news + raw = db.session.execute( """SELECT topic.id FROM topic + INNER JOIN forum ON topic.forum_id = forum.id + INNER JOIN comment ON topic.thread_id = comment.thread_id + INNER JOIN post ON post.id = comment.id + WHERE forum.url LIKE '/actus%' + GROUP BY topic.id + ORDER BY MIN(post.date_created) DESC + LIMIT 10; + """) + last_news = [Topic.query.get(id) for id in raw] + return dict(login_form=login_form, search_form=search_form, - main_forum=main_forum, last_active_topics=last_active_topics) + main_forum=main_forum, last_active_topics=last_active_topics, + last_news=last_news) diff --git a/app/templates/base/navbar/forum.html b/app/templates/base/navbar/forum.html index 29688c6..a38b67b 100644 --- a/app/templates/base/navbar/forum.html +++ b/app/templates/base/navbar/forum.html @@ -20,10 +20,8 @@

Derniers topics actifs

diff --git a/app/templates/base/navbar/news.html b/app/templates/base/navbar/news.html index 0bea391..844b7ed 100644 --- a/app/templates/base/navbar/news.html +++ b/app/templates/base/navbar/news.html @@ -18,10 +18,8 @@

Derniers articles

From eb5ce1bd5c3ff9baeebcbbb1641c12b05b252362 Mon Sep 17 00:00:00 2001 From: Darks Date: Tue, 26 Apr 2022 23:29:11 +0200 Subject: [PATCH 03/92] attachement: switch to uuid + check permission in dl widget (#109) Also added is_default_accessible() to Thread class as its owner may be a Topic with forum access restrictions or public main content (like Program) [MIGRATION] This commit contains a new version of the schema. /!\ This migration breaks all attachments --- app/models/attachment.py | 9 +++-- app/models/thread.py | 7 ++++ app/models/user.py | 4 +++ app/utils/markdown_extensions/pclinks.py | 16 +++++++-- ...33816b21_switched_attachment_id_to_uuid.py | 34 +++++++++++++++++++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 migrations/versions/72df33816b21_switched_attachment_id_to_uuid.py diff --git a/app/models/attachment.py b/app/models/attachment.py index 0bc5856..b710298 100644 --- a/app/models/attachment.py +++ b/app/models/attachment.py @@ -1,13 +1,16 @@ from werkzeug.utils import secure_filename +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import backref from app import db from app.utils.filesize import filesize from config import V5Config import os +import uuid + class Attachment(db.Model): __tablename__ = 'attachment' - id = db.Column(db.Integer, primary_key=True) + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # Original name of the file name = db.Column(db.Unicode(64)) @@ -24,11 +27,11 @@ class Attachment(db.Model): @property def path(self): return os.path.join(V5Config.DATA_FOLDER, "attachments", - f"{self.id:05}", self.name) + f"{self.id}", self.name) @property def url(self): - return f"/fichiers/{self.id:05}/{self.name}" + return f"/fichiers/{self.id}/{self.name}" def __init__(self, file, comment): diff --git a/app/models/thread.py b/app/models/thread.py index 8dfaa52..cedb4d6 100644 --- a/app/models/thread.py +++ b/app/models/thread.py @@ -53,6 +53,13 @@ class Thread(db.Model): return self.owner_program[0] return None + def is_default_accessible(self): + if self.owner_program != []: + return True + if self.owner_topic != []: + return self.owner_topic[0].forum.is_default_accessible() + return False + def delete(self): """Recursively delete thread and all associated contents.""" # Remove reference to top comment diff --git a/app/models/user.py b/app/models/user.py index 325040d..e1a006d 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -250,6 +250,10 @@ class Member(User): post = comment.thread.owner_post return self.can_edit_post(post) and (comment.author == post.author) + def can_access_file(self, file): + """Whether this member can access the file.""" + return self.can_access_post(file.comment) + def update(self, **data): """ Update all or part of the user's metadata. The [data] dictionary diff --git a/app/utils/markdown_extensions/pclinks.py b/app/utils/markdown_extensions/pclinks.py index f674f60..0e94793 100644 --- a/app/utils/markdown_extensions/pclinks.py +++ b/app/utils/markdown_extensions/pclinks.py @@ -13,8 +13,10 @@ License: [BSD](https://opensource.org/licenses/bsd-license.php) from markdown.extensions import Extension from markdown.inlinepatterns import InlineProcessor +from uuid import UUID import xml.etree.ElementTree as etree from flask import url_for, render_template +from flask_login import current_user from app.utils.unicode_names import normalize from app.utils.filters.humanize import humanize from app.models.poll import Poll @@ -36,7 +38,7 @@ class PCLinkExtension(Extension): self.md = md # append to end of inline patterns - PCLINK_RE = r'<([a-z]+): ?(\w+)>' + PCLINK_RE = r'<([a-z]+): ?([\w-]+)>' pclinkPattern = PCLinksInlineProcessor(PCLINK_RE, self.getConfigs()) pclinkPattern.md = md md.inlinePatterns.register(pclinkPattern, 'pclink', 135) @@ -125,7 +127,7 @@ def handleUser(content_id, context): def handleFile(content_id, context): try: - content_id = int(content_id) + UUID(content_id) except ValueError: return "[ID de fichier invalide]" @@ -134,6 +136,16 @@ def handleFile(content_id, context): if file is None: return "[Fichier non trouvé]" + # Manage permissions + # TODO: use Guest methods when implemented + if current_user.is_authenticated: + if not current_user.can_access_file(file): + return "[Accès au fichier refusé]" + else: + if not file.comment.thread.is_default_accessible(): + return "[Accès au fichier refusé]" + + html = render_template('widgets/download_button.html', file=file) html = html.replace('\n', '') # Needed to avoid lots of
due to etree return etree.fromstring(html) diff --git a/migrations/versions/72df33816b21_switched_attachment_id_to_uuid.py b/migrations/versions/72df33816b21_switched_attachment_id_to_uuid.py new file mode 100644 index 0000000..f330989 --- /dev/null +++ b/migrations/versions/72df33816b21_switched_attachment_id_to_uuid.py @@ -0,0 +1,34 @@ +"""Switched attachment id to UUID + +Revision ID: 72df33816b21 +Revises: d2227d2479e2 +Create Date: 2022-04-26 21:50:05.466388 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '72df33816b21' +down_revision = 'd2227d2479e2' +branch_labels = None +depends_on = None + + +def upgrade(): + # Create uuid-ossp extension, required to use uuid_generate_v4() + op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";') + # /!\ This operation will break all attachments /!\ + op.execute("""ALTER TABLE attachment ALTER COLUMN id DROP DEFAULT, + ALTER COLUMN id SET DATA TYPE UUID USING (uuid_generate_v4()), + ALTER COLUMN id SET DEFAULT uuid_generate_v4();""") + # ### end Alembic commands ### + + +def downgrade(): + # /!\ This operation will break all attachments /!\ + op.execute("""ALTER TABLE attachment ALTER COLUMN id DROP DEFAULT, + ALTER COLUMN id SET DATA TYPE integer USING nextval('attachment_id_seq'::regclass), + ALTER COLUMN id SET DEFAULT nextval('attachment_id_seq'::regclass);""") + # ### end Alembic commands ### From a3ed63379164cb82f96b38de7d3ce9f9400e684c Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 5 May 2022 19:31:26 +0100 Subject: [PATCH 04/92] config: update and slight improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename Config → FlaskApplicationSettings so we know exactly what we're talking about * Clarify that LocalConfig overrides both V5Config and Flask settings * Only give defaults that are needed in LocalConfig and remove old settings that are no longer used --- app/__init__.py | 6 +++--- config.py | 31 +++++++++++++++++++++++++++---- local_config.py.default | 12 ------------ 3 files changed, 30 insertions(+), 19 deletions(-) delete mode 100644 local_config.py.default diff --git a/app/__init__.py b/app/__init__.py index 969e8ed..0cbec4d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,13 +4,13 @@ from flask_migrate import Migrate from flask_login import LoginManager from flask_mail import Mail from flask_wtf.csrf import CSRFProtect -from config import Config +from config import FlaskApplicationSettings app = Flask(__name__) -app.config.from_object(Config) +app.config.from_object(FlaskApplicationSettings) # Check security of secret -if Config.SECRET_KEY == "a-random-secret-key": +if FlaskApplicationSettings.SECRET_KEY == "a-random-secret-key": raise Exception("Please use a strong secret key!") db = SQLAlchemy(app) diff --git a/config.py b/config.py index 8b4efe8..96725a5 100644 --- a/config.py +++ b/config.py @@ -6,11 +6,28 @@ try: except ImportError: print(" \033[92mWARNING: Local config not found\033[0m") + # The LocalConfig class serves a dual purpose and overrides settings in + # both V5Config and some fields of FlaskApplicationSettings. The first has + # defaults (DefaultConfig), but not the second, so we provide them here. class LocalConfig(): - pass + FLASK_DEBUG = False + DB_NAME = "pcv5" + SECRET_KEY = "a-random-secret-key" +#--- +# Flask configuration +#--- + +class FlaskApplicationSettings(object): + """ + This object specifies the settings for the Flask application. All the keys + and values are predefined by Flask. + + See: https://flask.palletsprojects.com/en/2.1.x/config/ + """ + + DEBUG = os.environ.get("FLASK_DEBUG") or LocalConfig.FLASK_DEBUG -class Config(object): SECRET_KEY = os.environ.get('SECRET_KEY') or LocalConfig.SECRET_KEY SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'postgresql+psycopg2://' + os.environ.get('USER') + ':@/' \ @@ -27,9 +44,11 @@ class Config(object): # Do not attach cookies to cross-origin requests SESSION_COOKIE_SAMESITE = "Lax" +#--- +# v5 configuration +#--- class DefaultConfig(object): - """Every value here can be overrided in the local_config.py class""" # Domain DOMAIN = "v5.planet-casio.com" # Database name @@ -62,5 +81,9 @@ class DefaultConfig(object): class V5Config(LocalConfig, DefaultConfig): - # Values put here cannot be overidden with local_config + """ + This object holds the settings for the v5 code. Each parameter has the + value specified in LocalConfig (if any), and defaults to the value set in + DefaultConfig. + """ pass diff --git a/local_config.py.default b/local_config.py.default deleted file mode 100644 index f631079..0000000 --- a/local_config.py.default +++ /dev/null @@ -1,12 +0,0 @@ -# This is a sample file -# You can override every value available in the class DefaultConfig - -class LocalConfig(object): - DB_NAME = "pcv5" - USE_LDAP = True - LDAP_PASSWORD = "openldap" - LDAP_ENV = "o=prod" - SECRET_KEY = "a-random-secret-key" # CHANGE THIS VALUE *NOW* - AVATARS_FOLDER = '/home/pc/data/avatars/' - ENABLE_GUEST_POST = True - SEND_MAILS = True From 5a87d29c7ff475beb0abdd81507908ff0398f3e6 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 5 May 2022 20:18:30 +0100 Subject: [PATCH 05/92] account: make default avatar selection less hacky --- app/models/user.py | 52 +++++++++++++++----------- app/routes/development.py | 4 +- app/templates/account/account.html | 2 +- app/templates/admin/edit_account.html | 2 +- app/templates/base/navbar.html | 2 +- app/templates/base/navbar/account.html | 2 +- app/templates/widgets/user.html | 2 +- 7 files changed, 37 insertions(+), 29 deletions(-) diff --git a/app/models/user.py b/app/models/user.py index e1a006d..0f80594 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,8 +1,7 @@ -from datetime import date +from flask import url_for from flask_login import UserMixin from sqlalchemy import func as SQLfunc -from os.path import isfile -from PIL import Image +import werkzeug.security from app import app, db from app.models.priv import SpecialPrivilege, Group, GroupMember, \ @@ -19,7 +18,9 @@ import app.utils.ldap as ldap from app.utils.unicode_names import normalize from config import V5Config -import werkzeug.security +from os.path import isfile +from datetime import date +from PIL import Image import math import app import os @@ -89,16 +90,6 @@ class Member(User): xp = db.Column(db.Integer) register_date = db.Column(db.Date, default=date.today) - avatar_id = db.Column(db.Integer, default=0) - @property - def avatar(self): - return f'{self.id}_{self.avatar_id}.png' - - @property - def level(self): - level = math.asinh(self.xp / 1000) * (100 / math.asinh(10)) - return int(level), int(level * 100) % 100 - # Groups and related privileges groups = db.relationship('Group', secondary=GroupMember, back_populates='members') @@ -115,6 +106,7 @@ class Member(User): # Settings newsletter = db.Column(db.Boolean, default=False) theme = db.Column(db.Unicode(32)) + avatar_id = db.Column(db.Integer, default=0) # Relations trophies = db.relationship('Trophy', secondary=TrophyMember, @@ -129,6 +121,23 @@ class Member(User): def programs(self): return db.session.query(Program).filter(Post.author_id==self.id).all() + @property + def avatar_filename(self): + return f'{self.id}_{self.avatar_id}.png' + + @property + def avatar_url(self): + if self.avatar_id == 0: + return url_for('static', filename='images/default_avatar.png') + else: + return url_for('avatar',filename=self.avatar_filename) + + @property + def level(self): + level = math.asinh(self.xp / 1000) * (100 / math.asinh(10)) + return int(level), int(level * 100) % 100 + + # Other fields populated automatically through relations: # List of unseen notifications (of type Notification) # Polls created by the member (of class Poll) @@ -312,20 +321,22 @@ class Member(User): def set_avatar(self, avatar): # Save old avatar filepath - old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar) + old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars", + self.avatar_filename) # Resize & convert image - size = 128, 128 im = Image.open(avatar) - im.thumbnail(size, Image.ANTIALIAS) + im.thumbnail((128, 128), Image.ANTIALIAS) + # Change avatar id # TODO: verify concurrency behavior current_id = db.session.query(SQLfunc.max(Member.avatar_id)).first()[0] self.avatar_id = current_id + 1 db.session.merge(self) db.session.commit() + # Save the new avatar - im.save(os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar), - 'PNG') + im.save(os.path.join(V5Config.DATA_FOLDER, "avatars", + self.avatar_filename), 'PNG') # If nothing has failed, remove old one (allow failure to regularize # exceptional situations like missing avatar or folder migration) try: @@ -553,8 +564,7 @@ class Member(User): # TODO: Trophy "actif" if context in ["on-profile-update", None]: - if isfile(os.path.join( - V5Config.DATA_FOLDER, "avatars", self.avatar)): + if self.avatar_id != 0: self.add_trophy("Artiste") else: self.del_trophy("Artiste") diff --git a/app/routes/development.py b/app/routes/development.py index f4f4cc0..28618b3 100644 --- a/app/routes/development.py +++ b/app/routes/development.py @@ -11,9 +11,7 @@ import os def avatar(filename): filename = secure_filename(filename) # No h4ckers allowed filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename) - if os.path.isfile(filepath): - return send_file(filepath) - return redirect(url_for('static', filename='images/default_avatar.png')) + return send_file(filepath) @app.route('/fichiers//') def attachment(path, name): diff --git a/app/templates/account/account.html b/app/templates/account/account.html index ee80383..92e424c 100644 --- a/app/templates/account/account.html +++ b/app/templates/account/account.html @@ -13,7 +13,7 @@
{{ form.avatar.label }}
- + {{ form.avatar }}
{% for error in form.avatar.errors %} diff --git a/app/templates/admin/edit_account.html b/app/templates/admin/edit_account.html index 724ef25..36fff34 100644 --- a/app/templates/admin/edit_account.html +++ b/app/templates/admin/edit_account.html @@ -14,7 +14,7 @@
{{ form.avatar.label }}
- + {{ form.avatar }}
{% for error in form.avatar.errors %} diff --git a/app/templates/base/navbar.html b/app/templates/base/navbar.html index 68b3ca1..02ef0eb 100644 --- a/app/templates/base/navbar.html +++ b/app/templates/base/navbar.html @@ -61,7 +61,7 @@
  • {% if current_user.is_authenticated %} - Avatar de {{ current_user.name }} + Avatar de {{ current_user.name }}
    Compte
    {% else %} diff --git a/app/templates/base/navbar/account.html b/app/templates/base/navbar/account.html index f34ac61..61ce0fc 100644 --- a/app/templates/base/navbar/account.html +++ b/app/templates/base/navbar/account.html @@ -2,7 +2,7 @@

    - Avatar de {{ current_user.name }} + Avatar de {{ current_user.name }} {{ current_user.name }}

    diff --git a/app/templates/widgets/user.html b/app/templates/widgets/user.html index 659927f..96ad546 100644 --- a/app/templates/widgets/user.html +++ b/app/templates/widgets/user.html @@ -1,7 +1,7 @@ {% macro profile(user) %} {% if user.type == "member" %}
    - Avatar de {{ user.name }} + Avatar de {{ user.name }}
    {% if user.title %} From 8f620c61507cf5f7deae5172844f990328174f6a Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 5 May 2022 20:32:54 +0100 Subject: [PATCH 06/92] meta: add optional setting for flask-debug-toolbar It provides profiling information and an overview of SQL requests while in development. --- REQUIREMENTS.md | 5 +++++ app/__init__.py | 8 +++++++- config.py | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 90660dc..6289a1f 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -24,3 +24,8 @@ python-pillow python-pyyaml python-slugify ``` + +Optionnel: +``` +python-flask-debugtoolbar +``` diff --git a/app/__init__.py b/app/__init__.py index 0cbec4d..1049e63 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,7 +4,7 @@ from flask_migrate import Migrate from flask_login import LoginManager from flask_mail import Mail from flask_wtf.csrf import CSRFProtect -from config import FlaskApplicationSettings +from config import FlaskApplicationSettings, V5Config app = Flask(__name__) app.config.from_object(FlaskApplicationSettings) @@ -36,3 +36,9 @@ from app.utils import filters # Register processors from app import processors + +# Enable flask-debug-toolbar if requested +if V5Config.ENABLE_FLASK_DEBUG_TOOLBAR: + from flask_debugtoolbar import DebugToolbarExtension + app.config['DEBUG_TB_PROFILER_ENABLED'] = True + toolbar = DebugToolbarExtension(app) diff --git a/config.py b/config.py index 96725a5..c0a3692 100644 --- a/config.py +++ b/config.py @@ -78,6 +78,8 @@ class DefaultConfig(object): # is computed in the page header, so it doesn't account for most of the # template generation. SLOW_REQUEST_THRESHOLD = 0.400 # s + # Whether to enable flask-debug-toolbar + ENABLE_FLASK_DEBUG_TOOLBAR = False class V5Config(LocalConfig, DefaultConfig): From f64e3a2c398444ad6387cebff24ebc77ea63d57c Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 12 May 2022 20:07:28 +0200 Subject: [PATCH 07/92] debugger: add some style to enhance it --- app/static/css/debugger.css | 3 +++ app/static/less/debugger.less | 4 ++++ app/utils/render.py | 1 + 3 files changed, 8 insertions(+) create mode 100644 app/static/css/debugger.css create mode 100644 app/static/less/debugger.less diff --git a/app/static/css/debugger.css b/app/static/css/debugger.css new file mode 100644 index 0000000..bd5f746 --- /dev/null +++ b/app/static/css/debugger.css @@ -0,0 +1,3 @@ +#flDebug * { + overflow: auto !important; +} \ No newline at end of file diff --git a/app/static/less/debugger.less b/app/static/less/debugger.less new file mode 100644 index 0000000..00b7417 --- /dev/null +++ b/app/static/less/debugger.less @@ -0,0 +1,4 @@ +/* Some styles to enhance debugger */ +#flDebug * { + overflow: auto !important; +} diff --git a/app/utils/render.py b/app/utils/render.py index 766e3f7..8ecf8e6 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -18,6 +18,7 @@ def render(*args, styles=[], scripts=[], **kwargs): 'css/pagination.css', 'css/simplemde.min.css', 'css/simplemde-override.css', + 'css/debugger.css', ] scripts_ = [ 'scripts/trigger_menu.js', From 1040d5750604de05956cf0de854e31b8a7bc2634 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 12 May 2022 19:24:02 +0100 Subject: [PATCH 08/92] css: fully recompile LESS files --- app/static/css/form.css | 2 +- app/static/css/table.css | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/static/css/form.css b/app/static/css/form.css index 716a28d..23dff8a 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -95,7 +95,7 @@ font-weight: 400; margin-top: 5px; } -.form input.abfield[type="email"] { +.form .abfield { display: none; } .form.filter { diff --git a/app/static/css/table.css b/app/static/css/table.css index 79f1975..8555168 100644 --- a/app/static/css/table.css +++ b/app/static/css/table.css @@ -111,8 +111,6 @@ table.thread:not(.topcomment) div.info { table.thread div.info { text-align: right; position: relative; - margin-left: 24px; - margin-bottom: 8px; } table.thread div.info > * { display: inline-block; From 7d9e897ae911883f983aeeb922f98773644ce634 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 12 May 2022 19:19:17 +0100 Subject: [PATCH 09/92] perf: optimize away special privilege requests by lazy loading --- app/models/priv.py | 8 ++-- app/models/user.py | 8 ++-- app/routes/admin/groups.py | 2 +- ...37_special_privileges_as_a_relationship.py | 38 +++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 migrations/versions/44fbbb1fd537_special_privileges_as_a_relationship.py diff --git a/app/models/priv.py b/app/models/priv.py index ff96ae4..765e59c 100644 --- a/app/models/priv.py +++ b/app/models/priv.py @@ -16,16 +16,18 @@ class SpecialPrivilege(db.Model): id = db.Column(db.Integer, primary_key=True) # Member that is granted the privilege - mid = db.Column(db.Integer, db.ForeignKey('member.id'), index=True) + member_id = db.Column(db.Integer, db.ForeignKey('member.id'), index=True) + member = db.relationship('Member', backref="special_privs", + foreign_keys=member_id) # Privilege name priv = db.Column(db.String(64)) def __init__(self, member, priv): - self.mid = member.id + self.member = member self.priv = priv def __repr__(self): - return f'' + return f'' # Group: User group, corresponds to a community role and a set of privileges diff --git a/app/models/user.py b/app/models/user.py index 0f80594..afe88a5 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -139,6 +139,7 @@ class Member(User): # Other fields populated automatically through relations: + # List of special privileges # List of unseen notifications (of type Notification) # Polls created by the member (of class Poll) @@ -194,7 +195,7 @@ class Member(User): Deletes the user, but not the posts; use either transfer_posts() or delete_posts() before calling this. """ - for sp in SpecialPrivilege.query.filter_by(mid=self.id).all(): + for sp in self.special_privs: db.session.delete(sp) self.trophies = [] @@ -207,7 +208,7 @@ class Member(User): def priv(self, priv): """Check whether the member has the specified privilege.""" - if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first(): + if priv in self.special_privs: return True return db.session.query(Group, GroupPrivilege).filter( Group.id.in_([g.id for g in self.groups]), @@ -216,8 +217,7 @@ class Member(User): def special_privileges(self): """List member's special privileges.""" - sp = SpecialPrivilege.query.filter_by(mid=self.id).all() - return sorted(row.priv for row in sp) + return sorted(self.special_privs) def can_access_forum(self, forum): """Whether this member can read the forum's contents.""" diff --git a/app/routes/admin/groups.py b/app/routes/admin/groups.py index 3c2ff3d..3315fdf 100644 --- a/app/routes/admin/groups.py +++ b/app/routes/admin/groups.py @@ -15,7 +15,7 @@ def adm_groups(): # Users with either groups or special privileges users_groups = Member.query.join(GroupMember) users_special = Member.query \ - .join(SpecialPrivilege, Member.id == SpecialPrivilege.mid) + .join(SpecialPrivilege, Member.id == SpecialPrivilege.member_id) users = users_groups.union(users_special) users = sorted(users, key = lambda x: x.name) diff --git a/migrations/versions/44fbbb1fd537_special_privileges_as_a_relationship.py b/migrations/versions/44fbbb1fd537_special_privileges_as_a_relationship.py new file mode 100644 index 0000000..4b7761c --- /dev/null +++ b/migrations/versions/44fbbb1fd537_special_privileges_as_a_relationship.py @@ -0,0 +1,38 @@ +"""special privileges as a relationship + +Revision ID: 44fbbb1fd537 +Revises: 72df33816b21 +Create Date: 2022-05-12 19:15:15.592896 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '44fbbb1fd537' +down_revision = '72df33816b21' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('special_privilege', sa.Column('member_id', sa.Integer(), nullable=True)) + op.drop_index('ix_special_privilege_mid', table_name='special_privilege') + op.create_index(op.f('ix_special_privilege_member_id'), 'special_privilege', ['member_id'], unique=False) + op.drop_constraint('special_privilege_mid_fkey', 'special_privilege', type_='foreignkey') + op.create_foreign_key(None, 'special_privilege', 'member', ['member_id'], ['id']) + op.drop_column('special_privilege', 'mid') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('special_privilege', sa.Column('mid', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'special_privilege', type_='foreignkey') + op.create_foreign_key('special_privilege_mid_fkey', 'special_privilege', 'member', ['mid'], ['id']) + op.drop_index(op.f('ix_special_privilege_member_id'), table_name='special_privilege') + op.create_index('ix_special_privilege_mid', 'special_privilege', ['mid'], unique=False) + op.drop_column('special_privilege', 'member_id') + # ### end Alembic commands ### From 38c4f274a0d3f6381d86ea914ef6888deb303512 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 12 May 2022 20:00:01 +0100 Subject: [PATCH 10/92] perf: optimize away more privilege requests [MIGRATION] This commit contains a new version of the schema. --- app/models/priv.py | 18 +++++++------ app/models/user.py | 8 +++--- master.py | 4 +-- ...e3_relate_groups_with_their_privileges_.py | 25 +++++++++++++++++++ 4 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 migrations/versions/fcf53f1a14e3_relate_groups_with_their_privileges_.py diff --git a/app/models/priv.py b/app/models/priv.py index 765e59c..2977227 100644 --- a/app/models/priv.py +++ b/app/models/priv.py @@ -47,6 +47,9 @@ class Group(db.Model): members = db.relationship('Member', secondary=lambda: GroupMember, back_populates='groups') + # Other fields populated automatically through relations: + # list of privileges + def __init__(self, name, css, descr): self.name = name self.css = css @@ -59,7 +62,7 @@ class Group(db.Model): * Group privileges """ - for gp in GroupPrivilege.query.filter_by(gid=self.id).all(): + for gp in self.privileges: db.session.delete(gp) db.session.commit() @@ -67,8 +70,7 @@ class Group(db.Model): db.session.commit() def privs(self): - gps = GroupPrivilege.query.filter_by(gid=self.id).all() - return sorted(gp.priv for gp in gps) + return sorted(gp.priv for gp in self.privileges) def __repr__(self): return f'' @@ -79,15 +81,17 @@ GroupMember = db.Table('group_member', db.Model.metadata, db.Column('gid', db.Integer, db.ForeignKey('group.id')), db.Column('uid', db.Integer, db.ForeignKey('member.id'))) - -# Many-to-many relationship for privileges granted to groups +# GroupPrivilege: A list of privileges for groups, materialized as a table class GroupPrivilege(db.Model): __tablename__ = 'group_privilege' id = db.Column(db.Integer, primary_key=True) - gid = db.Column(db.Integer, db.ForeignKey('group.id')) + group_id = db.Column(db.Integer, db.ForeignKey('group.id')) + group = db.relationship('Group', backref='privileges', + foreign_keys=group_id) + priv = db.Column(db.String(64)) def __init__(self, group, priv): - self.gid = group.id + self.group = group self.priv = priv diff --git a/app/models/user.py b/app/models/user.py index afe88a5..7a50a24 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -210,10 +210,10 @@ class Member(User): """Check whether the member has the specified privilege.""" if priv in self.special_privs: return True - return db.session.query(Group, GroupPrivilege).filter( - Group.id.in_([g.id for g in self.groups]), - GroupPrivilege.gid==Group.id, - GroupPrivilege.priv==priv).first() is not None + for g in self.groups: + if priv in g.privs(): + return True + return False def special_privileges(self): """List member's special privileges.""" diff --git a/master.py b/master.py index 6a79f4c..e0b9a7d 100755 --- a/master.py +++ b/master.py @@ -78,11 +78,11 @@ def update_groups(): # Update an existing group if g is not None: changes = (g.css != css) or (g.description != descr) or \ - (set(g.privs()) != set(privs)) + (set(g.privs) != set(privs)) g.css = css g.description = descr - for gpriv in GroupPrivilege.query.filter_by(gid=g.id): + for gpriv in g.privs: db.session.delete(gpriv) for priv in privs: db.session.add(GroupPrivilege(g, priv)) diff --git a/migrations/versions/fcf53f1a14e3_relate_groups_with_their_privileges_.py b/migrations/versions/fcf53f1a14e3_relate_groups_with_their_privileges_.py new file mode 100644 index 0000000..36f4205 --- /dev/null +++ b/migrations/versions/fcf53f1a14e3_relate_groups_with_their_privileges_.py @@ -0,0 +1,25 @@ +"""relate groups with their privileges through a relationship + +Revision ID: fcf53f1a14e3 +Revises: 44fbbb1fd537 +Create Date: 2022-05-12 19:43:04.436448 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'fcf53f1a14e3' +down_revision = '44fbbb1fd537' +branch_labels = None +depends_on = None + + +def upgrade(): + # Actually modified by hand for once - Lephe' + op.alter_column('group_privilege', 'gid', new_column_name='group_id') + + +def downgrade(): + op.alter_column('group_privilege', 'group_id', new_column_name='gid') From b7dc2ebbf2ee54afe2a3038b40d0804189cae375 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 12 May 2022 21:39:10 +0200 Subject: [PATCH 11/92] hook: add reminder to build css before commiting when needed --- hooks/pre-commit | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 hooks/pre-commit diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..68bf213 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,8 @@ +#!/bin/bash + +modifiedless=$(make -n css | grep less) +if [[ -n "$modifiedless" ]] +then + echo -e "\e[33;1m[error] remember to run 'make css' before commiting\e[0m" + exit 1 +fi From 8393cf1933610ae01138c4b40a8475f4d0e3cc4c Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 12 May 2022 20:45:00 +0100 Subject: [PATCH 12/92] perf: eagerly load auxiliary data --- app/models/attachment.py | 3 ++- app/models/comment.py | 3 ++- app/models/priv.py | 11 +++++------ app/models/user.py | 16 ++++++++++------ 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/models/attachment.py b/app/models/attachment.py index b710298..a822325 100644 --- a/app/models/attachment.py +++ b/app/models/attachment.py @@ -18,7 +18,8 @@ class Attachment(db.Model): # The comment linked with comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False, index=True) - comment = db.relationship('Comment', backref=backref('attachments')) + comment = db.relationship('Comment', back_populates='attachments', + foreign_keys=comment_id) # The size of the file size = db.Column(db.Integer) diff --git a/app/models/comment.py b/app/models/comment.py index f5cb42a..a7386db 100644 --- a/app/models/comment.py +++ b/app/models/comment.py @@ -20,7 +20,8 @@ class Comment(Post): backref=backref('comments', lazy='dynamic'), foreign_keys=thread_id) - # attachments (relation from Attachment) + attachments = db.relationship('Attachment', back_populates='comment', + lazy='joined') @property def is_top_comment(self): diff --git a/app/models/priv.py b/app/models/priv.py index 2977227..32cecbb 100644 --- a/app/models/priv.py +++ b/app/models/priv.py @@ -17,7 +17,7 @@ class SpecialPrivilege(db.Model): # Member that is granted the privilege member_id = db.Column(db.Integer, db.ForeignKey('member.id'), index=True) - member = db.relationship('Member', backref="special_privs", + member = db.relationship('Member', back_populates="special_privs", foreign_keys=member_id) # Privilege name priv = db.Column(db.String(64)) @@ -45,10 +45,9 @@ class Group(db.Model): description = db.Column(db.UnicodeText) # List of members (lambda delays evaluation) members = db.relationship('Member', secondary=lambda: GroupMember, - back_populates='groups') - - # Other fields populated automatically through relations: - # list of privileges + back_populates='groups', lazy='joined') + # List of privileges + privileges = db.relationship('GroupPrivilege', back_populates='group') def __init__(self, name, css, descr): self.name = name @@ -87,7 +86,7 @@ class GroupPrivilege(db.Model): id = db.Column(db.Integer, primary_key=True) group_id = db.Column(db.Integer, db.ForeignKey('group.id')) - group = db.relationship('Group', backref='privileges', + group = db.relationship('Group', back_populates='privileges', foreign_keys=group_id) priv = db.Column(db.String(64)) diff --git a/app/models/user.py b/app/models/user.py index 7a50a24..afbab8c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -92,7 +92,7 @@ class Member(User): # Groups and related privileges groups = db.relationship('Group', secondary=GroupMember, - back_populates='members') + back_populates='members', lazy='joined') # Personal information, all optional bio = db.Column(db.UnicodeText) @@ -112,6 +112,15 @@ class Member(User): trophies = db.relationship('Trophy', secondary=TrophyMember, back_populates='owners') + # Specially-offered privileges (use self.special_privileges()) + special_privs = db.relationship('SpecialPrivilege', + back_populates='member', lazy='joined') + + + # Other fields populated automatically through relations: + # List of unseen notifications (of type Notification) + # Polls created by the member (of class Poll) + # Access to polymorphic posts # TODO: Check that the query uses the double index on Post.{author_id,type} def comments(self): @@ -138,11 +147,6 @@ class Member(User): return int(level), int(level * 100) % 100 - # Other fields populated automatically through relations: - # List of special privileges - # List of unseen notifications (of type Notification) - # Polls created by the member (of class Poll) - def __init__(self, name, email, password): """Register a new user.""" self.name = name From 13ce27b6828b775c1121be31b26bc04ea6d3291e Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 12 May 2022 21:53:26 +0100 Subject: [PATCH 13/92] perf: avoid N+1 query in recent topics and news --- app/processors/menu.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/processors/menu.py b/app/processors/menu.py index a2879ff..f1824e6 100644 --- a/app/processors/menu.py +++ b/app/processors/menu.py @@ -16,13 +16,17 @@ def menu_processor(): main_forum = Forum.query.filter_by(parent=None).first() # Constructing last active topics - raw = db.session.execute( """SELECT topic.id FROM topic + rows = db.session.execute( """SELECT topic.id FROM topic INNER JOIN comment ON topic.thread_id = comment.thread_id INNER JOIN post ON post.id = comment.id GROUP BY topic.id ORDER BY MAX(post.date_created) DESC LIMIT 20;""") - last_active_topics = [Topic.query.get(id) for id in raw] + ids = [row[0] for row in rows] + # Somewhat inelegant, but much better than loading individually + recent_topics = db.session.query(Topic).filter(Topic.id.in_(ids)).all() + recent_topics = sorted(recent_topics, key=lambda t: ids.index(t.id)) + # Filter the topics the user can view and limit to 10 if current_user.is_authenticated: @@ -30,10 +34,10 @@ def menu_processor(): else: f = lambda t: t.forum.is_default_accessible() - last_active_topics = list(filter(f, last_active_topics))[:10] + recent_topics = list(filter(f, recent_topics))[:10] # Constructing last news - raw = db.session.execute( """SELECT topic.id FROM topic + rows = db.session.execute( """SELECT topic.id FROM topic INNER JOIN forum ON topic.forum_id = forum.id INNER JOIN comment ON topic.thread_id = comment.thread_id INNER JOIN post ON post.id = comment.id @@ -42,8 +46,10 @@ def menu_processor(): ORDER BY MIN(post.date_created) DESC LIMIT 10; """) - last_news = [Topic.query.get(id) for id in raw] + ids = [row[0] for row in rows] + recent_news = db.session.query(Topic).filter(Topic.id.in_(ids)).all() + recent_news = sorted(recent_news, key=lambda t: ids.index(t.id)) return dict(login_form=login_form, search_form=search_form, - main_forum=main_forum, last_active_topics=last_active_topics, - last_news=last_news) + main_forum=main_forum, last_active_topics=recent_topics, + last_news=recent_news) From 011ea3d2a63457425fd68e82e53edbc445e4b056 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 19 May 2022 18:50:49 +0100 Subject: [PATCH 14/92] programs: add the model for tags [MIGRATION] This commit contains a new version of the schema. --- app/models/__init__.py | 1 + app/models/program.py | 2 + app/models/tag.py | 15 ++++++++ .../1de8b6b6aed8_add_tags_for_programs.py | 38 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 app/models/tag.py create mode 100644 migrations/versions/1de8b6b6aed8_add_tags_for_programs.py diff --git a/app/models/__init__.py b/app/models/__init__.py index fd484d2..58c31dd 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -4,3 +4,4 @@ from app.models.forum import Forum from app.models.topic import Topic from app.models.notification import Notification from app.models.program import Program +from app.models.tag import Tag diff --git a/app/models/program.py b/app/models/program.py index 592e2e1..7489510 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -19,6 +19,8 @@ class Program(Post): thread = db.relationship('Thread', foreign_keys=thread_id, back_populates='owner_program') + tags = db.relationship('Tag', back_populates='post', lazy='joined') + # TODO: Number of views, statistics, attached files, etc def __init__(self, author, title, thread): diff --git a/app/models/tag.py b/app/models/tag.py new file mode 100644 index 0000000..25cfd20 --- /dev/null +++ b/app/models/tag.py @@ -0,0 +1,15 @@ +from app import db + +class Tag(db.Model): + __tablename__ = 'tag' + id = db.Column(db.Integer, primary_key=True) + + # Tagged post + post_id = db.Column(db.Integer, db.ForeignKey('post.id'), index=True) + post = db.relationship('Post', back_populates='tags', foreign_keys=post_id) + # Tag name + name = db.Column(db.String(64), index=True) + + def __init__(self, post, tag): + self.post = post + self.tag = tag diff --git a/migrations/versions/1de8b6b6aed8_add_tags_for_programs.py b/migrations/versions/1de8b6b6aed8_add_tags_for_programs.py new file mode 100644 index 0000000..88fa5e9 --- /dev/null +++ b/migrations/versions/1de8b6b6aed8_add_tags_for_programs.py @@ -0,0 +1,38 @@ +"""add tags for programs + +Revision ID: 1de8b6b6aed8 +Revises: fcf53f1a14e3 +Create Date: 2022-05-19 18:50:12.894735 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1de8b6b6aed8' +down_revision = 'fcf53f1a14e3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tag', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('post_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(length=64), nullable=True), + sa.ForeignKeyConstraint(['post_id'], ['post.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_tag_name'), 'tag', ['name'], unique=False) + op.create_index(op.f('ix_tag_post_id'), 'tag', ['post_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_tag_post_id'), table_name='tag') + op.drop_index(op.f('ix_tag_name'), table_name='tag') + op.drop_table('tag') + # ### end Alembic commands ### From 0e1b434f7db75cb22b07ffcfd7cdc97f53b49157 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 19 May 2022 19:12:09 +0100 Subject: [PATCH 15/92] program: fix tag assignment --- app/models/post.py | 3 +++ app/models/program.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/post.py b/app/models/post.py index 85d2dea..c272a0e 100644 --- a/app/models/post.py +++ b/app/models/post.py @@ -21,6 +21,9 @@ class Post(db.Model): index=True) author = db.relationship('User', backref="posts", foreign_keys=author_id) + # Tags, for programs and tutorials + tags = db.relationship('Tag', back_populates='post', lazy='joined') + __mapper_args__ = { 'polymorphic_identity': __tablename__, 'polymorphic_on': type diff --git a/app/models/program.py b/app/models/program.py index 7489510..5a0569f 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -12,15 +12,12 @@ class Program(Post): title = db.Column(db.Unicode(128)) # TODO: Category (games/utilities/lessons) - # TODO: Tags # TODO: Compatible calculator models thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) thread = db.relationship('Thread', foreign_keys=thread_id, back_populates='owner_program') - tags = db.relationship('Tag', back_populates='post', lazy='joined') - # TODO: Number of views, statistics, attached files, etc def __init__(self, author, title, thread): From b047ed97afcd3f0ec92be684814ca81932117a2a Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 19 May 2022 20:34:23 +0100 Subject: [PATCH 16/92] programs: program creation + view + comments This is very much a work in progress, but the main ideas are here. [MIGRATION] This commit contains a new version of the schema. --- app/__init__.py | 1 + app/forms/programs.py | 14 +++++ app/models/program.py | 4 +- app/models/tag.py | 2 +- app/routes/__init__.py | 2 +- app/routes/programs/index.py | 4 +- app/routes/programs/program.py | 62 +++++++++++++++++++ app/routes/programs/submit.py | 59 ++++++++++++++++++ app/templates/base/navbar/programs.html | 3 + app/templates/programs/index.html | 17 +++-- app/templates/programs/program.html | 56 ++++++++++++++++- app/templates/programs/submit.html | 56 +++++++++++++++++ app/utils/converters.py | 11 +++- ...43c24_rename_program_title_program_name.py | 25 ++++++++ 14 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 app/forms/programs.py create mode 100644 app/routes/programs/program.py create mode 100644 app/routes/programs/submit.py create mode 100644 app/templates/programs/submit.html create mode 100644 migrations/versions/fa34c9f43c24_rename_program_title_program_name.py diff --git a/app/__init__.py b/app/__init__.py index 1049e63..e6b64da 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -27,6 +27,7 @@ login.login_message = "Veuillez vous authentifier avant de continuer." from app.utils.converters import * app.url_map.converters['forum'] = ForumConverter app.url_map.converters['topicpage'] = TopicPageConverter +app.url_map.converters['programpage'] = ProgramPageConverter # Register routes from app import routes diff --git a/app/forms/programs.py b/app/forms/programs.py new file mode 100644 index 0000000..1f462e7 --- /dev/null +++ b/app/forms/programs.py @@ -0,0 +1,14 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField +from wtforms.validators import InputRequired, Length +import app.utils.validators as vf +from app.utils.antibot_field import AntibotField +from app.forms.forum import CommentForm + +class ProgramCreationForm(CommentForm): + name = StringField('Nom du programme', + validators=[InputRequired(), Length(min=3, max=64)]) + + tags = StringField('Liste de tags', description='Séparés par des virgules') + + submit = SubmitField('Soumettre le programme') diff --git a/app/models/program.py b/app/models/program.py index 5a0569f..92c43db 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -9,7 +9,7 @@ class Program(Post): id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True) # Program name - title = db.Column(db.Unicode(128)) + name = db.Column(db.Unicode(128)) # TODO: Category (games/utilities/lessons) # TODO: Compatible calculator models @@ -43,4 +43,4 @@ class Program(Post): db.session.delete(self) def __repr__(self): - return f'' + return f'' diff --git a/app/models/tag.py b/app/models/tag.py index 25cfd20..5d96089 100644 --- a/app/models/tag.py +++ b/app/models/tag.py @@ -12,4 +12,4 @@ class Tag(db.Model): def __init__(self, post, tag): self.post = post - self.tag = tag + self.name = tag diff --git a/app/routes/__init__.py b/app/routes/__init__.py index b39260d..b2d87e5 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -7,7 +7,7 @@ from app.routes.admin import index, groups, account, trophies, forums, \ from app.routes.forum import index, topic from app.routes.polls import vote, delete from app.routes.posts import edit -from app.routes.programs import index +from app.routes.programs import index, submit, program from app.routes.api import markdown try: diff --git a/app/routes/programs/index.py b/app/routes/programs/index.py index 7d267c1..333a485 100644 --- a/app/routes/programs/index.py +++ b/app/routes/programs/index.py @@ -4,5 +4,5 @@ from app.utils.render import render @app.route('/programmes') def program_index(): - programs = Program.query.all() - return render('/programs/index.html') + programs = Program.query.order_by(Program.date_created.desc()).all() + return render('/programs/index.html', programs=programs) diff --git a/app/routes/programs/program.py b/app/routes/programs/program.py new file mode 100644 index 0000000..f02294c --- /dev/null +++ b/app/routes/programs/program.py @@ -0,0 +1,62 @@ +from app import app, db +from app.models.program import Program +from app.models.comment import Comment +from app.models.thread import Thread +from app.utils.render import render +from app.forms.forum import CommentForm, AnonymousCommentForm +from config import V5Config + +from flask_login import current_user +from flask import redirect, url_for, flash + +@app.route('/programmes/', methods=['GET','POST']) +def program_view(page): + p, page = page + + if current_user.is_authenticated: + form = CommentForm() + else: + form = AnonymousCommentForm() + + if form.validate_on_submit() and ( + V5Config.ENABLE_GUEST_POST or current_user.is_authenticated): + + # Manage author + if current_user.is_authenticated: + author = current_user + else: + author = Guest(form.pseudo.data) + db.session.add(author) + + # Create comment + c = Comment(author, form.message.data, p.thread) + db.session.add(c) + db.session.commit() + + # Manage files + attachments = [] + for file in form.attachments.data: + if file.filename != "": + a = Attachment(file, c) + attachments.append((a, file)) + db.session.add(a) + db.session.commit() + for a, file in attachments: + a.set_file(file) + + # Update member's xp and trophies + if current_user.is_authenticated: + current_user.add_xp(1) + current_user.update_trophies('new-post') + + flash('Message envoyé', 'ok') + # Redirect to empty the form + return redirect(url_for('program_view', page=(p, "fin"), _anchor=c.id)) + + if page == -1: + page = (p.thread.comments.count() - 1) // Thread.COMMENTS_PER_PAGE + 1 + + comments = p.thread.comments.order_by(Comment.date_created.asc()) \ + .paginate(page, Thread.COMMENTS_PER_PAGE, True) + + return render('/programs/program.html', p=p, form=form, comments=comments) diff --git a/app/routes/programs/submit.py b/app/routes/programs/submit.py new file mode 100644 index 0000000..0675a84 --- /dev/null +++ b/app/routes/programs/submit.py @@ -0,0 +1,59 @@ +from app import app, db +from app.models.program import Program +from app.models.thread import Thread +from app.models.comment import Comment +from app.models.tag import Tag +from app.utils.render import render +from app.forms.programs import ProgramCreationForm + +from flask_login import current_user +from flask import redirect, url_for, flash + +@app.route('/programmes/soumettre', methods=['GET', 'POST']) +def program_submit(): + + if current_user.is_authenticated: + form = ProgramCreationForm() + if form.validate_on_submit(): + # First create a new thread + # TODO: Reuse a thread when performing topic promotion + th = Thread() + db.session.add(th) + db.session.commit() + + # Create its top comment + c = Comment(current_user, form.message.data, th) + db.session.add(c) + db.session.commit() + th.set_top_comment(c) + db.session.merge(th) + + # Then build the actual program + p = Program(current_user, form.name.data, th) + db.session.add(p) + db.session.commit() + + # Add tags + # TODO: Check tags against a predefined set + for tag in form.tags.data.split(","): + db.session.add(Tag(p, tag.strip())) + db.session.commit() + + # Manage files + attachments = [] + for file in form.attachments.data: + if file.filename != "": + a = Attachment(file, c) + attachments.append((a, file)) + db.session.add(a) + db.session.commit() + for a, file in attachments: + a.set_file(file) + + current_user.add_xp(20) + current_user.update_trophies('new-program') + + flash('Le programme a bien été soumis', 'ok') + return redirect(url_for('program_index')) + + return render('/programs/submit.html', form=form) diff --git a/app/templates/base/navbar/programs.html b/app/templates/base/navbar/programs.html index 75953ee..3e74cdd 100644 --- a/app/templates/base/navbar/programs.html +++ b/app/templates/base/navbar/programs.html @@ -4,6 +4,9 @@ Programmes + Index des programmes +
    + diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html index f141ddf..17ba396 100644 --- a/app/templates/programs/index.html +++ b/app/templates/programs/index.html @@ -6,15 +6,24 @@ {% block content %}
    -

    Tous les programmes

    +

    Publications récentes

    +

    [Ici quelques "cartes" de programmes récents]

    +

    Populaires

    +

    [Ici quelques "cartes" de programmes populaires aléatoires]

    + +

    Poster un programme

    +

    Poster un nouveau programme sur Planète Casio

    + +

    Tous les programmes

    - + {% for p in programs %} - + - + + {% endfor %}
    IDNomAuteurPublié le
    IDNomAuteurPublié leTags
    {{ p.id }}{{ p.name }}{{ p.name }} {{ p.author.name }}{{ p.date_created }}
    {{ p.date_created | dyndate }}{% for tag in p.tags %}{{ tag.name }} {% endfor %}
    diff --git a/app/templates/programs/program.html b/app/templates/programs/program.html index a6ca9fe..2c83c47 100644 --- a/app/templates/programs/program.html +++ b/app/templates/programs/program.html @@ -7,18 +7,18 @@ {% block title %} -

    Programme {{ program.name }}

    +

    Programme: {{ p.name }}

    {% endblock %} {% block content %}
    - {{ widget_user.profile(program.author) }} + {{ widget_user.profile(p.author) }}
    - {{ program.title }} + {{ p.title }}

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae @@ -78,4 +78,54 @@ auctor a. Praesent sit amet libero risus.

    + {% if p.thread.top_comment %} + {% call widget_thread.thread_leader(p.thread.top_comment) %} +
    +
    Posté le {{ p.date_created | dyndate }}
    + {{ widget_thread.post_actions(p) }} +
    + {{ p.thread.top_comment.text | md }} + {{ widget_attachments.attachments(p.thread.top_comment) }} + {% endcall %} + {% endif %} + + {{ widget_pagination.paginate(comments, 'program_view', p) }} + + {{ widget_thread.thread(comments.items, p.thread.top_comment) }} + + {{ widget_pagination.paginate(comments, 'program_view', p) }} + + {% if V5Config.ENABLE_GUEST_POST or current_user.is_authenticated %} +
    +

    Commenter le programme

    +
    + {{ form.hidden_tag() }} + + {% if form.pseudo %} +
    + {{ form.pseudo.label }} + {{ form.pseudo }} + {% for error in form.pseudo.errors %} + {{ error }} + {% endfor %} + {{ form.ab }} +
    + {% endif %} + + {{ widget_editor.text_editor(form.message, label=False) }} + +
    + {{ form.attachments.label }} +
    + {{ form.attachments }} + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
    +
    + +
    {{ form.submit(class_='bg-ok') }}
    +
    +
    + {% endif %} {% endblock %} diff --git a/app/templates/programs/submit.html b/app/templates/programs/submit.html new file mode 100644 index 0000000..0ac58f7 --- /dev/null +++ b/app/templates/programs/submit.html @@ -0,0 +1,56 @@ +{% extends "base/base.html" %} +{% import "widgets/editor.html" as widget_editor %} + +{% block title %} +

    Programmes de Planète Casio

    +{% endblock %} + +{% block content %} +
    + + {% if current_user.is_authenticated %} +
    +

    Soumettre un programme

    +
    + {{ form.hidden_tag() }} + +
    + {{ form.name.label }} + {{ form.name() }} + {% for error in form.name.errors %} + {{ error }} + {% endfor %} +
    + +
    + {{ form.tags.label }} +
    {{ form.tags.description }}
    + {{ form.tags() }} + {% for error in form.tags.errors %} + {{ error }} + {% endfor %} +
    + + {{ widget_editor.text_editor(form.message) }} + +
    + {{ form.attachments.label }} +
    + {{ form.attachments }} + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
    +
    + +
    {{ form.submit(class_='bg-ok') }}
    +
    +
    + {% else %} +
    +

    Vous devez être connecté·e pour poster un programme (pour que le programme puisse être modifié ensuite). Si vous n'avez pas de compte, vous pouvez vous inscrire ici.

    +
    + {% endif %} + +
    +{% endblock %} diff --git a/app/utils/converters.py b/app/utils/converters.py index e887f9b..6936a61 100644 --- a/app/utils/converters.py +++ b/app/utils/converters.py @@ -19,6 +19,7 @@ For more information, see the Werkzeug documentation: from werkzeug.routing import BaseConverter, ValidationError from app.models.forum import Forum from app.models.topic import Topic +from app.models.program import Program from slugify import slugify @@ -44,6 +45,7 @@ class PageConverter(BaseConverter): # number, a slug, or a page number followed by a slug regex = r'(\d+)(?:/(\d+)|/fin)?(?:/[\w-]+)?' object = None + get_title = lambda o: "empty-title" def to_python(self, url): tid, *args = url.split('/') @@ -70,11 +72,16 @@ class PageConverter(BaseConverter): def to_url(self, object_and_page): o, page = object_and_page page = str(page) if page != -1 else "fin" - slug = slugify(o.title) + slug = slugify(self.get_title(o)) return f'{o.id}/{page}/{slug}' class TopicPageConverter(PageConverter): object = Topic + get_title = lambda self, t: t.title + +class ProgramPageConverter(PageConverter): + object = Program + get_title = lambda self, p: p.name # Export only the converter classes -__all__ = "ForumConverter TopicPageConverter".split() +__all__ = ["ForumConverter", "TopicPageConverter", "ProgramPageConverter"] diff --git a/migrations/versions/fa34c9f43c24_rename_program_title_program_name.py b/migrations/versions/fa34c9f43c24_rename_program_title_program_name.py new file mode 100644 index 0000000..9a42430 --- /dev/null +++ b/migrations/versions/fa34c9f43c24_rename_program_title_program_name.py @@ -0,0 +1,25 @@ +"""rename Program.title -> Program.name + +Revision ID: fa34c9f43c24 +Revises: 1de8b6b6aed8 +Create Date: 2022-05-19 20:16:47.855756 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'fa34c9f43c24' +down_revision = '1de8b6b6aed8' +branch_labels = None +depends_on = None + + +def upgrade(): + # Once again modified by hand - Lephe' + op.alter_column('program', 'title', new_column_name='name') + + +def downgrade(): + op.alter_column('program', 'name', new_column_name='title') From 84066eaca3a8985b504311f01b251872db79b327 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 26 May 2022 20:07:50 +0100 Subject: [PATCH 17/92] css: fix antibot field being visible --- app/static/css/form.css | 2 +- app/static/less/form.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/css/form.css b/app/static/css/form.css index 23dff8a..b780e4a 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -95,7 +95,7 @@ font-weight: 400; margin-top: 5px; } -.form .abfield { +.form input[type='email'].abfield { display: none; } .form.filter { diff --git a/app/static/less/form.less b/app/static/less/form.less index c391989..36e37b4 100644 --- a/app/static/less/form.less +++ b/app/static/less/form.less @@ -109,7 +109,7 @@ } /* anti-bots field */ - .abfield { + input[type='email'].abfield { display: none; } } From 6756838882bf6fff61408471844dd9fb02d8bb22 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 26 May 2022 20:08:01 +0100 Subject: [PATCH 18/92] forum: factor attachment creation code --- app/models/comment.py | 13 +++++++++++++ app/routes/forum/topic.py | 12 +----------- app/routes/programs/program.py | 13 ++----------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/app/models/comment.py b/app/models/comment.py index a7386db..e502362 100644 --- a/app/models/comment.py +++ b/app/models/comment.py @@ -1,5 +1,6 @@ from app import db from app.models.post import Post +from app.models.attachment import Attachment from sqlalchemy.orm import backref @@ -54,5 +55,17 @@ class Comment(Post): db.session.commit() db.session.delete(self) + def create_attachments(self, multiple_file_field_data): + """Create attachements from a form's MultipleFileField.data.""" + attachments = [] + for file in multiple_file_field_data: + if file.filename != "": + a = Attachment(file, self) + attachments.append((a, file)) + db.session.add(a) + db.session.commit() + for a, file in attachments: + a.set_file(file) + def __repr__(self): return f'' diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index ebb2be7..31a9f5e 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -46,17 +46,7 @@ def forum_topic(f, page): c = Comment(author, form.message.data, t.thread) db.session.add(c) db.session.commit() - - # Manage files - attachments = [] - for file in form.attachments.data: - if file.filename != "": - a = Attachment(file, c) - attachments.append((a, file)) - db.session.add(a) - db.session.commit() - for a, file in attachments: - a.set_file(file) + c.create_attachments(form.attachments.data) # Update member's xp and trophies if current_user.is_authenticated: diff --git a/app/routes/programs/program.py b/app/routes/programs/program.py index f02294c..0229313 100644 --- a/app/routes/programs/program.py +++ b/app/routes/programs/program.py @@ -1,4 +1,5 @@ from app import app, db +from app.models.user import Guest from app.models.program import Program from app.models.comment import Comment from app.models.thread import Thread @@ -32,17 +33,7 @@ def program_view(page): c = Comment(author, form.message.data, p.thread) db.session.add(c) db.session.commit() - - # Manage files - attachments = [] - for file in form.attachments.data: - if file.filename != "": - a = Attachment(file, c) - attachments.append((a, file)) - db.session.add(a) - db.session.commit() - for a, file in attachments: - a.set_file(file) + c.create_attachments(form.attachments.data) # Update member's xp and trophies if current_user.is_authenticated: From c26861527b69182d549e16ff7d09bcb98016affe Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 26 May 2022 20:16:29 +0100 Subject: [PATCH 19/92] admin: remove trophy edition interface (#82) It was decided to keep using the master script to update them. --- app/routes/__init__.py | 2 +- app/routes/admin/trophies.py | 77 ------------------------- app/templates/admin/delete_trophy.html | 28 --------- app/templates/admin/edit_trophy.html | 58 ------------------- app/templates/admin/index.html | 1 - app/templates/admin/trophies.html | 80 -------------------------- 6 files changed, 1 insertion(+), 245 deletions(-) delete mode 100644 app/routes/admin/trophies.py delete mode 100644 app/templates/admin/delete_trophy.html delete mode 100644 app/templates/admin/edit_trophy.html delete mode 100644 app/templates/admin/trophies.html diff --git a/app/routes/__init__.py b/app/routes/__init__.py index b2d87e5..df23730 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -2,7 +2,7 @@ from app.routes import index, search, users, tools, development from app.routes.account import login, account, notification, polls -from app.routes.admin import index, groups, account, trophies, forums, \ +from app.routes.admin import index, groups, account, forums, \ attachments, config, members, polls, login_as from app.routes.forum import index, topic from app.routes.polls import vote, delete diff --git a/app/routes/admin/trophies.py b/app/routes/admin/trophies.py deleted file mode 100644 index b0ac084..0000000 --- a/app/routes/admin/trophies.py +++ /dev/null @@ -1,77 +0,0 @@ -from flask import request, flash, redirect, url_for -from app.utils.priv_required import priv_required -from app.models.trophy import Trophy, Title -from app.forms.trophy import TrophyForm, DeleteTrophyForm -from app.utils.render import render -from app import app, db - - -@app.route('/admin/trophees', methods=['GET', 'POST']) -@priv_required('misc.admin-panel', 'edit.trophies') -def adm_trophies(): - form = TrophyForm() - if request.method == "POST": - if form.validate_on_submit(): - is_title = form.title.data - if is_title: - trophy = Title(form.name.data, form.desc.data, - form.hidden.data, form.css.data) - else: - trophy = Trophy(form.name.data, form.desc.data, - form.hidden.data) - db.session.add(trophy) - db.session.commit() - flash(f'Nouveau {["trophée", "titre"][is_title]} ajouté', 'ok') - else: - flash('Erreur lors de la création du trophée', 'error') - - trophies = Trophy.query.all() - return render('admin/trophies.html', trophies=trophies, - form=form) - - -@app.route('/admin/trophees//editer', methods=['GET', 'POST']) -@priv_required('misc.admin-panel', 'edit.trophies') -def adm_edit_trophy(trophy_id): - trophy = Trophy.query.filter_by(id=trophy_id).first_or_404() - - form = TrophyForm() - if request.method == "POST": - if form.validate_on_submit(): - is_title = form.title.data != "" - if is_title: - trophy.name = form.name.data - trophy.description = form.desc.data - trophy.title = form.title.data - trophy.hidden = form.hidden.data - trophy.css = form.css.data - else: - trophy.name = form.name.data - trophy.description = form.desc.data - trophy.hidden = form.hidden.data - db.session.merge(trophy) - db.session.commit() - flash(f'{["Trophée", "Titre"][is_title]} modifié', 'ok') - return redirect(url_for('adm_trophies')) - else: - flash('Erreur lors de la création du trophée', 'error') - return render('admin/edit_trophy.html', trophy=trophy, form=form) - - -@app.route('/admin/trophees//supprimer', methods=['GET', 'POST']) -@priv_required('misc.admin-panel', 'edit.trophies') -def adm_delete_trophy(trophy_id): - trophy = Trophy.query.filter_by(id=trophy_id).first_or_404() - - # TODO: Add an overview of what will be deleted. - del_form = DeleteTrophyForm() - if request.method == "POST": - if del_form.validate_on_submit(): - trophy.delete() - db.session.commit() - flash('Trophée supprimé', 'ok') - return redirect(url_for('adm_trophies')) - else: - flash('Erreur lors de la suppression du trophée', 'error') - del_form.delete.data = False # Force to tick to delete the trophy - return render('admin/delete_trophy.html', trophy=trophy, del_form=del_form) diff --git a/app/templates/admin/delete_trophy.html b/app/templates/admin/delete_trophy.html deleted file mode 100644 index 9fdde33..0000000 --- a/app/templates/admin/delete_trophy.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "base/base.html" %} - -{% block title %} -Panneau d'administration » Titres et trophées »

    Suppression du trophée '{{ trophy.name }}'

    -{% endblock %} - -{% block content %} -
    -

    Confirmer la suppression du trophée

    -

    Le trophée '{{ trophy.name }}' que vous allez supprimer est lié à :

    -
      -
    • {{ trophy.owners | length }} membre{{ trophy.owners|length|pluralize }}
    • -
    - -
    - {{ del_form.hidden_tag() }} -
    - {{ del_form.delete.label }} - {{ del_form.delete(checked=False) }} -
    {{ del_form.delete.description }}
    - {% for error in del_form.delete.errors %} - {{ error }} - {% endfor %} -
    -
    {{ del_form.submit(class_="bg-error") }}
    -
    -
    -{% endblock %} diff --git a/app/templates/admin/edit_trophy.html b/app/templates/admin/edit_trophy.html deleted file mode 100644 index ec4f375..0000000 --- a/app/templates/admin/edit_trophy.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "base/base.html" %} - -{% block title %} -Panneau d'administration » Titres et trophées »

    Édition du trophée '{{ trophy.name }}'

    -{% endblock %} - -{% block content %} -
    -
    - {{ form.hidden_tag() }} -

    Éditer le trophée

    - -
    - - {{ trophy.name }} -
    - -
    - {{ form.name.label }} - {{ form.name(value=trophy.name) }} - {% for error in form.name.errors %} - {{ error }} - {% endfor %} -
    -
    - {{ form.desc.label }} - {{ form.desc(value=trophy.description) }} - {% for error in form.desc.errors %} - {{ error }} - {% endfor %} -
    -
    - {{ form.hidden.label }} - {{ form.hidden(checked=trophy.hidden) }} -
    {{ form.hidden.description }}
    - {% for error in form.hidden.errors %} - {{ error }} - {% endfor %} -
    -
    - {{ form.title.label }} - {{ form.title() }} -
    {{ form.title.description }}
    - {% for error in form.title.errors %} - {{ error }} - {% endfor %} -
    -
    - {{ form.css.label }} -
    {{ form.css.description }}
    - {{ form.css(value=trophy.css) }} - {% for error in form.css.errors %} - {{ error }} - {% endfor %} -
    -
    {{ form.submit(class_="bg-ok") }}
    -
    -{% endblock %} diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html index 6cc7a28..d0d57cc 100644 --- a/app/templates/admin/index.html +++ b/app/templates/admin/index.html @@ -10,7 +10,6 @@
    • Groupes et privilèges
    • Liste des membres
    • -
    • Titres et trophées
    • Arbre des forums
    • Sondages
    • Pièces-jointes
    • diff --git a/app/templates/admin/trophies.html b/app/templates/admin/trophies.html deleted file mode 100644 index c6f2349..0000000 --- a/app/templates/admin/trophies.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "base/base.html" %} - -{% block title %} -Panneau d'administration »

      Titres et trophées

      -{% endblock %} - -{% block content %} -
      -

      Cette page présente une vue d'ensemble des titres et trophées. Les - conditions d'obtention exactes des trophées sont définies dans le code et - non dans la base de données.

      - -

      Titres et trophées

      - - - - - - {% for trophy in trophies %} - - - {% if trophy | is_title %} - - {% else %} - - {% endif %} - - - - - {% endfor %} -
      NomTitreStyleModifierSupprimer
      - {{ trophy.name }}{{ trophy.name }}OuiNon{{ trophy.css }}ModifierSupprimer
      -
      - -
      - - {{ form.hidden_tag() }} -

      Nouveau trophée

      -
      - {{ form.name.label }} - {{ form.name }} - {% for error in form.name.errors %} - {{ error }} - {% endfor %} -
      -
      - {{ form.desc.label }} - {{ form.desc }} - {% for error in form.desc.errors %} - {{ error }} - {% endfor %} -
      -
      - {{ form.hidden.label }} - {{ form.hidden }} -
      {{ form.hidden.description }}
      - {% for error in form.hidden.errors %} - {{ error }} - {% endfor %} -
      -
      - {{ form.title.label }} - {{ form.title }} -
      {{ form.title.description }}
      - {% for error in form.title.errors %} - {{ error }} - {% endfor %} -
      -
      - {{ form.css.label }} -
      {{ form.css.description }}
      - {{ form.css }} - {% for error in form.css.errors %} - {{ error }} - {% endfor %} -
      -
      {{ form.submit(class_="bg-ok") }}
      -
      -{% endblock %} From 85323e896d60912c8ced162b261fda1678bfd6be Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 26 May 2022 21:24:50 +0100 Subject: [PATCH 20/92] forum: fix edit timestamp recording + display On the preproduction server there are messages that have an edit timestamp some 20 ns after their creation, for some reason. --- app/routes/posts/edit.py | 3 ++- app/templates/widgets/thread.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index b62b2ee..ea988a3 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -26,7 +26,7 @@ def edit_post(postid): p = Post.query.filter_by(id=postid).first_or_404() - # Check permissions. TODO: Allow guests to edit their posts? + # Check permissions if not current_user.can_edit_post(p): abort(403) @@ -68,6 +68,7 @@ def edit_post(postid): attachments.append((a, file)) db.session.add(a) + comment.touch() db.session.add(comment) if isinstance(p, Topic): diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index c315060..a1e87f7 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -61,7 +61,7 @@
      - {% if c.date_created != c.date_modified %} + {% if (c.date_modified - c.date_created).seconds > 120 %} {% endif %} {{ post_actions(c) }} From f4b9110ce2e5da223f38ab3cc41ddd25e429ffe4 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 26 May 2022 23:01:02 +0100 Subject: [PATCH 21/92] master: fix group update --- master.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/master.py b/master.py index e0b9a7d..d587555 100755 --- a/master.py +++ b/master.py @@ -78,11 +78,11 @@ def update_groups(): # Update an existing group if g is not None: changes = (g.css != css) or (g.description != descr) or \ - (set(g.privs) != set(privs)) + (set(g.privs()) != set(privs)) g.css = css g.description = descr - for gpriv in g.privs: + for gpriv in g.privileges: db.session.delete(gpriv) for priv in privs: db.session.add(GroupPrivilege(g, priv)) From c74abf3fcca4b4a739e94ebd21425bd83c8dad09 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sun, 12 Jun 2022 18:11:46 +0100 Subject: [PATCH 22/92] post: add a tagging system, with a common base set of tags Adds the tagging system, with 3 types of tags: * Calculator models grouped by compatibility classes * Programming languages * Game, tools, and course categories [MIGRATION] This commit contains a new version of the schema. [BREAKS] This commit breaks existing tag assignments. [MASTER] Run the 'update-tags' command of master.py. --- app/data/tags.yaml | 102 ++++++++++++++++++ app/models/program.py | 6 +- app/models/tag.py | 34 +++++- master.py | 44 +++++++- .../189bbc0e1543_add_tag_information.py | 42 ++++++++ 5 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 app/data/tags.yaml create mode 100644 migrations/versions/189bbc0e1543_add_tag_information.py diff --git a/app/data/tags.yaml b/app/data/tags.yaml new file mode 100644 index 0000000..8e09d88 --- /dev/null +++ b/app/data/tags.yaml @@ -0,0 +1,102 @@ +# This is a list of all tags, sorted by category. The category names are used +# to name CSS rules and shouldn't be changed directly. + +# The following category groups calculators by common compatibility properties. +# Each comment indicates why the group should exist on its own rather than +# being merged with another one. +calc: + # Middle-school level, only basic algorithms; a unique property in this list. + fx92: + pretty: fx-92 Scientifique Collège+ + # Some of the most limited Graph models, no add-ins. + g25: + pretty: Graph 25/25+E/25+EII + # The whole series with more Basic constructs than g25, but SH3 for add-ins + # We don't separate based on whether an OS update is required (deemed safe) + gsh3: + pretty: Graph 35+/75/85/95 (SH3) + # Same as gsh3, but with SH4 for add-ins; support CasioPython + gsh4: + pretty: Graph 35+/35+E/75+/75+E (SH4) + # Like gsh3, but has Python; also; issues with the display and MonochromLib + g35+e2: + pretty: Graph 35+E II + # Color display, nothing like the previous models + cg20: + pretty: fx-CG 10/20/Prizm + # Like cg20, but has Python, and some incompatibilities on add-in + g90+e: + pretty: Graph 90+E + # Different series entirely; has an SDK for add-ins + cp300: + pretty: Classpad 300/330 + # Like cp300, but does not have an SDK + cp330+: + pretty: Classpad 330+ + # Color display, entirely new model; no SDK + cp400: + pretty: Classpad 400/400+E + +lang: + basic: + pretty: Basic CASIO + cbasic: + pretty: C.Basic + python: + pretty: Python + c: + pretty: C/C++ (add-in) + lua: + pretty: LuaFX + other: + pretty: "Langage: autre" + +games: + adventure: + pretty: Aventure + fighting: + pretty: Combat + other: + pretty: "Jeu: autre" + platform: + pretty: Plateforme + puzzle: + pretty: Puzzle + rpg: + pretty: RPG + rythm: + pretty: Rythme + shooting: + pretty: Tir/FPS + simulation: + pretty: Simulation + sport: + pretty: Sport + strategy: + pretty: Stratégie + survival: + pretty: Survie + +tools: + conversion: + pretty: Outil de conversion + graphics: + pretty: Outil graphique + science: + pretty: Outil scientifique + programming: + pretty: Outil pour programmer + other: + pretty: "Outil: autre" + +courses: + math: + pretty: Maths + physics: + pretty: Physique + engineering: + pretty: SI/Électronique + economics: + pretty: Économie + other: + pretty: "Cours: autre" diff --git a/app/models/program.py b/app/models/program.py index 92c43db..23bf006 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -13,12 +13,16 @@ class Program(Post): # TODO: Category (games/utilities/lessons) # TODO: Compatible calculator models + # TODO: Number of views, statistics, etc + # Thread with the program description (top comment) and comments thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) thread = db.relationship('Thread', foreign_keys=thread_id, back_populates='owner_program') - # TODO: Number of views, statistics, attached files, etc + # Implicit attributes: + # * tags (inherited from Post) + # * attachements (available at thread.top_comment.attachments) def __init__(self, author, title, thread): """ diff --git a/app/models/tag.py b/app/models/tag.py index 5d96089..a7ad50d 100644 --- a/app/models/tag.py +++ b/app/models/tag.py @@ -1,15 +1,43 @@ from app import db +class TagInformation(db.Model): + """Detailed information about tags, by dot-string tag identifier.""" + + __tablename__ = 'tag_information' + # The ID is the dot-string of the tag (eg. "calc.g35+e2") + id = db.Column(db.String(64), primary_key=True) + + # List of uses. Note how we load tag information along individual tags, but + # we don't load uses unless the field is accessed. + uses = db.relationship('Tag', back_populates='tag', lazy='dynamic') + + # Pretty name + pretty = db.Column(db.String(64)) + + # ... any other static information about tags + + def __init__(self, id): + self.id = id + class Tag(db.Model): + """Association between a Post and a dot-string tag identifier.""" + __tablename__ = 'tag' id = db.Column(db.Integer, primary_key=True) # Tagged post post_id = db.Column(db.Integer, db.ForeignKey('post.id'), index=True) post = db.relationship('Post', back_populates='tags', foreign_keys=post_id) - # Tag name - name = db.Column(db.String(64), index=True) + # Tag name. Note how we always load the information along the tag, but not + # the other way around. + tag_id = db.Column(db.String(64), db.ForeignKey('tag_information.id'), + index=True) + tag = db.relationship('TagInformation', back_populates='uses', + foreign_keys=tag_id, lazy='joined') def __init__(self, post, tag): self.post = post - self.name = tag + + if isinstance(tag, str): + tag = TagInformation.query.filter_by(id=tag).one() + self.tag = tag diff --git a/master.py b/master.py index d587555..9cf9252 100755 --- a/master.py +++ b/master.py @@ -5,6 +5,7 @@ from app.models.user import Member, Group, GroupPrivilege from app.models.priv import SpecialPrivilege from app.models.trophy import Trophy, Title from app.models.forum import Forum +from app.models.tag import TagInformation from app.utils import unicode_names import os import sys @@ -23,11 +24,13 @@ Listing commands: groups Show privilege groups forums Show forum tree trophies Show trophies + tags Show tags Install and update commands: update-groups Create or update groups from app/data/ update-forums Create or update the forum tree from app/data/ update-trophies Create or update trophies + update-tags Create or update tag information generate-trophy-icons Regenerate all trophy icons create-common-accounts Remove and recreate 'Planète Casio' and 'GLaDOS' add-group # Add to group # (presumably admins) @@ -56,6 +59,11 @@ def trophies(*args): for t in Trophy.query.all(): print(t) +def tags(*args): + tags = TagInformation.query.all() + for t in sorted(tags, key=lambda t: t.id): + print(f"{t.id}: {t.pretty}") + # # Install and update commands # @@ -160,7 +168,6 @@ def update_trophies(): existing = Trophy.query.all() # Get the list of what we want to obtain - tr = [] with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp: tr = yaml.safe_load(fp.read()) tr = { t["name"]: t for t in tr } @@ -207,6 +214,39 @@ def update_trophies(): db.session.commit() +def update_tags(): + existing = TagInformation.query.all() + + with open(os.path.join(app.root_path, "data", "tags.yaml")) as fp: + data = yaml.safe_load(fp.read()) + tags = { ctgy + "." + name: data[ctgy][name] + for ctgy in data for name in data[ctgy] } + + # Remove bad tags + for t in existing: + if t.id not in tags: + print(f"[tags] Deleted '{t.id}'") + db.session.delete(t) + db.session.commit() + + for name, info in tags.items(): + pretty = info.get("pretty", name) + t = TagInformation.query.filter_by(id=name).first() + + if t is not None: + changes = (t.pretty != pretty) + t.pretty = pretty + if changes: + print(f"[tags] Updated '{name}'") + else: + t = TagInformation(name) + t.pretty = pretty + print(f"[tags] Created '{name}'") + + db.session.add(t) + + db.session.commit() + def generate_trophy_icons(): tr = [] with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp: @@ -308,9 +348,11 @@ commands = { "groups": groups, "forums": forums, "trophies": trophies, + "tags": tags, "update-groups": update_groups, "update-forums": update_forums, "update-trophies": update_trophies, + "update-tags": update_tags, "generate-trophy-icons": generate_trophy_icons, "create-common-accounts": create_common_accounts, "add-group": add_group, diff --git a/migrations/versions/189bbc0e1543_add_tag_information.py b/migrations/versions/189bbc0e1543_add_tag_information.py new file mode 100644 index 0000000..17f99bb --- /dev/null +++ b/migrations/versions/189bbc0e1543_add_tag_information.py @@ -0,0 +1,42 @@ +"""add tag information + +Revision ID: 189bbc0e1543 +Revises: fa34c9f43c24 +Create Date: 2022-06-09 22:26:58.562710 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '189bbc0e1543' +down_revision = 'fa34c9f43c24' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tag_information', + sa.Column('id', sa.String(length=64), nullable=False), + sa.Column('pretty', sa.String(length=64), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('tag', sa.Column('tag_id', sa.String(length=64), nullable=True)) + op.drop_index('ix_tag_name', table_name='tag') + op.create_index(op.f('ix_tag_tag_id'), 'tag', ['tag_id'], unique=False) + op.create_foreign_key(None, 'tag', 'tag_information', ['tag_id'], ['id']) + op.drop_column('tag', 'name') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('tag', sa.Column('name', sa.VARCHAR(length=64), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'tag', type_='foreignkey') + op.drop_index(op.f('ix_tag_tag_id'), table_name='tag') + op.create_index('ix_tag_name', 'tag', ['name'], unique=False) + op.drop_column('tag', 'tag_id') + op.drop_table('tag_information') + # ### end Alembic commands ### From db0e42d28555b1408a7ffedbb54310b9bdb25d69 Mon Sep 17 00:00:00 2001 From: Lephe Date: Tue, 14 Jun 2022 23:16:19 +0100 Subject: [PATCH 23/92] programs: add tag input and display (#114) * Add a TagListField which automatically validates its input against the TagInformation database, and has a richer .selected_tags() method * Add a dynamic tag input widget, available through a macro (*import with context*), that supports both JS and non-JS input * Add a TagInformation.all_tags() function * Add colored tag display to all themes * Fix a bug causing programs to have no names * Add tags: games.action, games.narrative, courses.informatics [MASTER] Run the 'update-tags' command of master.py. --- app/data/tags.yaml | 6 +++ app/forms/programs.py | 3 +- app/models/program.py | 6 +-- app/models/tag.py | 13 +++++ app/processors/utilities.py | 5 +- app/routes/programs/submit.py | 5 +- app/static/css/form.css | 17 +++++- app/static/css/themes/FK_dark_theme.css | 20 +++++++ app/static/css/themes/Tituya_v43_theme.css | 19 +++++++ app/static/css/themes/default_theme.css | 19 +++++++ app/static/css/widgets.css | 11 ++++ app/static/less/form.less | 25 ++++++++- app/static/less/widgets.less | 14 +++++ app/static/scripts/tag_selector.js | 61 ++++++++++++++++++++++ app/templates/account/register.html | 2 +- app/templates/programs/index.html | 10 ++-- app/templates/programs/submit.html | 10 +--- app/templates/widgets/tag_selector.html | 57 ++++++++++++++++++++ app/templates/widgets/thread.html | 2 +- app/utils/converters.py | 4 +- app/utils/render.py | 1 + app/utils/tag_field.py | 24 +++++++++ 22 files changed, 308 insertions(+), 26 deletions(-) create mode 100644 app/static/scripts/tag_selector.js create mode 100644 app/templates/widgets/tag_selector.html create mode 100644 app/utils/tag_field.py diff --git a/app/data/tags.yaml b/app/data/tags.yaml index 8e09d88..88a3bea 100644 --- a/app/data/tags.yaml +++ b/app/data/tags.yaml @@ -52,10 +52,14 @@ lang: pretty: "Langage: autre" games: + action: + pretty: Action adventure: pretty: Aventure fighting: pretty: Combat + narrative: + pretty: Narratif other: pretty: "Jeu: autre" platform: @@ -98,5 +102,7 @@ courses: pretty: SI/Électronique economics: pretty: Économie + informatics: + pretty: Informatique other: pretty: "Cours: autre" diff --git a/app/forms/programs.py b/app/forms/programs.py index 1f462e7..ca4cf16 100644 --- a/app/forms/programs.py +++ b/app/forms/programs.py @@ -3,12 +3,13 @@ from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField from wtforms.validators import InputRequired, Length import app.utils.validators as vf from app.utils.antibot_field import AntibotField +from app.utils.tag_field import TagListField from app.forms.forum import CommentForm class ProgramCreationForm(CommentForm): name = StringField('Nom du programme', validators=[InputRequired(), Length(min=3, max=64)]) - tags = StringField('Liste de tags', description='Séparés par des virgules') + tags = TagListField('Liste de tags') submit = SubmitField('Soumettre le programme') diff --git a/app/models/program.py b/app/models/program.py index 23bf006..ba291ad 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -24,18 +24,18 @@ class Program(Post): # * tags (inherited from Post) # * attachements (available at thread.top_comment.attachments) - def __init__(self, author, title, thread): + def __init__(self, author, name, thread): """ Create a Program. Arguments: author -- post author (User, though only Members can post) - title -- program title (unicode string) + name -- program name (unicode string) thread -- discussion thread attached to the topic """ Post.__init__(self, author) - self.title = title + self.name = name self.thread = thread @staticmethod diff --git a/app/models/tag.py b/app/models/tag.py index a7ad50d..ed4f548 100644 --- a/app/models/tag.py +++ b/app/models/tag.py @@ -19,6 +19,19 @@ class TagInformation(db.Model): def __init__(self, id): self.id = id + def category(self): + return self.id.split(".", 1)[0] + + @staticmethod + def all_tags(): + all_tags = {} + for ti in TagInformation.query.all(): + ctgy = ti.category() + if ctgy not in all_tags: + all_tags[ctgy] = [] + all_tags[ctgy].append(ti) + return all_tags + class Tag(db.Model): """Association between a Post and a dot-string tag identifier.""" diff --git a/app/processors/utilities.py b/app/processors/utilities.py index 0b8edf2..18bbf1d 100644 --- a/app/processors/utilities.py +++ b/app/processors/utilities.py @@ -3,15 +3,16 @@ from flask import url_for from config import V5Config from slugify import slugify from app.utils.login_as import is_vandal +from app.models.tag import TagInformation @app.context_processor def utilities_processor(): """ Add some utilities to render context """ return dict( len=len, - # enumerate=enumerate, _url_for=lambda route, args, **other: url_for(route, **args, **other), V5Config=V5Config, slugify=slugify, - is_vandal=is_vandal + is_vandal=is_vandal, + db_all_tags=TagInformation.all_tags, ) diff --git a/app/routes/programs/submit.py b/app/routes/programs/submit.py index 0675a84..8bbdb7e 100644 --- a/app/routes/programs/submit.py +++ b/app/routes/programs/submit.py @@ -34,9 +34,8 @@ def program_submit(): db.session.commit() # Add tags - # TODO: Check tags against a predefined set - for tag in form.tags.data.split(","): - db.session.add(Tag(p, tag.strip())) + for tag in form.tags.selected_tags(): + db.session.add(Tag(p, tag)) db.session.commit() # Manage files diff --git a/app/static/css/form.css b/app/static/css/form.css index b780e4a..9465375 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -8,7 +8,7 @@ .form form label + .desc { margin: 0 0 4px 0; font-size: 80%; - opacity: .75; + opacity: .65; } .form form .avatar { width: 128px; @@ -98,6 +98,21 @@ .form input[type='email'].abfield { display: none; } +form .dynamic-tag-selector { + display: none; +} +form .dynamic-tag-selector input[type="text"] { + display: none; +} +form .dynamic-tag-selector .tag { + cursor: pointer; +} +form .dynamic-tag-selector .tags-selected { + margin: 0 0 4px 0; +} +form .dynamic-tag-selector .tags-selected .tag { + display: none; +} .form.filter { margin-bottom: 16px; } diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css index 2a8ae11..3d9bd13 100644 --- a/app/static/css/themes/FK_dark_theme.css +++ b/app/static/css/themes/FK_dark_theme.css @@ -148,3 +148,23 @@ div.editor-toolbar, div.CodeMirror { --border: rgba(255, 255, 255, 0.8); --selected: rgba(255, 0, 0, 1.0); } + +.tag { + --background: #22292c; + --color: white; +} +.tag.tag-calc { + --background: #917e1a; +} +.tag.tag-lang { + --background: #4a8033; +} +.tag.tag-games { + --background: #488695; +} +.tag.tag-tools { + --background: #70538a; +} +.tag.tag-courses { + --background: #884646; +} diff --git a/app/static/css/themes/Tituya_v43_theme.css b/app/static/css/themes/Tituya_v43_theme.css index f2cb187..571176c 100644 --- a/app/static/css/themes/Tituya_v43_theme.css +++ b/app/static/css/themes/Tituya_v43_theme.css @@ -172,3 +172,22 @@ div.pagination { font-size: 14px; margin: 13px; } + +.tag { + --background: #e0e0e0; +} +.tag.tag-calc { + --background: #f0ca81; +} +.tag.tag-lang { + --background: #aad796; +} +.tag.tag-games { + --background: #a7ccd5; +} +.tag.tag-tools { + --background: #c6aae1; +} +.tag.tag-courses { + --background: #f0a0a0; +} diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index b87e4f8..1168211 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -142,3 +142,22 @@ div.editor-toolbar, div.CodeMirror { table.codehilitetable td.linenos { color: #808080; } + +.tag { + --background: #e0e0e0; +} +.tag.tag-calc { + --background: #f0ca81; +} +.tag.tag-lang { + --background: #aad796; +} +.tag.tag-games { + --background: #a7ccd5; +} +.tag.tag-tools { + --background: #c6aae1; +} +.tag.tag-courses { + --background: #f0a0a0; +} diff --git a/app/static/css/widgets.css b/app/static/css/widgets.css index a7bb6eb..fcf977d 100644 --- a/app/static/css/widgets.css +++ b/app/static/css/widgets.css @@ -207,4 +207,15 @@ hr.signature { } .gallery-spot * { cursor: pointer; +} +.tag { + display: inline-block; + background: var(--background); + color: var(--color); + padding: 4px 12px; + margin: 4px 0; + border-radius: 8px; + border-radius: calc(4.5em); + user-select: none; + cursor: default; } \ No newline at end of file diff --git a/app/static/less/form.less b/app/static/less/form.less index 36e37b4..046b7dc 100644 --- a/app/static/less/form.less +++ b/app/static/less/form.less @@ -13,7 +13,7 @@ & + .desc { margin: 0 0 4px 0; font-size: 80%; - opacity: .75; + opacity: .65; } } @@ -115,6 +115,29 @@ } +/* Interactive tag selector */ + +form .dynamic-tag-selector { + display: none; + + input[type="text"] { + display: none; + } + + .tag { + cursor: pointer; + } + + .tags-selected { + margin: 0 0 4px 0; + + .tag { + display: none; + } + } +} + + /* Interactive filter forms */ .form.filter { diff --git a/app/static/less/widgets.less b/app/static/less/widgets.less index c86f2e0..41d602e 100644 --- a/app/static/less/widgets.less +++ b/app/static/less/widgets.less @@ -239,3 +239,17 @@ hr.signature { cursor: pointer; } } + +/* Tags */ + +.tag { + display: inline-block; + background: var(--background); + color: var(--color); + padding: 4px 12px; + margin: 4px 0; + border-radius: 8px; + border-radius: calc(0.5em + 4px); + user-select: none; + cursor: default; +} diff --git a/app/static/scripts/tag_selector.js b/app/static/scripts/tag_selector.js new file mode 100644 index 0000000..9836205 --- /dev/null +++ b/app/static/scripts/tag_selector.js @@ -0,0 +1,61 @@ +function tag_selector_find(node) { + while(node != document.body) { + if(node.classList.contains("dynamic-tag-selector")) + return node; + node = node.parentNode; + } + return undefined; +} + +function tag_selector_get(ts) { + return ts.querySelector("input").value + .split(",") + .map(str => str.trim()) + .filter(str => str !== ""); +} + +function tag_selector_set(ts, values) { + ts.querySelector("input").value = values.join(", "); + tag_selector_update(ts); +} + +function tag_selector_update(ts) { + if(ts === undefined) return; + const input_names = tag_selector_get(ts); + + /* Update visibility of selected tags */ + ts.querySelectorAll(".tags-selected .tag[data-name]").forEach(tag => { + const visible = input_names.includes(tag.dataset.name); + tag.style.display = visible ? "inline-block" : "none"; + }); + + /* Update visibility of pool tags */ + ts.querySelectorAll(".tags-pool .tag[data-name]").forEach(tag => { + const visible = !input_names.includes(tag.dataset.name); + tag.style.display = visible ? "inline-block" : "none"; + }); +} + +function tag_selector_add(ts, id) { + if(ts === undefined) return; + + let tags = tag_selector_get(ts); + if(!tags.includes(id)) + tags.push(id); + + tag_selector_set(ts, tags); +} + +function tag_selector_remove(ts, id) { + if(ts === undefined) return; + + let tags = tag_selector_get(ts); + tags = tags.filter(str => str !== id); + + tag_selector_set(ts, tags); +} + +document.querySelectorAll(".dynamic-tag-selector").forEach(ts => { + ts.style.display = "block"; + tag_selector_update(ts); +}); diff --git a/app/templates/account/register.html b/app/templates/account/register.html index 6efe74f..272318b 100644 --- a/app/templates/account/register.html +++ b/app/templates/account/register.html @@ -46,7 +46,7 @@
      {{ form.newsletter.label }} {{ form.newsletter() }} -
      {{ form.newsletter.description }}
      +
      {{ form.newsletter.description }}
      {% for error in form.newsletter.errors %} {{ error }} {% endfor %} diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html index 17ba396..26fdc86 100644 --- a/app/templates/programs/index.html +++ b/app/templates/programs/index.html @@ -20,10 +20,14 @@ IDNomAuteurPublié leTags {% for p in programs %} {{ p.id }} - {{ p.name }} + {{ p.name }} {{ p.author.name }} - {{ p.date_created | dyndate }} - {% for tag in p.tags %}{{ tag.name }} {% endfor %} + {{ p.date_created | dyndate }} + + {%- for tag in p.tags %} + {{ tag.tag.pretty }} + {% endfor -%} + {% endfor %}
    diff --git a/app/templates/programs/submit.html b/app/templates/programs/submit.html index 0ac58f7..27d2bee 100644 --- a/app/templates/programs/submit.html +++ b/app/templates/programs/submit.html @@ -1,5 +1,6 @@ {% extends "base/base.html" %} {% import "widgets/editor.html" as widget_editor %} +{% import "widgets/tag_selector.html" as widget_tag_selector with context %} {% block title %}

    Programmes de Planète Casio

    @@ -22,14 +23,7 @@ {% endfor %}
    -
    - {{ form.tags.label }} -
    {{ form.tags.description }}
    - {{ form.tags() }} - {% for error in form.tags.errors %} - {{ error }} - {% endfor %} -
    + {{ widget_tag_selector.tag_selector(form.tags) }} {{ widget_editor.text_editor(form.message) }} diff --git a/app/templates/widgets/tag_selector.html b/app/templates/widgets/tag_selector.html new file mode 100644 index 0000000..8813e20 --- /dev/null +++ b/app/templates/widgets/tag_selector.html @@ -0,0 +1,57 @@ +{# Import this module with context: + {% import "widgets/tag_selector.html" as widget_tag_selector with context %} + This is necessary because it uses global names from context processors. #} + +{% macro tag_selector(field) %} + {% set all_tags = db_all_tags() %} + + {# When Javascript is disabled, we use the text field directly #} +
    + +
    + + {# When Javascript is enabled, use a dynamic system where the user can add + and remove tags with the mouse #} +
    +
    {{ field.label }}: + + {% for ctgy, tags in all_tags.items() %} + {% for tag in tags %} + {{ tag.pretty }} + {% endfor %} + {% endfor %} + +
    + {% if field.description %} +
    {{ field.description }}
    + {% endif %} + {{ field(oninput="tag_selector_update(tag_selector_find(this))") }} + {% for error in field.errors %} + {{ error }} + {% endfor %} + +
    Tags disponibles :
    + {% for ctgy, tags in all_tags.items() %} +
    + {% for tag in tags %} + {{ tag.pretty }} + {% endfor %} +
    + {% endfor %} +
    +{% endmacro %} diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index a1e87f7..67cd270 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -19,7 +19,7 @@ {% set suffix = " le programme" %} {% endif %} - {% if can_edit or can_delete or can_punish or can_topcomm %} + {% if can_edit or can_move or can_delete or can_punish or can_topcomm %}
    diff --git a/app/utils/converters.py b/app/utils/converters.py index 6936a61..a6fe429 100644 --- a/app/utils/converters.py +++ b/app/utils/converters.py @@ -77,11 +77,11 @@ class PageConverter(BaseConverter): class TopicPageConverter(PageConverter): object = Topic - get_title = lambda self, t: t.title + get_title = lambda self, t: t.title or "unnamed-topic-error" class ProgramPageConverter(PageConverter): object = Program - get_title = lambda self, p: p.name + get_title = lambda self, p: p.name or "unnamed-program-error" # Export only the converter classes __all__ = ["ForumConverter", "TopicPageConverter", "ProgramPageConverter"] diff --git a/app/utils/render.py b/app/utils/render.py index 8ecf8e6..a7cd320 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -27,6 +27,7 @@ def render(*args, styles=[], scripts=[], **kwargs): 'scripts/simplemde.min.js', 'scripts/gallery.js', 'scripts/filter.js', + 'scripts/tag_selector.js', ] # Apply theme from user settings diff --git a/app/utils/tag_field.py b/app/utils/tag_field.py new file mode 100644 index 0000000..25117de --- /dev/null +++ b/app/utils/tag_field.py @@ -0,0 +1,24 @@ +from wtforms.fields.simple import StringField +from wtforms.validators import ValidationError +from app.models.tag import TagInformation + +def tag_validator(form, field): + all_tags = TagInformation.all_tags() + for name in field.selected_tags(): + if all(ti.id != name for ctgy in all_tags for ti in all_tags[ctgy]): + raise ValidationError(f"Tag inconnu: {name}") + return True + +class TagListField(StringField): + + def __init__(self, title, *args, **kwargs): + validators = kwargs.get("validators", []) + [tag_validator] + super().__init__( + title, + *args, + **kwargs, + validators=[tag_validator]) + + def selected_tags(self): + raw = map(lambda x: x.strip(), self.data.split(",")) + return [name for name in raw if name != ""] From 8ff21c615dbb8d3d9f4d97f31094cd4f587cb6a8 Mon Sep 17 00:00:00 2001 From: Lephe Date: Wed, 15 Jun 2022 11:23:58 +0100 Subject: [PATCH 24/92] program: add infrastructure for the progrank job (#114) * Add an automatic job every day at 4 AM to recompute the progrank of every program. Currently everyone gets progrank 0. [MIGRATION] This commit contains a new version of the schema. [SETUP] * Install flask-crontab (with pip) * Run `flask crontab add` to register the jobs --- REQUIREMENTS.md | 7 ++++- app/__init__.py | 5 ++++ app/jobs/__init__.py | 1 + app/jobs/update_progrank.py | 11 +++++++ app/models/program.py | 4 +++ app/templates/programs/index.html | 3 +- app/utils/filters/date.py | 3 ++ ..._program_add_progrank_score_and_update_.py | 30 +++++++++++++++++++ 8 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 app/jobs/__init__.py create mode 100644 app/jobs/update_progrank.py create mode 100644 migrations/versions/daa5d5913ef8_program_add_progrank_score_and_update_.py diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 6289a1f..6d4cec8 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -25,7 +25,12 @@ python-pyyaml python-slugify ``` +Non disponibles sur l'AUR, mais disponibles sur pip : +``` +flask-crontab (0.1.2) +``` + Optionnel: ``` -python-flask-debugtoolbar +python-flask-debugtoolbar (out-of-date sur l'AUR : bien installer la 0.13) ``` diff --git a/app/__init__.py b/app/__init__.py index e6b64da..2f960d7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ from flask_migrate import Migrate from flask_login import LoginManager from flask_mail import Mail from flask_wtf.csrf import CSRFProtect +from flask_crontab import Crontab from config import FlaskApplicationSettings, V5Config app = Flask(__name__) @@ -17,6 +18,7 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) mail = Mail(app) csrf = CSRFProtect(app) +crontab = Crontab(app) login = LoginManager(app) login.login_view = 'login' @@ -38,6 +40,9 @@ from app.utils import filters # Register processors from app import processors +# Register scheduled jobs +from app import jobs + # Enable flask-debug-toolbar if requested if V5Config.ENABLE_FLASK_DEBUG_TOOLBAR: from flask_debugtoolbar import DebugToolbarExtension diff --git a/app/jobs/__init__.py b/app/jobs/__init__.py new file mode 100644 index 0000000..9a77601 --- /dev/null +++ b/app/jobs/__init__.py @@ -0,0 +1 @@ +from app.jobs.update_progrank import update_progrank diff --git a/app/jobs/update_progrank.py b/app/jobs/update_progrank.py new file mode 100644 index 0000000..e475599 --- /dev/null +++ b/app/jobs/update_progrank.py @@ -0,0 +1,11 @@ +from app import db, crontab +from app.models.program import Program +from datetime import datetime + +@crontab.job(minute="0", hour="4") +def update_progrank(): + for p in Program.query.all(): + p.progrank = 0 + p.progrank_date = datetime.now() + db.session.merge(p) + db.session.commit() diff --git a/app/models/program.py b/app/models/program.py index ba291ad..46aef41 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -20,6 +20,10 @@ class Program(Post): thread = db.relationship('Thread', foreign_keys=thread_id, back_populates='owner_program') + # Progrank, and last date of progrank update + progrank = db.Column(db.Integer) + progrank_date = db.Column(db.DateTime) + # Implicit attributes: # * tags (inherited from Post) # * attachements (available at thread.top_comment.attachments) diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html index 26fdc86..29ec171 100644 --- a/app/templates/programs/index.html +++ b/app/templates/programs/index.html @@ -17,12 +17,13 @@

    Tous les programmes

    - + {% for p in programs %} +
    IDNomAuteurPublié leTags
    IDNomAuteurPublié leProgrankTags
    {{ p.id }} {{ p.name }} {{ p.author.name }} {{ p.date_created | dyndate }}{{ p.progrank }} {%- for tag in p.tags %} {{ tag.tag.pretty }} diff --git a/app/utils/filters/date.py b/app/utils/filters/date.py index a8155cc..93a2566 100644 --- a/app/utils/filters/date.py +++ b/app/utils/filters/date.py @@ -7,6 +7,9 @@ def filter_date(date, format="%Y-%m-%d à %H:%M"): Print a date in a human-readable format. """ + if date is None: + return "None" + if format == "dynamic": d = "1er" if date.day == 1 else int(date.day) m = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", diff --git a/migrations/versions/daa5d5913ef8_program_add_progrank_score_and_update_.py b/migrations/versions/daa5d5913ef8_program_add_progrank_score_and_update_.py new file mode 100644 index 0000000..7077a9b --- /dev/null +++ b/migrations/versions/daa5d5913ef8_program_add_progrank_score_and_update_.py @@ -0,0 +1,30 @@ +"""program: add progrank score and update date fields + +Revision ID: daa5d5913ef8 +Revises: 189bbc0e1543 +Create Date: 2022-06-15 11:14:30.745287 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'daa5d5913ef8' +down_revision = '189bbc0e1543' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('program', sa.Column('progrank', sa.Integer(), nullable=True)) + op.add_column('program', sa.Column('progrank_date', sa.DateTime(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('program', 'progrank_date') + op.drop_column('program', 'progrank') + # ### end Alembic commands ### From 417fc05d293f0d0209826ee77d45104287b81596 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 16 Jun 2022 17:00:59 +0100 Subject: [PATCH 25/92] program: add metadata and a basic model for events (#114) This commit adds most of the optional metadata for programs. The event related to the program's publication is an actual relationship to an Event model. The idea is to expand on that model in the future to include: - A link to the event's main topic - List of programs published in the event - Possibly, a list of all related topics (announcement, start, results, etc) all sharing a common 1-line header so they are linked together - This would be used for event-related trophies - And possibly for an event calendar --- app/models/__init__.py | 1 + app/models/event.py | 11 +++ app/models/program.py | 17 ++++- app/templates/programs/index.html | 11 ++- app/templates/programs/program.html | 72 ++----------------- ...59_add_program_metadata_and_a_base_for_.py | 48 +++++++++++++ 6 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 app/models/event.py create mode 100644 migrations/versions/ba47de949e59_add_program_metadata_and_a_base_for_.py diff --git a/app/models/__init__.py b/app/models/__init__.py index 58c31dd..1337ad3 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -5,3 +5,4 @@ from app.models.topic import Topic from app.models.notification import Notification from app.models.program import Program from app.models.tag import Tag +from app.models.event import Event diff --git a/app/models/event.py b/app/models/event.py new file mode 100644 index 0000000..ddfb17b --- /dev/null +++ b/app/models/event.py @@ -0,0 +1,11 @@ +from app import db + +class Event(db.Model): + __tablename__ = 'event' + id = db.Column(db.Integer, primary_key=True) + + # Pretty event name, eg. "CPC #28" + name = db.Column(db.Unicode(128)) + + # Main topic, used to automatically insert links + main_topic = db.Column(db.Integer, db.ForeignKey('topic.id')) diff --git a/app/models/program.py b/app/models/program.py index 46aef41..02a2dfd 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -10,10 +10,21 @@ class Program(Post): # Program name name = db.Column(db.Unicode(128)) + # Author, when different from the poster + real_author = db.Column(db.Unicode(128)) + # Version + version = db.Column(db.Unicode(64)) + # Approximate size as indicated by poster + size = db.Column(db.Unicode(64)) + # License identifier + license = db.Column(db.String(32)) - # TODO: Category (games/utilities/lessons) - # TODO: Compatible calculator models - # TODO: Number of views, statistics, etc + # Label de qualité + label = db.Column(db.Boolean, nullable=False, server_default="FALSE") + # Event for which the program was posted + event = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=True) + + # TODO: Number of views and downloads # Thread with the program description (top comment) and comments thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html index 29ec171..7f888f6 100644 --- a/app/templates/programs/index.html +++ b/app/templates/programs/index.html @@ -17,12 +17,19 @@

    Tous les programmes

    - + {% for p in programs %} - + + \n"+e+"\n"},o.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"\n"},o.prototype.strong=function(e){return""+e+""},o.prototype.em=function(e){return""+e+""},o.prototype.codespan=function(e){return""+e+""},o.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},o.prototype.del=function(e){return""+e+""},o.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(s(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(i){return""}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:"))return""}var o='"},o.prototype.image=function(e,t,n){var r=''+n+'":">"},o.prototype.text=function(e){return e},a.parse=function(e,t,n){var r=new a(t,n);return r.parse(e)},a.prototype.parse=function(e){this.inline=new i(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},a.prototype.next=function(){return this.token=this.tokens.pop()},a.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},a.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},a.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i,o="",a="";for(n="",e=0;ea;a++)for(var s=this.compoundRules[a],c=0,u=s.length;u>c;c++)this.compoundRuleCodes[s[c]]=[];"ONLYINCOMPOUND"in this.flags&&(this.compoundRuleCodes[this.flags.ONLYINCOMPOUND]=[]),this.dictionaryTable=this._parseDIC(n);for(var a in this.compoundRuleCodes)0==this.compoundRuleCodes[a].length&&delete this.compoundRuleCodes[a];for(var a=0,l=this.compoundRules.length;l>a;a++){for(var f=this.compoundRules[a],h="",c=0,u=f.length;u>c;c++){var d=f[c];h+=d in this.compoundRuleCodes?"("+this.compoundRuleCodes[d].join("|")+")":d}this.compoundRules[a]=new RegExp(h,"i")}}return this};i.prototype={load:function(e){for(var t in e)this[t]=e[t];return this},_readFile:function(t,r){if(r||(r="utf8"),"undefined"!=typeof XMLHttpRequest){var i=new XMLHttpRequest;return i.open("GET",t,!1),i.overrideMimeType&&i.overrideMimeType("text/plain; charset="+r),i.send(null),i.responseText}if("undefined"!=typeof e){var o=e("fs");try{if(o.existsSync(t)){var a=o.statSync(t),l=o.openSync(t,"r"),s=new n(a.size);return o.readSync(l,s,0,s.length,null),s.toString(r,0,s.length)}console.log("Path "+t+" does not exist.")}catch(c){return console.log(c),""}}},_parseAFF:function(e){var t={};e=this._removeAffixComments(e);for(var n=e.split("\n"),r=0,i=n.length;i>r;r++){var o=n[r],a=o.split(/\s+/),l=a[0];if("PFX"==l||"SFX"==l){for(var s=a[1],c=a[2],u=parseInt(a[3],10),f=[],h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/),m=p[2],g=p[3].split("/"),v=g[0];"0"===v&&(v="");var y=this.parseRuleCodes(g[1]),x=p[4],b={};b.add=v,y.length>0&&(b.continuationClasses=y),"."!==x&&("SFX"===l?b.match=new RegExp(x+"$"):b.match=new RegExp("^"+x)),"0"!=m&&("SFX"===l?b.remove=new RegExp(m+"$"):b.remove=m),f.push(b)}t[s]={type:l,combineable:"Y"==c,entries:f},r+=u}else if("COMPOUNDRULE"===l){for(var u=parseInt(a[1],10),h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/);this.compoundRules.push(p[1])}r+=u}else if("REP"===l){var p=o.split(/\s+/);3===p.length&&this.replacementTable.push([p[1],p[2]])}else this.flags[l]=a[1]}return t},_removeAffixComments:function(e){return e=e.replace(/#.*$/gm,""),e=e.replace(/^\s\s*/m,"").replace(/\s\s*$/m,""),e=e.replace(/\n{2,}/g,"\n"),e=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")},_parseDIC:function(e){function t(e,t){e in r&&"object"==typeof r[e]||(r[e]=[]),r[e].push(t)}e=this._removeDicComments(e);for(var n=e.split("\n"),r={},i=1,o=n.length;o>i;i++){var a=n[i],l=a.split("/",2),s=l[0];if(l.length>1){var c=this.parseRuleCodes(l[1]);"NEEDAFFIX"in this.flags&&-1!=c.indexOf(this.flags.NEEDAFFIX)||t(s,c);for(var u=0,f=c.length;f>u;u++){var h=c[u],d=this.rules[h];if(d)for(var p=this._applyRule(s,d),m=0,g=p.length;g>m;m++){var v=p[m];if(t(v,[]),d.combineable)for(var y=u+1;f>y;y++){var x=c[y],b=this.rules[x];if(b&&b.combineable&&d.type!=b.type)for(var w=this._applyRule(v,b),k=0,S=w.length;S>k;k++){var C=w[k];t(C,[])}}}h in this.compoundRuleCodes&&this.compoundRuleCodes[h].push(s)}}else t(s.trim(),[])}return r},_removeDicComments:function(e){return e=e.replace(/^\t.*$/gm,"")},parseRuleCodes:function(e){if(!e)return[];if(!("FLAG"in this.flags))return e.split("");if("long"===this.flags.FLAG){for(var t=[],n=0,r=e.length;r>n;n+=2)t.push(e.substr(n,2));return t}return"num"===this.flags.FLAG?textCode.split(","):void 0},_applyRule:function(e,t){for(var n=t.entries,r=[],i=0,o=n.length;o>i;i++){var a=n[i];if(!a.match||e.match(a.match)){var l=e;if(a.remove&&(l=l.replace(a.remove,"")),"SFX"===t.type?l+=a.add:l=a.add+l,r.push(l),"continuationClasses"in a)for(var s=0,c=a.continuationClasses.length;c>s;s++){var u=this.rules[a.continuationClasses[s]];u&&(r=r.concat(this._applyRule(l,u)))}}}return r},check:function(e){var t=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"");if(this.checkExact(t))return!0;if(t.toUpperCase()===t){var n=t[0]+t.substring(1).toLowerCase();if(this.hasFlag(n,"KEEPCASE"))return!1;if(this.checkExact(n))return!0}var r=t.toLowerCase();if(r!==t){if(this.hasFlag(r,"KEEPCASE"))return!1;if(this.checkExact(r))return!0}return!1},checkExact:function(e){var t=this.dictionaryTable[e];if("undefined"==typeof t){if("COMPOUNDMIN"in this.flags&&e.length>=this.flags.COMPOUNDMIN)for(var n=0,r=this.compoundRules.length;r>n;n++)if(e.match(this.compoundRules[n]))return!0;return!1}if("object"==typeof t){for(var n=0,r=t.length;r>n;n++)if(!this.hasFlag(e,"ONLYINCOMPOUND",t[n]))return!0;return!1}},hasFlag:function(e,t,n){if(t in this.flags){if("undefined"==typeof n)var n=Array.prototype.concat.apply([],this.dictionaryTable[e]);if(n&&-1!==n.indexOf(this.flags[t]))return!0}return!1},alphabet:"",suggest:function(e,t){function n(e){for(var t=[],n=0,r=e.length;r>n;n++){for(var i=e[n],o=[],a=0,l=i.length+1;l>a;a++)o.push([i.substring(0,a),i.substring(a,i.length)]);for(var s=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1]&&s.push(u[0]+u[1].substring(1))}for(var f=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1].length>1&&f.push(u[0]+u[1][1]+u[1][0]+u[1].substring(2))}for(var h=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1].substring(1))}for(var m=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1])}t=t.concat(s),t=t.concat(f),t=t.concat(h),t=t.concat(m)}return t}function r(e){for(var t=[],n=0;nu;u++)l[u]in s?s[l[u]]+=1:s[l[u]]=1;var h=[];for(var u in s)h.push([u,s[u]]);h.sort(i).reverse();for(var d=[],u=0,f=Math.min(t,h.length);f>u;u++)c.hasFlag(h[u][0],"NOSUGGEST")||d.push(h[u][0]);return d}if(t||(t=5),this.check(e))return[];for(var o=0,a=this.replacementTable.length;a>o;o++){var l=this.replacementTable[o];if(-1!==e.indexOf(l[0])){var s=e.replace(l[0],l[1]);if(this.check(s))return[s]}}var c=this;return c.alphabet="abcdefghijklmnopqrstuvwxyz",i(e)}},"undefined"!=typeof t&&(t.exports=i)}).call(this,e("buffer").Buffer,"/node_modules/typo-js")},{buffer:3,fs:2}],19:[function(e,t,n){var r=e("codemirror");r.commands.tabAndIndentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentMore");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}},r.commands.shiftTabAndUnindentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentLess");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}}},{codemirror:10}],20:[function(e,t,n){"use strict";function r(e){return e=U?e.replace("Ctrl","Cmd"):e.replace("Cmd","Ctrl")}function i(e,t,n){e=e||{};var r=document.createElement("a");return t=void 0==t?!0:t,e.title&&t&&(r.title=a(e.title,e.action,n),U&&(r.title=r.title.replace("Ctrl","⌘"),r.title=r.title.replace("Alt","⌥"))),r.tabIndex=-1,r.className=e.className,r}function o(){var e=document.createElement("i");return e.className="separator",e.innerHTML="|",e}function a(e,t,n){var i,o=e;return t&&(i=Y(t),n[i]&&(o+=" ("+r(n[i])+")")),o}function l(e,t){t=t||e.getCursor("start");var n=e.getTokenAt(t);if(!n.type)return{};for(var r,i,o=n.type.split(" "),a={},l=0;l=0&&(d=c.getLineHandle(o),!t(d));o--);var v,y,x,b,w=c.getTokenAt({line:o,ch:1}),k=n(w).fencedChars;t(c.getLineHandle(u.line))?(v="",y=u.line):t(c.getLineHandle(u.line-1))?(v="",y=u.line-1):(v=k+"\n",y=u.line),t(c.getLineHandle(f.line))?(x="",b=f.line,0===f.ch&&(b+=1)):0!==f.ch&&t(c.getLineHandle(f.line+1))?(x="",b=f.line+1):(x=k+"\n",b=f.line+1),0===f.ch&&(b-=1),c.operation(function(){c.replaceRange(x,{line:b,ch:0},{line:b+(x?0:1),ch:0}),c.replaceRange(v,{line:y,ch:0},{line:y+(v?0:1),ch:0})}),c.setSelection({line:y+(v?1:0),ch:0},{line:b+(v?1:-1),ch:0}),c.focus()}else{var S=u.line;if(t(c.getLineHandle(u.line))&&("fenced"===r(c,u.line+1)?(o=u.line,S=u.line+1):(a=u.line,S=u.line-1)),void 0===o)for(o=S;o>=0&&(d=c.getLineHandle(o),!t(d));o--);if(void 0===a)for(l=c.lineCount(),a=S;l>a&&(d=c.getLineHandle(a),!t(d));a++);c.operation(function(){c.replaceRange("",{line:o,ch:0},{line:o+1,ch:0}),c.replaceRange("",{line:a-1,ch:0},{line:a,ch:0})}),c.focus()}else if("indented"===p){if(u.line!==f.line||u.ch!==f.ch)o=u.line,a=f.line,0===f.ch&&a--;else{for(o=u.line;o>=0;o--)if(d=c.getLineHandle(o),!d.text.match(/^\s*$/)&&"indented"!==r(c,o,d)){o+=1;break}for(l=c.lineCount(),a=u.line;l>a;a++)if(d=c.getLineHandle(a),!d.text.match(/^\s*$/)&&"indented"!==r(c,a,d)){a-=1;break}}var C=c.getLineHandle(a+1),L=C&&c.getTokenAt({line:a+1,ch:C.text.length-1}),T=L&&n(L).indentedCode;T&&c.replaceRange("\n",{line:a+1,ch:0});for(var M=o;a>=M;M++)c.indentLine(M,"subtract");c.focus()}else{var N=u.line===f.line&&u.ch===f.ch&&0===u.ch,A=u.line!==f.line;N||A?i(c,u,f,s):E(c,!1,["`","`"])}}function d(e){var t=e.codemirror;I(t,"quote")}function p(e){var t=e.codemirror;O(t,"smaller")}function m(e){var t=e.codemirror;O(t,"bigger")}function g(e){var t=e.codemirror;O(t,void 0,1)}function v(e){var t=e.codemirror;O(t,void 0,2)}function y(e){var t=e.codemirror;O(t,void 0,3)}function x(e){var t=e.codemirror;I(t,"unordered-list")}function b(e){var t=e.codemirror;I(t,"ordered-list")}function w(e){var t=e.codemirror;R(t)}function k(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.link),!i)?!1:void E(t,n.link,r.insertTexts.link,i)}function S(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.image),!i)?!1:void E(t,n.image,r.insertTexts.image,i)}function C(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.table,r.insertTexts.table)}function L(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.image,r.insertTexts.horizontalRule)}function T(e){var t=e.codemirror;t.undo(),t.focus()}function M(e){var t=e.codemirror;t.redo(),t.focus()}function N(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.nextSibling,i=e.toolbarElements["side-by-side"],o=!1;/editor-preview-active-side/.test(r.className)?(r.className=r.className.replace(/\s*editor-preview-active-side\s*/g,""),i.className=i.className.replace(/\s*active\s*/g,""),n.className=n.className.replace(/\s*CodeMirror-sided\s*/g," ")):(setTimeout(function(){t.getOption("fullScreen")||s(e),r.className+=" editor-preview-active-side"},1),i.className+=" active",n.className+=" CodeMirror-sided",o=!0);var a=n.lastChild;if(/editor-preview-active/.test(a.className)){a.className=a.className.replace(/\s*editor-preview-active\s*/g,"");var l=e.toolbarElements.preview,c=n.previousSibling;l.className=l.className.replace(/\s*active\s*/g,""),c.className=c.className.replace(/\s*disabled-for-preview*/g,"")}var u=function(){r.innerHTML=e.options.previewRender(e.value(),r)};t.sideBySideRenderingFunction||(t.sideBySideRenderingFunction=u),o?(r.innerHTML=e.options.previewRender(e.value(),r),t.on("update",t.sideBySideRenderingFunction)):t.off("update",t.sideBySideRenderingFunction),t.refresh()}function A(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.previousSibling,i=e.options.toolbar?e.toolbarElements.preview:!1,o=n.lastChild;o&&/editor-preview/.test(o.className)||(o=document.createElement("div"),o.className="editor-preview",n.appendChild(o)),/editor-preview-active/.test(o.className)?(o.className=o.className.replace(/\s*editor-preview-active\s*/g,""),i&&(i.className=i.className.replace(/\s*active\s*/g,""),r.className=r.className.replace(/\s*disabled-for-preview*/g,""))):(setTimeout(function(){o.className+=" editor-preview-active"},1),i&&(i.className+=" active",r.className+=" disabled-for-preview")),o.innerHTML=e.options.previewRender(e.value(),o);var a=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(a.className)&&N(e)}function E(e,t,n,r){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){var i,o=n[0],a=n[1],l=e.getCursor("start"),s=e.getCursor("end");r&&(a=a.replace("#url#",r)),t?(i=e.getLine(l.line),o=i.slice(0,l.ch),a=i.slice(l.ch),e.replaceRange(o+a,{line:l.line,ch:0})):(i=e.getSelection(),e.replaceSelection(o+i+a),l.ch+=o.length,l!==s&&(s.ch+=o.length)),e.setSelection(l,s),e.focus()}}function O(e,t,n){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var r=e.getCursor("start"),i=e.getCursor("end"),o=r.line;o<=i.line;o++)!function(r){var i=e.getLine(r),o=i.search(/[^#]/);i=void 0!==t?0>=o?"bigger"==t?"###### "+i:"# "+i:6==o&&"smaller"==t?i.substr(7):1==o&&"bigger"==t?i.substr(2):"bigger"==t?i.substr(1):"#"+i:1==n?0>=o?"# "+i:o==n?i.substr(o+1):"# "+i.substr(o+1):2==n?0>=o?"## "+i:o==n?i.substr(o+1):"## "+i.substr(o+1):0>=o?"### "+i:o==n?i.substr(o+1):"### "+i.substr(o+1),e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(o);e.focus()}}function I(e,t){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var n=l(e),r=e.getCursor("start"),i=e.getCursor("end"),o={quote:/^(\s*)\>\s+/,"unordered-list":/^(\s*)(\*|\-|\+)\s+/,"ordered-list":/^(\s*)\d+\.\s+/},a={quote:"> ","unordered-list":"* ","ordered-list":"1. "},s=r.line;s<=i.line;s++)!function(r){var i=e.getLine(r);i=n[t]?i.replace(o[t],"$1"):a[t]+i,e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(s);e.focus()}}function P(e,t,n,r){if(!/editor-preview-active/.test(e.codemirror.getWrapperElement().lastChild.className)){r="undefined"==typeof r?n:r;var i,o=e.codemirror,a=l(o),s=n,c=r,u=o.getCursor("start"),f=o.getCursor("end");a[t]?(i=o.getLine(u.line),s=i.slice(0,u.ch),c=i.slice(u.ch),"bold"==t?(s=s.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),c=c.replace(/(\*\*|__)/,"")):"italic"==t?(s=s.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),c=c.replace(/(\*|_)/,"")):"strikethrough"==t&&(s=s.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),c=c.replace(/(\*\*|~~)/,"")),o.replaceRange(s+c,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),"bold"==t||"strikethrough"==t?(u.ch-=2,u!==f&&(f.ch-=2)):"italic"==t&&(u.ch-=1,u!==f&&(f.ch-=1))):(i=o.getSelection(),"bold"==t?(i=i.split("**").join(""),i=i.split("__").join("")):"italic"==t?(i=i.split("*").join(""),i=i.split("_").join("")):"strikethrough"==t&&(i=i.split("~~").join("")),o.replaceSelection(s+i+c),u.ch+=n.length,f.ch=u.ch+i.length),o.setSelection(u,f),o.focus()}}function R(e){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className))for(var t,n=e.getCursor("start"),r=e.getCursor("end"),i=n.line;i<=r.line;i++)t=e.getLine(i),t=t.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/,""),e.replaceRange(t,{line:i,ch:0},{line:i,ch:99999999999999})}function D(e,t){for(var n in t)t.hasOwnProperty(n)&&(t[n]instanceof Array?e[n]=t[n].concat(e[n]instanceof Array?e[n]:[]):null!==t[n]&&"object"==typeof t[n]&&t[n].constructor===Object?e[n]=D(e[n]||{},t[n]):e[n]=t[n]);return e}function H(e){for(var t=1;t=19968?n[i].length:1;return r}function B(e){e=e||{},e.parent=this;var t=!0;if(e.autoDownloadFontAwesome===!1&&(t=!1),e.autoDownloadFontAwesome!==!0)for(var n=document.styleSheets,r=0;r-1&&(t=!1);if(t){var i=document.createElement("link");i.rel="stylesheet",i.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(i)}if(e.element)this.element=e.element;else if(null===e.element)return void console.log("SimpleMDE: Error. No element was found.");if(void 0===e.toolbar){e.toolbar=[];for(var o in K)K.hasOwnProperty(o)&&(-1!=o.indexOf("separator-")&&e.toolbar.push("|"),(K[o]["default"]===!0||e.showIcons&&e.showIcons.constructor===Array&&-1!=e.showIcons.indexOf(o))&&e.toolbar.push(o))}e.hasOwnProperty("status")||(e.status=["autosave","lines","words","cursor"]),e.previewRender||(e.previewRender=function(e){return this.parent.markdown(e)}),e.parsingConfig=H({highlightFormatting:!0},e.parsingConfig||{}),e.insertTexts=H({},X,e.insertTexts||{}),e.promptTexts=Z,e.blockStyles=H({},J,e.blockStyles||{}),e.shortcuts=H({},G,e.shortcuts||{}),void 0!=e.autosave&&void 0!=e.autosave.unique_id&&""!=e.autosave.unique_id&&(e.autosave.uniqueId=e.autosave.unique_id),this.options=e,this.render(),!e.initialValue||this.options.autosave&&this.options.autosave.foundSavedValue===!0||this.value(e.initialValue)}function _(){if("object"!=typeof localStorage)return!1;try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch(e){return!1}return!0}var F=e("codemirror");e("codemirror/addon/edit/continuelist.js"),e("./codemirror/tablist"),e("codemirror/addon/display/fullscreen.js"),e("codemirror/mode/markdown/markdown.js"),e("codemirror/addon/mode/overlay.js"),e("codemirror/addon/display/placeholder.js"),e("codemirror/addon/selection/mark-selection.js"),e("codemirror/mode/gfm/gfm.js"),e("codemirror/mode/xml/xml.js");var z=e("codemirror-spell-checker"),j=e("marked"),U=/Mac/.test(navigator.platform),q={toggleBold:c,toggleItalic:u,drawLink:k,toggleHeadingSmaller:p,toggleHeadingBigger:m,drawImage:S,toggleBlockquote:d,toggleOrderedList:b,toggleUnorderedList:x,toggleCodeBlock:h,togglePreview:A,toggleStrikethrough:f,toggleHeading1:g,toggleHeading2:v,toggleHeading3:y,cleanBlock:w,drawTable:C,drawHorizontalRule:L,undo:T,redo:M,toggleSideBySide:N,toggleFullScreen:s},G={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},Y=function(e){for(var t in q)if(q[t]===e)return t;return null},$=function(){var e=!1;return function(t){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(t)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(t.substr(0,4)))&&(e=!0); -}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:f,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:d,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:S,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:C,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:["![](","#url#)"],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Z={link:"URL for the link:",image:"URL of the image:"},J={bold:"**",code:"```",italic:"*"};B.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},B.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var a,l;if(t.spellChecker!==!1?(a="spell-checker",l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1,z({codeMirrorInstance:F})):(a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1),this.codemirror=F.fromTextArea(e,{mode:a,backdrop:l,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||"",styleSelectedText:void 0!=t.styleSelectedText?t.styleSelectedText:!0}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element;var u=this.codemirror;setTimeout(function(){u.refresh()}.bind(u),0)}},B.prototype.autosave=function(){if(_()){var e=this;if(void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to use the autosave feature");null!=e.element.form&&void 0!=e.element.form&&e.element.form.addEventListener("submit",function(){localStorage.removeItem("smde_"+e.options.autosave.uniqueId)}),this.options.autosave.loaded!==!0&&("string"==typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&""!=localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0),localStorage.setItem("smde_"+this.options.autosave.uniqueId,e.value());var t=document.getElementById("autosaved");if(null!=t&&void 0!=t&&""!=t){var n=new Date,r=n.getHours(),i=n.getMinutes(),o="am",a=r;a>=12&&(a=r-12,o="pm"),0==a&&(a=12),i=10>i?"0"+i:i,t.innerHTML="Autosaved: "+a+":"+i+" "+o}this.autosaveTimeoutId=setTimeout(function(){e.autosave()},this.options.autosave.delay||1e4)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.clearAutosavedValue=function(){if(_()){if(void 0==this.options.autosave||void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to clear the autosave value");localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.createSideBySide=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;n&&/editor-preview-side/.test(n.className)||(n=document.createElement("div"),n.className="editor-preview-side",t.parentNode.insertBefore(n,t.nextSibling));var r=!1,i=!1;return e.on("scroll",function(e){if(r)return void(r=!1);i=!0;var t=e.getScrollInfo().height-e.getScrollInfo().clientHeight,o=parseFloat(e.getScrollInfo().top)/t,a=(n.scrollHeight-n.clientHeight)*o;n.scrollTop=a}),n.onscroll=function(){if(i)return void(i=!1);r=!0;var t=n.scrollHeight-n.clientHeight,o=parseFloat(n.scrollTop)/t,a=(e.getScrollInfo().height-e.getScrollInfo().clientHeight)*o;e.scrollTo(0,a)},n},B.prototype.createToolbar=function(e){if(e=e||this.options.toolbar,e&&0!==e.length){var t;for(t=0;t +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {{ field.label if label }} {{ field() }} {% for error in field.errors %} {{ error }} diff --git a/app/utils/render.py b/app/utils/render.py index ac2da5f..f319886 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -16,19 +16,18 @@ def render(*args, styles=[], scripts=[], **kwargs): 'css/flash.css', 'css/table.css', 'css/pagination.css', - 'css/simplemde.min.css', - 'css/simplemde-override.css', 'css/debugger.css', 'css/programs.css', + 'css/editor.css', ] scripts_ = [ 'scripts/trigger_menu.js', 'scripts/pc-utils.js', 'scripts/smartphone_patch.js', - 'scripts/simplemde.min.js', 'scripts/gallery.js', 'scripts/filter.js', 'scripts/tag_selector.js', + 'scripts/editor.js', ] # Apply theme from user settings From 5bb581f4f3b62f64f90a24f50fec481efb1352b1 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 21 Apr 2022 21:06:05 +0200 Subject: [PATCH 31/92] Delete scripts --- scripts/init.sh | 4 ---- scripts/migrate.sh | 3 --- scripts/run_dev.sh | 2 -- 3 files changed, 9 deletions(-) delete mode 100755 scripts/init.sh delete mode 100755 scripts/migrate.sh delete mode 100755 scripts/run_dev.sh diff --git a/scripts/init.sh b/scripts/init.sh deleted file mode 100755 index 64476b7..0000000 --- a/scripts/init.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -flask db init -flask db migrate -m "initialisation" -flask db upgrade diff --git a/scripts/migrate.sh b/scripts/migrate.sh deleted file mode 100755 index 843deb8..0000000 --- a/scripts/migrate.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env sh -flask db migrate -m $1 -flask db upgrade diff --git a/scripts/run_dev.sh b/scripts/run_dev.sh deleted file mode 100755 index c2ac34f..0000000 --- a/scripts/run_dev.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -flask run From a01b74f3e85dc6fb2c364e32796ce44fd56b61f2 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 21 Apr 2022 21:46:51 +0200 Subject: [PATCH 32/92] Change themes for better contrast on buttons --- app/static/css/themes/FK_dark_theme.css | 3 ++ app/static/css/themes/default_theme.css | 60 ++++++++++++------------- app/static/less/editor.less | 12 +++-- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css index 3d9bd13..dfe315e 100644 --- a/app/static/css/themes/FK_dark_theme.css +++ b/app/static/css/themes/FK_dark_theme.css @@ -127,6 +127,9 @@ table tr:nth-child(odd) { table.codehilitetable { --background: #263238; } +.editor svg { + --icons: #fff; +} div.editor-toolbar, div.CodeMirror { --border: #404040; diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index 68ec1d6..404e517 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -1,26 +1,26 @@ /* Some colors, variables etc. to be used as theme */ :root { - --background: #ffffff; - --text: #000000; - --text-light: #101010; + --background: #fff; + --text: #000; + --text-light: #111; --links: #c61a1a; --ok: #149641; - --ok-text: #ffffff; + --ok-text: #fff; --ok-active: #0f7331; --warn: #f59f25; - --warn-text: #ffffff; + --warn-text: #fff; --warn-active: #ea9720; --error: #d23a2f; - --error-text: #ffffff; + --error-text: #fff; --error-active: #b32a20; --info: #2e7aec; - --info-text: #ffffff; + --info-text: #fff; --info-active: #215ab0; --hr-border: 1px solid #d8d8d8; @@ -33,21 +33,21 @@ table tr:nth-child(odd) { --background: rgba(0, 0, 0, .1); } table th { - --background: #e0e0e0; - --border: #d0d0d0; + --background: #eee; + --border: #ddd; } .form { - --background: #ffffff; - --text: #000000; + --background: #fff; + --text: #000; --border: 1px solid #c8c8c8; --border-focused: #7cade0; --shadow-focused: rgba(87, 143, 228, 0.5); } .editor button { - --background: #ffffff; - --text: #000000; + --background: #fff; + --text: #000; --border: 1px solid rgba(0, 0, 0, 0); --border-focused: 1px solid rgba(0, 0, 0, .5); } @@ -82,13 +82,13 @@ header { footer { --background: #ffffff; - --text: #a0a0a0; - --border: #d0d0d0; + --text: #aaa; + --border: #ddd; } .flash { - --background: #ffffff; - --text: #212121; + --background: #fff; + --text: #222; --shadow: 0 1px 12px rgba(0, 0, 0, 0.3); /* Uncomment to inherit :root values @@ -98,27 +98,27 @@ footer { --info: #2e7aec; */ --btn-bg: rgba(0, 0, 0, 0); - --btn-text: #000000; + --btn-text: #000; --btn-bg-active: rgba(0, 0, 0, .15); } .profile-xp { - --background: #e0e0e0; - --border: 1px solid #c0c0c0; - --background-xp: #f85555; - --background-xp-100: #d03333; - --border-xp: 1px solid #d03333; + --background: #eee; + --border: 1px solid #ccc; + --background-xp: #f55; + --background-xp-100: #d33; + --border-xp: 1px solid #d33; } .context-menu { - --background: #ffffff; - --shadow: 0 0 12px -9px #000000; - --border: #d0d0d0; - --background-light: #f0f0f0; + --background: #fff; + --shadow: 0 0 12px -9px #000; + --border: #ddd; + --background-light: #fff; } -.editor { - --icons: #ffffff; +.editor svg { + --icons: #000; } .dl-button { @@ -136,7 +136,7 @@ footer { /* Extra style on top of the Pygments style */ table.codehilitetable td.linenos { - color: #808080; + color: #888; } .tag { diff --git a/app/static/less/editor.less b/app/static/less/editor.less index f51946e..520b6f7 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -1,11 +1,15 @@ @import "vars"; .editor { - & > button > svg { - width: 25px; + & > button { + background-color: #000; - & > path, & > rect { - fill: var(--icons); + & > svg { + width: 25px; + + & > path, & > rect { + fill: var(--icons); + } } } } From 490ab2714c5f0fca74fc25db6caab33dde893c70 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 21 Apr 2022 23:10:54 +0200 Subject: [PATCH 33/92] Start to add editor script --- app/static/css/editor.css | 22 ++- app/static/css/themes/FK_dark_theme.css | 3 +- app/static/less/editor.less | 28 ++- app/static/scripts/editor.js | 96 +-------- app/templates/widgets/editor.html | 251 ++++++++++++------------ 5 files changed, 180 insertions(+), 220 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index 8251dc5..072aeec 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -1,7 +1,23 @@ -.editor > button > svg { +.editor .btn-group { + display: flex; + justify-content: space-between; +} +.editor .btn-group button { + background-color: var(--background); +} +.editor .btn-group button > svg { width: 25px; } -.editor > button > svg > path, -.editor > button > svg > rect { +.editor .btn-group button > svg > path, +.editor .btn-group button > svg > rect { fill: var(--icons); +} +.separator { + display: inline-block; + width: 0; + border-left: 1px solid #d9d9d9; + border-right: 1px solid #ffffff; + color: transparent; + text-indent: -10px; + margin: 0 6px; } \ No newline at end of file diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css index dfe315e..7936096 100644 --- a/app/static/css/themes/FK_dark_theme.css +++ b/app/static/css/themes/FK_dark_theme.css @@ -127,8 +127,9 @@ table tr:nth-child(odd) { table.codehilitetable { --background: #263238; } -.editor svg { +.editor svg, .editor button { --icons: #fff; + --background: #0d1215; } div.editor-toolbar, div.CodeMirror { diff --git a/app/static/less/editor.less b/app/static/less/editor.less index 520b6f7..1f4ce94 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -1,15 +1,31 @@ @import "vars"; .editor { - & > button { - background-color: #000; + & .btn-group { + display: flex; + justify-content: space-between; - & > svg { - width: 25px; + & button { + background-color: var(--background); - & > path, & > rect { - fill: var(--icons); + & > svg { + width: 25px; + + & > path, & > rect { + fill: var(--icons); + } } } } } + +// From gitea +.separator { + display: inline-block; + width: 0; + border-left: 1px solid #d9d9d9; + border-right: 1px solid #fff; + color: transparent; + text-indent: -10px; + margin: 0 6px; +} diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 56a3e83..231ae53 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -1,87 +1,11 @@ /* Add callbacks on text formatting buttons */ -function edit(e, type) { - function inline(type, str, repeat, insert) { - // Characters used to format inline blocs - // repeat: if true, add one more char to the longest suite found - // insert: insert between char and str (before and after) - var chars = { - 'bold': '*', - 'italic': '/', - 'underline': '_', - 'strikethrough': '~', - 'inline-code': '`', - 'h1': '===', - 'h2': '---', - 'h3': '...', - } - - if (repeat) { - // Detect longest suite of similar chars - var n = 1; var tmp = 1; - for(var i = 0; i < str.length; i++) { - if(str[i] == chars[type]) tmp++; - else tmp = 1; - n = (tmp > n) ? tmp : n; - } - return chars[type].repeat(n) + insert + str + insert + chars[type].repeat(n); - } - - return chars[type] + insert + str + insert + chars[type]; - } - - function list(type, str) { - switch(type) { - case 'list-bulleted': - return '* ' + str.replaceAll('\n', '\n* '); - break; - case 'list-numbered': - return '1. ' + str; - break; - } - } - - var ta = e.parentNode.parentNode.querySelector('textarea'); - var start = ta.selectionStart; - var end = ta.selectionEnd; - - switch(type) { - case 'bold': - case 'italic': - case 'underline': - case 'strikethrough': - case 'inline-code': - ta.value = ta.value.substring(0, start) - + inline(type, ta.value.substring(start, end), true, '') - + ta.value.substring(end); - break; - case 'h1': - case 'h2': - case 'h3': - ta.value = ta.value.substring(0, start) - + inline(type, ta.value.substring(start, end), false, ' ') - + ta.value.substring(end); - break; - case 'list-bulleted': - case 'list-numbered': - ta.value = ta.value.substring(0, start) - + list(type, ta.value.substring(start, end)) - + ta.value.substring(end); - break; - } -} - -function pre(type, str, multiline) { - -} - - -function bold(e) { - var ta = e.parentNode.parentNode.querySelector('textarea'); - var indexStart = ta.selectionStart; - var indexEnd = ta.selectionEnd; - var txt = ta.value.substring(indexStart, indexEnd); - ta.value += '\n' + inline('bold', txt); +function editor_bold(e) { + let ta = document.querySelector(".editor textarea"); + let indexStart = ta.selectionStart; + let indexEnd = ta.selectionEnd; + let txt = ta.value.substring(indexStart, indexEnd); + ta.value += '\n' + 'bold'; } @@ -89,18 +13,18 @@ function bold(e) { // Ctrl+Enter send the form ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { - var keyCode = e.keyCode || e.which; + let keyCode = e.keyCode || e.which; if (keyCode == 9) { e.preventDefault(); - var start = e.target.selectionStart; - var end = e.target.selectionEnd; + let start = e.target.selectionStart; + let end = e.target.selectionEnd; // set textarea value to: text before caret + tab + text after caret e.target.value = e.target.value.substring(0, start) + "\t" + e.target.value.substring(end); e.target.selectionEnd = start + 1; } if (e.ctrlKey && keyCode == 13) { - var e = e.target; + let e = e.target; while(! (e instanceof HTMLFormElement)) { e = e.parentNode; } diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 97ef374..4d27eea 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -1,128 +1,131 @@ {% macro text_editor(field, label=True, autofocus=false) %} -
    - - - - - - - - - - - - - - - - - - - - - - - + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + {{ field.label if label }} + {{ field() }} + + + + + + {% for error in field.errors %} + {{ error }} + {% endfor %}
    - {{ field.label if label }} - {{ field() }} - - {% for error in field.errors %} - {{ error }} - {% endfor %} {% endmacro %} From 0a0ad4d55869fba583cd62cdd27774616b874f04 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 21 Apr 2022 23:34:32 +0200 Subject: [PATCH 34/92] editor: Add input type button to fix HTML problems --- app/static/scripts/entropy.js | 8 +++--- app/static/scripts/filter.js | 8 +++--- app/static/scripts/pc-utils.js | 8 +++--- app/static/scripts/smartphone_patch.js | 8 +++--- app/static/scripts/trigger_menu.js | 32 +++++++++++----------- app/templates/widgets/editor.html | 38 +++++++++++++------------- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/app/static/scripts/entropy.js b/app/static/scripts/entropy.js index 3465557..369663a 100644 --- a/app/static/scripts/entropy.js +++ b/app/static/scripts/entropy.js @@ -1,5 +1,5 @@ function entropy(password) { - var chars = [ + let chars = [ "abcdefghijklmnopqrstuvwxyz", "ABCDFEGHIJKLMNOPQRSTUVWXYZ", "0123456789", @@ -19,9 +19,9 @@ function entropy(password) { } function update_entropy(ev) { - var i = document.querySelector(".entropy").previousElementSibling; - var p = document.querySelector(".entropy"); - var e = entropy(i.value); + let i = document.querySelector(".entropy").previousElementSibling; + let p = document.querySelector(".entropy"); + let e = entropy(i.value); p.classList.remove('low'); p.classList.remove('medium'); diff --git a/app/static/scripts/filter.js b/app/static/scripts/filter.js index 6d5f9d3..661b355 100644 --- a/app/static/scripts/filter.js +++ b/app/static/scripts/filter.js @@ -8,7 +8,7 @@ const patterns = [ function* lex(str) { while(str = str.trim()) { - var t = T.ERR, best = undefined; + let t = T.ERR, best = undefined; for(const i in patterns) { const m = str.match(patterns[i]); @@ -86,7 +86,7 @@ class Parser { return e; } - var e = { + let e = { type: "Atom", field: this.expect(T.NAME), op: this.expect(T.COMP), @@ -124,8 +124,8 @@ function filter_update(input) { const th = t.querySelectorAll("tr:first-child > th"); /* Generate the names of fields from the header */ - var fields = {}; - for(var i = 0; i < th.length; i++) { + let fields = {}; + for(let i = 0; i < th.length; i++) { const name = th[i].dataset.filter; if(name) fields[name] = i; } diff --git a/app/static/scripts/pc-utils.js b/app/static/scripts/pc-utils.js index 98948d4..bc213ea 100644 --- a/app/static/scripts/pc-utils.js +++ b/app/static/scripts/pc-utils.js @@ -1,13 +1,13 @@ function setCookie(name, value) { - var end = new Date(); + let end = new Date(); end.setTime( end.getTime() + 3600 * 1000 ); - var str=name+"="+escape(value)+"; expires="+end.toGMTString()+"; path=/; Secure; SameSite=lax"; + let str=name+"="+escape(value)+"; expires="+end.toGMTString()+"; path=/; Secure; SameSite=lax"; document.cookie = str; } function getCookie(name) { - var debut = document.cookie.indexOf(name); + let debut = document.cookie.indexOf(name); if( debut == -1 ) return null; - var end = document.cookie.indexOf( ";", debut+name.length+1 ); + let end = document.cookie.indexOf( ";", debut+name.length+1 ); if( end == -1 ) end = document.cookie.length; return unescape( document.cookie.substring( debut+name.length+1, end ) ); } diff --git a/app/static/scripts/smartphone_patch.js b/app/static/scripts/smartphone_patch.js index 83b01ec..954a8db 100644 --- a/app/static/scripts/smartphone_patch.js +++ b/app/static/scripts/smartphone_patch.js @@ -1,11 +1,11 @@ /* Smartphone patch for menu */ /* It don't work if links haven't any href attribute */ -var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0) +let w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0) if(w < 700) { - var buttons = document.getElementById('light-menu').getElementsByTagName('li'); - for(var i = 0; i < buttons.length; i++) { + let buttons = document.getElementById('light-menu').getElementsByTagName('li'); + for(let i = 0; i < buttons.length; i++) { buttons[i].getElementsByTagName('a')[0].setAttribute('href', '#'); } -} \ No newline at end of file +} diff --git a/app/static/scripts/trigger_menu.js b/app/static/scripts/trigger_menu.js index 7c3fcfa..07fa7ce 100644 --- a/app/static/scripts/trigger_menu.js +++ b/app/static/scripts/trigger_menu.js @@ -1,24 +1,24 @@ /* Trigger actions for the menu */ /* Initialization */ -var b = document.querySelectorAll('#light-menu a'); -for(var i = 1; i < b.length; i++) { +let b = document.querySelectorAll('#light-menu a'); +for(let i = 1; i < b.length; i++) { b[i].setAttribute('onfocus', "this.setAttribute('f', 'true');"); b[i].setAttribute('onblur', "this.setAttribute('f', 'false');"); b[i].removeAttribute('href'); } -var trigger_menu = function(active) { - var display = function(element) { +let trigger_menu = function(active) { + let display = function(element) { element.classList.add('opened'); } - var hide = function(element) { + let hide = function(element) { element.classList.remove('opened'); } - var menu = document.querySelector('#menu'); - var buttons = document.querySelectorAll('#light-menu li'); - var menus = document.querySelectorAll('#menu > div'); + let menu = document.querySelector('#menu'); + let buttons = document.querySelectorAll('#light-menu li'); + let menus = document.querySelectorAll('#menu > div'); if(active == -1 || buttons[active].classList.contains('opened')) { hide(menu); @@ -39,12 +39,12 @@ var trigger_menu = function(active) { } } -var mouse_trigger = function(event) { - var menu = document.querySelector('#menu'); - var buttons = document.querySelectorAll('#light-menu li'); +let mouse_trigger = function(event) { + let menu = document.querySelector('#menu'); + let buttons = document.querySelectorAll('#light-menu li'); if(!menu.contains(event.target)) { - var active = -1; + let active = -1; for(i = 0; i < buttons.length; i++) { if(buttons[i].contains(event.target)) @@ -56,12 +56,12 @@ var mouse_trigger = function(event) { } } -var keyboard_trigger = function(event) { - var menu = document.getElementById('menu'); - var buttons = document.querySelectorAll('#light-menu li'); +let keyboard_trigger = function(event) { + let menu = document.getElementById('menu'); + let buttons = document.querySelectorAll('#light-menu li'); if(event.keyCode == 13) { - for(var i = 0; i < buttons.length; i++) { + for(let i = 0; i < buttons.length; i++) { if(buttons[i].querySelector('a').getAttribute('f') == 'true') { trigger_menu(i); } diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 4d27eea..c95708e 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -3,22 +3,22 @@
    - - - - - - - - - - - - - - + +
    From 15ce72b72d97707da840ada0f737d69a486f4f06 Mon Sep 17 00:00:00 2001 From: Lephe Date: Wed, 27 Apr 2022 10:51:52 +0100 Subject: [PATCH 37/92] editor: fix variable shadowing in let causing use before declaration --- app/static/scripts/editor.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index d12b403..4304b4f 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -25,14 +25,14 @@ ta.addEventListener('keydown', function(e) { e.target.selectionEnd = start + 1; } if (e.ctrlKey && keyCode == 13) { - let e = e.target; - while(! (e instanceof HTMLFormElement)) { - e = e.parentNode; + let t = e.target; + while(! (t instanceof HTMLFormElement)) { + t = t.parentNode; } try { - e.submit(); + t.submit(); } catch(exception) { - e.submit.click(); + t.submit.click(); } } }); From e54b01efe0fc95c829d625ec0f9aa6654a9e255c Mon Sep 17 00:00:00 2001 From: Lephe Date: Wed, 27 Apr 2022 11:43:35 +0100 Subject: [PATCH 38/92] editor: basic markup insertion around selection --- app/static/scripts/editor.js | 45 +++++++++++++++++++++++++++---- app/templates/widgets/editor.html | 8 +++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 4304b4f..df09d14 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -1,13 +1,48 @@ /* Add callbacks on text formatting buttons */ -function editor_bold(e) { - let ta = document.querySelector(".editor textarea"); +function editor_insert_around(e) { + let button = undefined; + let editor = undefined; + + /* Grab the button and the parent editor block. The onclick event itself + usually reports the SVG in the button as the source */ + let node = e.target || e.srcElement; + while(node != document.body) { + if(node.tagName == "BUTTON" && !button) + button = node; + if(node.classList.contains("editor") && !editor) { + editor = node; + break; + } + node = node.parentNode; + } + if(!button || !editor) return; + + /* Find the textarea */ + const ta = editor.querySelector("textarea"); + ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; - let txt = ta.value.substring(indexStart, indexEnd); - ta.value += '\n' + 'bold'; -} + const before = button.dataset.before || ""; + const after = button.dataset.after || ""; + + ta.value = ta.value.substring(0, indexStart) + + before + + ta.value.substring(indexStart, indexEnd) + + after + + ta.value.substring(indexEnd); + + /* Restore selection */ + if(indexStart != indexEnd) { + ta.selectionStart = indexStart; + ta.selectionEnd = indexEnd + before.length + after.length; + } + else { + ta.selectionStart = indexStart + before.length; + ta.selectionEnd = ta.selectionStart; + } +} // Tab insert some spaces // Ctrl+Enter send the form diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index a0363cf..363ced9 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -3,22 +3,22 @@
    - - - - - - - - - - -
    - {{ c.text|md }} + {{ c.text|md(c.id) }} {{ widget_attachments.attachments(c) }} {% if c.author.signature %} diff --git a/app/utils/filters/markdown.py b/app/utils/filters/markdown.py index 71912b4..d353bcb 100644 --- a/app/utils/filters/markdown.py +++ b/app/utils/filters/markdown.py @@ -3,7 +3,7 @@ from markupsafe import Markup from markdown import markdown from markdown.extensions.codehilite import CodeHiliteExtension from markdown.extensions.footnotes import FootnoteExtension -from markdown.extensions.toc import TocExtension +from markdown.extensions.toc import TocExtension, slugify_unicode from bleach import clean from app.utils.bleach_allowlist import markdown_tags, markdown_attrs @@ -15,8 +15,14 @@ from app.utils.markdown_extensions.media import MediaExtension from app.utils.markdown_extensions.gallery import GalleryExtension +def slug(prefix, text, sep): + if prefix is None: + return slugify_unicode(text, sep) + else: + return str(prefix) + sep + slugify_unicode(text, sep) + @app.template_filter('md') -def md(text): +def md(text, prefix=None): """ Converts markdown to html5 """ @@ -33,7 +39,7 @@ def md(text): FootnoteExtension(UNIQUE_IDS=True), HardBreakExtension(), LinkifyExtension(), - TocExtension(baselevel=2), + TocExtension(baselevel=2, slugify=lambda *args: slug(prefix, *args)), PCLinkExtension(), MediaExtension(), GalleryExtension(), From 07bd7075d6996c1f5da75422ae4c510c3eeebf37 Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 28 Apr 2022 19:49:42 +0100 Subject: [PATCH 43/92] editor: add placeholder help link --- app/static/css/editor.css | 7 +++++-- app/static/less/editor.less | 7 +++++-- app/templates/widgets/editor.html | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index afefba3..ca3b05d 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -2,13 +2,12 @@ display: flex; justify-content: space-between; flex-wrap: wrap; + align-items: flex-end; } .editor .btn-group .filler { flex-grow: 1; } .editor .btn-group button { - width: 32px; - height: 32px; padding: 6px; background-color: var(--background); } @@ -26,6 +25,10 @@ .editor .btn-group button, .editor .btn-group .separator { margin: 0 8px 8px 0; + height: 32px; +} +.editor .btn-group > a { + margin: 0 0 8px 0; } .editor textarea { min-height: 15rem; diff --git a/app/static/less/editor.less b/app/static/less/editor.less index a28524f..d643d24 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -5,13 +5,12 @@ display: flex; justify-content: space-between; flex-wrap: wrap; + align-items: flex-end; .filler { flex-grow: 1; } button { - width: 32px; - height: 32px; /* This centers the 20x20 SVG in the button */ padding: 6px; background-color: var(--background); @@ -32,6 +31,10 @@ button, .separator { margin: 0 8px 8px 0; + height: 32px; + } + & > a { + margin: 0 0 8px 0; } } diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index fde8bd4..80343d5 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -114,6 +114,7 @@
    +
    Aide From bcbab7033da25d9f12e8d2f4b72555cf27ad3fd6 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 12 May 2022 23:02:40 +0200 Subject: [PATCH 44/92] editor: Add preview --- app/routes/api/markdown.py | 2 ++ app/static/scripts/editor.js | 43 ++++++++++++++++++++++++++++--- app/templates/widgets/editor.html | 11 +++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/routes/api/markdown.py b/app/routes/api/markdown.py index 105111c..9ed98e8 100644 --- a/app/routes/api/markdown.py +++ b/app/routes/api/markdown.py @@ -2,9 +2,11 @@ from app import app from app.utils.filters.markdown import md from flask import request, abort from werkzeug.exceptions import BadRequestKeyError +from app import csrf class API(): @app.route("/api/markdown", methods=["POST"]) + @csrf.exempt def api_markdown(): try: markdown = request.get_json()['text'] diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 2cfc81b..1ce15f2 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -12,8 +12,9 @@ function editor_event_source(event) usually reports the SVG in the button as the source */ let node = event.target || event.srcElement; while(node != document.body) { - if(node.tagName == "BUTTON" && !button) + if(node.tagName == "BUTTON" && !button) { button = node; + } if(node.classList.contains("editor") && !editor) { editor = node; break; @@ -123,10 +124,11 @@ function editor_set_title(event, level, diff) }); } -// Tab insert some spaces -// Ctrl+Enter send the form +previewTimeout = null; ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { + // Tab insert some spaces + // Ctrl+Enter send the form let keyCode = e.keyCode || e.which; if (keyCode == 9) { // TODO Add one tab to selected text without replacing it @@ -149,4 +151,39 @@ ta.addEventListener('keydown', function(e) { t.submit.click(); } } + + // Set a timeout for refreshing the preview + if (previewTimeout != null) { + clearTimeout(previewTimeout); + } + previewTimeout = setTimeout(preview, 1000); }); + + +function preview() { + const previewArea = document.querySelector("#editor_content_preview"); + + const textarea = document.querySelector(".editor textarea"); + const payload = {text: ta.value}; + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + + const params = { + method: "POST", + body: JSON.stringify(payload), + headers + }; + + console.log(payload); + + fetch("/api/markdown", params).then( + (response) => { + response.text().then( + (text) => { + previewArea.innerHTML = text; + } + ); + }); +} + diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 80343d5..9942cf7 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -121,14 +121,17 @@ {{ field.label if label }} {{ field() }} - - +
    +
    {% for error in field.errors %} {{ error }} {% endfor %} + + + {% endmacro %} From 0d00b4dfb6ace90622c87d192e7478101df2550e Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 19 May 2022 22:29:45 +0200 Subject: [PATCH 45/92] Editor: Add inline code and quotes --- app/static/scripts/editor.js | 16 ++++++++++++++++ app/templates/widgets/editor.html | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 1ce15f2..47c9c0b 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -124,6 +124,22 @@ function editor_set_title(event, level, diff) }); } +function editor_quote(event) +{ + editor_act_on_lines(event, function(line) { + /* Strip all the initial > (and count them) */ + let count = 0; + while(count < line.length && line[count] == '>') count++; + + let contents_index = count; + if(count < line.length && line[count] == ' ') contents_index++; + let contents = line.slice(contents_index); + + /* Apply the difference */ + return '>'.repeat(count + 1) + ' ' + contents; + }); +} + previewTimeout = null; ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 9942cf7..ead4515 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -54,12 +54,12 @@ - - + - - - - - - - --> +
    Aide From 28935b2ae80c7e86a502d344246a7d29c63bbf5c Mon Sep 17 00:00:00 2001 From: Eragon Date: Mon, 28 Nov 2022 11:25:22 +0100 Subject: [PATCH 52/92] editor: The numbered list button have the same usage as the old one --- app/static/scripts/editor.js | 14 ++++++++++++++ app/templates/widgets/editor.html | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index decaebe..3319573 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -154,6 +154,20 @@ function editor_bullet_list(event) }); } +function editor_numbered_list(event) +{ + editor_act_on_lines(event, function(line) { + let ident_match = line.match(/^[\t]+/m) ?? ['']; + let ident = ident_match[0]; + let count = ident.length; + + const contents = line.slice(count); + if((count < line.length || count == 0) && isNaN(line[count])) return '1. ' + contents; + + return ident + "\t" + contents; + }); +} + previewTimeout = null; ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 68d6cf4..46663d2 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -73,7 +73,7 @@ - - - --> -
    Aide From 5eaf1cc2077040a036a71f260a0026ec9ae3d74d Mon Sep 17 00:00:00 2001 From: Eragon Date: Mon, 12 Dec 2022 19:21:59 +0100 Subject: [PATCH 56/92] editor: Link and Images buttons works, still need some CSS and cleaning --- app/static/scripts/editor.js | 60 +++++++++++++++---------------- app/templates/widgets/editor.html | 29 +++++++++++---- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index e9ac0c9..a1f916b 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -96,42 +96,42 @@ function editor_act_on_lines(event, fn) ta.selectionEnd = end; } -/* - * Create a modal used to get input from the user when generating some M⬇ -*/ -function editor_create_modal(event, content = document.createElement("div"), fn = {}) { - // The anchor is two levels above because the event.srcElement/event.target - // are the path inside the button. - const anchor = event.currentTarget; +function editor_clear_modals(event, close = true) { + // Stop the propagation of the event + event.stopPropagation() - const modal = document.createElement("div"); - const contentDiv = document.createElement("div"); - const btnDiv = document.createElement("div"); - const validateBtn = document.createElement("button"); - const cancelBtn = document.createElement("button"); + // Reset all modal inputs + document.getElementById('img-alt-input').value = ''; + document.getElementById('img-link-input').value = ''; + document.getElementById('link-desc-input').value = ''; + document.getElementById('link-desc-input').value = ''; - modal.classList.add("modal"); + // Close all modal if requested + if (!close) { return } + const modals = document.getElementsByClassName('modal'); + for (const i of modals) {i.style.display = 'none'}; +} - validateBtn.onclick = function() { - modal.remove() +function editor_insert_link(event, link, text = "", image = false) +{ + const [editor, button, ta] = editor_event_source(event); + ta.focus(); + let indexStart = ta.selectionStart; + let indexEnd = ta.selectionEnd; - fn(event, content); - }; - validateBtn.textContent = "Valider"; - validateBtn.classList = ["bg-ok"]; + editor_clear_modals(event); - cancelBtn.onclick = function() { - modal.remove() - }; - cancelBtn.textContent = "Annuler"; - cancelBtn.classList = ["bg-error"]; + let [start, end] = editor_replace_range(ta, indexStart, indexEnd, + `[${text.length === 0 ? ta.value.substring(indexStart, indexEnd) : text}]${image ? "!" : ""}(${link})`); - contentDiv.appendChild(content); - modal.appendChild(contentDiv); - btnDiv.appendChild(validateBtn); - btnDiv.appendChild(cancelBtn); - modal.appendChild(btnDiv); - anchor.appendChild(modal); + /* Restore selection */ + if(indexStart != indexEnd) { + ta.selectionStart = start; + ta.selectionEnd = end; + } + else { + ta.selectionStart = ta.selectionEnd = start + 1; + } } function editor_set_title(event, level, diff) diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 165f9bb..d9798bb 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -103,30 +103,47 @@ --> - - +
    Aide From e2283b7675d0256a37ac7ff9ca1a560fc158a8bb Mon Sep 17 00:00:00 2001 From: Eragon Date: Mon, 12 Dec 2022 19:33:56 +0100 Subject: [PATCH 57/92] editor: CSS and JS cleaning --- app/static/css/editor.css | 16 ++++++++++++---- app/static/css/form.css | 3 +++ app/static/less/editor.less | 18 ++++++++++++++---- app/static/less/form.less | 1 + app/static/scripts/editor.js | 5 ++++- app/templates/widgets/editor.html | 4 ++-- 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index 7ef3201..9c39e0b 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -51,16 +51,24 @@ position: absolute; left: 0px; width: auto; + min-width: 30vw; white-space: nowrap; text-align: left; right: inherit; - background-image: none; - background-color: #161819; - border-color: #3a3f42; - border-radius: .4rem; + background: var(--background-hover); + border: var(--border); + color: var(--text); padding: .2rem; top: 3.2rem; z-index: 100; list-style-position: initial; list-style-type: none; +} +.editor .modal > div { + margin: 0.8rem; + margin-top: 0.4rem; + margin-bottom: 1rem; +} +.editor .modal > div label { + margin-top: 0.4rem; } \ No newline at end of file diff --git a/app/static/css/form.css b/app/static/css/form.css index 1597f03..5c40203 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -23,6 +23,7 @@ .form input[type='date'], .form input[type='password'], .form input[type='search'], +.form input[type='url'], .form textarea, .form select { display: block; @@ -38,6 +39,7 @@ .form input[type='date']:focus, .form input[type='password']:focus, .form input[type='search']:focus, +.form input[type='url']:focus, .form textarea:focus, .form select:focus { border-color: var(--border-focused); @@ -48,6 +50,7 @@ .form input[type='date']:focus-within, .form input[type='password']:focus-within, .form input[type='search']:focus-within, +.form input[type='url']:focus-within, .form textarea:focus-within, .form select:focus-within { outline: none; diff --git a/app/static/less/editor.less b/app/static/less/editor.less index 484b834..4317849 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -64,19 +64,29 @@ position: absolute; left: 0px; width: auto; + min-width: 30vw; white-space: nowrap; text-align: left; right: inherit; - background-image: none; - background-color: rgb(22, 24, 25); - border-color: rgb(58, 63, 66); - border-radius: .4rem; + background: var(--background-hover); + border: var(--border); + color: var(--text); padding: .2rem; top: 3.2rem; z-index: 100; list-style-position: initial; list-style-type: none; + + & > div { + margin: 0.8rem; + margin-top: 0.4rem; + margin-bottom: 1rem; + + label { + margin-top: 0.4rem; + } + } } } diff --git a/app/static/less/form.less b/app/static/less/form.less index fa76a8f..6673f7a 100644 --- a/app/static/less/form.less +++ b/app/static/less/form.less @@ -32,6 +32,7 @@ input[type='date'], input[type='password'], input[type='search'], + input[type='url'], textarea, select { display: block; diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index a1f916b..6ad3524 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -112,13 +112,16 @@ function editor_clear_modals(event, close = true) { for (const i of modals) {i.style.display = 'none'}; } -function editor_insert_link(event, link, text = "", image = false) +function editor_insert_link(event, link_id, text_id, image = false) { const [editor, button, ta] = editor_event_source(event); ta.focus(); let indexStart = ta.selectionStart; let indexEnd = ta.selectionEnd; + const link = document.getElementById(link_id).value; + const text = document.getElementById(text_id).value; + editor_clear_modals(event); let [start, end] = editor_replace_range(ta, indexStart, indexEnd, diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index d9798bb..010e4ba 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -115,7 +115,7 @@ @@ -132,7 +132,7 @@ From 2da20720bbee3c51ca374cf77ed4a19ae7e07519 Mon Sep 17 00:00:00 2001 From: Eragon Date: Wed, 14 Dec 2022 09:56:22 +0100 Subject: [PATCH 58/92] editor: Dirty CSS hack for small screen devices --- app/static/css/editor.css | 10 +++++++++- app/static/less/editor.less | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index 9c39e0b..17a2f8e 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -52,7 +52,6 @@ left: 0px; width: auto; min-width: 30vw; - white-space: nowrap; text-align: left; right: inherit; background: var(--background-hover); @@ -71,4 +70,13 @@ } .editor .modal > div label { margin-top: 0.4rem; +} +@media screen and (max-width:849px) { + .editor .modal { + width: 80vw; + position: fixed; + left: 50vw; + transform: translateX(-50%); + top: 50vh; + } } \ No newline at end of file diff --git a/app/static/less/editor.less b/app/static/less/editor.less index 4317849..8ef98f5 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -65,10 +65,17 @@ left: 0px; width: auto; min-width: 30vw; - white-space: nowrap; text-align: left; right: inherit; + @media screen and (max-width: @tiny) { + width: 80vw; + position: fixed; + left: 50vw; + transform: translateX(-50%); + top: 50vh; + } + background: var(--background-hover); border: var(--border); color: var(--text); From 546b32c22b016f48157d6f6d85013597b3b35b54 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 15 Dec 2022 12:05:58 +0100 Subject: [PATCH 59/92] editor: Multiples bugfixes from Lephe's review Refresh preview 3 sec after last keypress Refresh preview on button usage Move cursor after the --- line when using the button Replace margin for padding in the preview css Add a slight background shade on the preview --- app/static/css/editor.css | 3 ++- app/static/less/editor.less | 3 ++- app/static/scripts/editor.js | 8 +++++++- app/templates/widgets/editor.html | 5 +++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index 17a2f8e..9f48062 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -43,9 +43,10 @@ min-height: 15rem; } .editor #editor_content_preview { - margin: 5px; + padding: 5px; margin-top: 10px; border: var(--border); + background-color: rgba(0,0,0,0.2); } .editor .modal { position: absolute; diff --git a/app/static/less/editor.less b/app/static/less/editor.less index 8ef98f5..774b91b 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -55,9 +55,10 @@ } #editor_content_preview { - margin: 5px; + padding: 5px; margin-top: 10px; border: var(--border); + background-color: rgba(0, 0, 0, 0.2); } .modal { diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 6ad3524..caaddd3 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -61,6 +61,8 @@ function editor_insert_around(event) else { ta.selectionStart = ta.selectionEnd = start + before.length; } + + preview(); } /* Event handler that modifies each line within the selection through a @@ -94,6 +96,8 @@ function editor_act_on_lines(event, fn) ta.selectionStart = start; ta.selectionEnd = end; + + preview(); } function editor_clear_modals(event, close = true) { @@ -135,6 +139,8 @@ function editor_insert_link(event, link_id, text_id, image = false) else { ta.selectionStart = ta.selectionEnd = start + 1; } + + preview(); } function editor_set_title(event, level, diff) @@ -241,7 +247,7 @@ ta.addEventListener('keydown', function(e) { if (previewTimeout != null) { clearTimeout(previewTimeout); } - previewTimeout = setTimeout(preview, 1000); + previewTimeout = setTimeout(preview, 3000); }); diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 010e4ba..2f25b94 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -90,8 +90,9 @@ - - - - - - - - - --> - - - + - + - --> + + + -
    +
    Aide From f15186d4f8c324aeac4ff42f122a98e0bae0709e Mon Sep 17 00:00:00 2001 From: Eragon Date: Mon, 15 May 2023 12:40:17 +0200 Subject: [PATCH 74/92] editor: Prevent default browser action for keybinds when textarea is foccused --- app/static/scripts/editor.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index d47f216..8b7a024 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -336,12 +336,15 @@ ta.addEventListener('keydown', function(e) { e.target.selectionEnd = start + 1; } - // TODO - // Ctrl+B adds bold - // Ctrl+I adds italic - // Ctrl+U adds underline - - // Ctrl+Enter send the form + /* + * Keybindings for buttons. The default action of the keybinding is prevented. + * Ctrl+B adds bold + * Ctrl+I adds italic + * Ctrl+U adds underline + * Ctrl+S adds strikethrough + * Ctrl+H adds Header +1 + * Ctrl+Enter send the form + */ if (e.ctrlKey) { switch (keyCode) { case 13: @@ -354,21 +357,27 @@ ta.addEventListener('keydown', function(e) { } catch(exception) { t.submit.click(); } + e.preventDefault(); break; case 66: // B editor_inline(e, "bold", false); + e.preventDefault(); break; case 72: // H editor_title(e, 0, +1); + e.preventDefault(); break; case 73: // I editor_inline(e, "italic", false); + e.preventDefault(); break; case 83: // S editor_inline(e, "strike", false); + e.preventDefault(); break; case 85: // U editor_inline(e, "underline", false); + e.preventDefault(); break; } } From 44609f2f9602925be7ec0a656389407c6586a416 Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 16 May 2023 13:50:47 +0200 Subject: [PATCH 75/92] editor: Toggle automatic preview and add manual preview button --- app/static/css/editor.css | 8 +++---- app/static/less/editor.less | 12 +++++------ app/static/scripts/editor.js | 36 ++++++++++++++++++++++++++++++- app/templates/widgets/editor.html | 17 +++++++++++++-- 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index 60383c4..aa9e410 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -2,7 +2,7 @@ display: flex; justify-content: space-between; flex-wrap: wrap; - align-items: flex-end; + align-items: center; } .editor .btn-group #filler { flex-grow: 1; @@ -42,8 +42,8 @@ min-height: 15rem; } .editor #editor_content_preview { - padding: 5px; - margin-top: 10px; + padding: 10px; + margin-top: 5px; border: var(--border); background-color: rgba(0,0,0,0.2); } @@ -79,4 +79,4 @@ transform: translateX(-50%); top: 50vh; } -} +} \ No newline at end of file diff --git a/app/static/less/editor.less b/app/static/less/editor.less index 774b91b..46d0a3b 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -5,9 +5,9 @@ display: flex; justify-content: space-between; flex-wrap: wrap; - align-items: flex-end; + align-items: center; - .filler { + #filler { flex-grow: 1; } button { @@ -39,12 +39,10 @@ margin: 0 0 8px 0; } - // From gitea .separator { display: inline-block; width: 0; - border-left: 1px solid #d9d9d9; - border-right: 1px solid #fff; + border: 1px solid var(--text); color: transparent; text-indent: -10px; } @@ -55,8 +53,8 @@ } #editor_content_preview { - padding: 5px; - margin-top: 10px; + padding: 10px; + margin-top: 5px; border: var(--border); background-color: rgba(0, 0, 0, 0.2); } diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 8b7a024..b0bd2e6 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -293,8 +293,34 @@ function editor_display_emoji_modal(event) { event.currentTarget.children[1].style = {'display': 'block'}; } +const DISABLE_PREVIEW_ICON = ''; +const ENABLE_PREVIEW_ICON = ''; + +function toggle_auto_preview() { + let auto_preview; + if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) { + auto_preview = document.cookie.split(";").some((item) => item.includes("auto-preview=true")); + } else { + auto_preview = true; + } + document.cookie = `auto-preview=${!auto_preview}; max-age=31536000; SameSite=Strict; Secure` + if (!auto_preview) { + document.getElementById("toggle_preview").title = "Désactiver la prévisualisation"; + document.getElementById("toggle_preview").innerHTML = DISABLE_PREVIEW_ICON; + document.getElementById("manual_preview").style = "display: none"; + } else { + document.getElementById("toggle_preview").title = "Activer la prévisualisation"; + document.getElementById("toggle_preview").innerHTML = ENABLE_PREVIEW_ICON; + document.getElementById("manual_preview").style = "display: block"; + } +} + /* This request the server to get a complete render of the current text in the textarea */ -function preview() { +function preview(manual=false) { + // If auto-preview is disabled and the preview is not manually requested by the user + if (document.cookie.split(";").some((item) => item.includes("auto-preview=false")) && !manual) { + return; + } const previewArea = document.querySelector("#editor_content_preview"); const textarea = document.querySelector(".editor textarea"); @@ -319,6 +345,14 @@ function preview() { }); } +if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) { + if (document.cookie.split(";").some((item) => item.includes("auto-preview=false"))) { + document.getElementById("toggle_preview").title = "Activer la prévisualisation"; + document.getElementById("toggle_preview").innerHTML = ENABLE_PREVIEW_ICON; + document.getElementById("manual_preview").style = "display: block"; + } +} + previewTimeout = null; ta = document.querySelector(".editor textarea"); diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 32bfba9..d04e085 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -155,6 +155,19 @@ -->
    + + Aide @@ -162,8 +175,8 @@ {{ field.label if label }} {{ field() }} -
    -
    + +
    {% for error in field.errors %} From 8fbec9ed87d402c2eb4caa2545733addaaf98cb7 Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 16 May 2023 22:23:08 +0200 Subject: [PATCH 76/92] editor: Fix input being cleared on click in link modal --- app/static/scripts/editor.js | 9 ++------- app/templates/widgets/editor.html | 5 ----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index b0bd2e6..13d9aa0 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -161,11 +161,6 @@ function editor_display_link_modal(event) { else if (selection != "") { event.currentTarget.querySelector("#link-desc-input").value = selection; } - // Or nothing selected - else { - event.currentTarget.querySelector("#link-desc-input").value = ""; - event.currentTarget.querySelector("#link-link-input").value = ""; - } event.currentTarget.children[1].style = {'display': 'block'}; } @@ -354,8 +349,8 @@ if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-previ } -previewTimeout = null; -ta = document.querySelector(".editor textarea"); +let previewTimeout = null; +let ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { // Tab insert some spaces let keyCode = e.keyCode || e.which; diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index d04e085..bb75b2e 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -182,10 +182,5 @@ {% for error in field.errors %} {{ error }} {% endfor %} - - - {% endmacro %} From 168b77c8dedc3bc1aa2302e054549b4b3e47f3f7 Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 16 May 2023 23:06:26 +0200 Subject: [PATCH 77/92] scripts: Fix variable declaration --- app/static/scripts/editor.js | 1 - app/static/scripts/trigger_menu.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 13d9aa0..930b223 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -348,7 +348,6 @@ if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-previ } } - let previewTimeout = null; let ta = document.querySelector(".editor textarea"); ta.addEventListener('keydown', function(e) { diff --git a/app/static/scripts/trigger_menu.js b/app/static/scripts/trigger_menu.js index 07fa7ce..b909046 100644 --- a/app/static/scripts/trigger_menu.js +++ b/app/static/scripts/trigger_menu.js @@ -46,7 +46,7 @@ let mouse_trigger = function(event) { if(!menu.contains(event.target)) { let active = -1; - for(i = 0; i < buttons.length; i++) { + for(let i = 0; i < buttons.length; i++) { if(buttons[i].contains(event.target)) active = i; buttons[i].querySelector('a').blur(); From 6cc066b4d6c8f15c962ba642b47ae3149f3ff0bd Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 16 May 2023 23:11:17 +0200 Subject: [PATCH 78/92] editor: Self-host the emoji-picker custom element. --- .../scripts/emoji-picker-element/data.json | 1 + .../scripts/emoji-picker-element/database.js | 972 ++++++ .../scripts/emoji-picker-element/i18n/ar.js | 34 + .../scripts/emoji-picker-element/i18n/de.js | 34 + .../scripts/emoji-picker-element/i18n/en.js | 34 + .../scripts/emoji-picker-element/i18n/es.js | 34 + .../scripts/emoji-picker-element/i18n/fr.js | 34 + .../scripts/emoji-picker-element/i18n/hi.js | 34 + .../scripts/emoji-picker-element/i18n/id.js | 34 + .../scripts/emoji-picker-element/i18n/it.js | 34 + .../emoji-picker-element/i18n/ms_MY.js | 34 + .../scripts/emoji-picker-element/i18n/nl.js | 34 + .../scripts/emoji-picker-element/i18n/pl.js | 34 + .../emoji-picker-element/i18n/pt_BR.js | 34 + .../emoji-picker-element/i18n/pt_PT.js | 34 + .../emoji-picker-element/i18n/ru_RU.js | 34 + .../scripts/emoji-picker-element/i18n/tr.js | 34 + .../emoji-picker-element/i18n/zh_CN.js | 27 + .../scripts/emoji-picker-element/index.js | 3 + .../scripts/emoji-picker-element/picker.js | 2704 +++++++++++++++++ .../scripts/emoji-picker-element/svelte.js | 2294 ++++++++++++++ app/templates/base/scripts.html | 2 +- app/templates/widgets/editor.html | 2 +- 23 files changed, 6513 insertions(+), 2 deletions(-) create mode 100644 app/static/scripts/emoji-picker-element/data.json create mode 100644 app/static/scripts/emoji-picker-element/database.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/ar.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/de.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/en.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/es.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/fr.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/hi.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/id.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/it.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/ms_MY.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/nl.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/pl.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/pt_BR.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/pt_PT.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/ru_RU.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/tr.js create mode 100644 app/static/scripts/emoji-picker-element/i18n/zh_CN.js create mode 100644 app/static/scripts/emoji-picker-element/index.js create mode 100644 app/static/scripts/emoji-picker-element/picker.js create mode 100644 app/static/scripts/emoji-picker-element/svelte.js diff --git a/app/static/scripts/emoji-picker-element/data.json b/app/static/scripts/emoji-picker-element/data.json new file mode 100644 index 0000000..1dab199 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/data.json @@ -0,0 +1 @@ +[{"shortcodes":["grinning","grinning_face"],"annotation":"grinning face","tags":["face","grin"],"emoji":"😀","order":1,"group":0,"version":1},{"shortcodes":["grinning_face_with_big_eyes","smiley"],"annotation":"grinning face with big eyes","tags":["face","mouth","open","smile"],"emoji":"😃","order":2,"group":0,"version":0.6},{"shortcodes":["grinning_face_with_closed_eyes","smile"],"annotation":"grinning face with smiling eyes","tags":["eye","face","mouth","open","smile"],"emoji":"😄","order":3,"group":0,"version":0.6,"emoticon":":D"},{"shortcodes":["beaming_face","grin"],"annotation":"beaming face with smiling eyes","tags":["eye","face","grin","smile"],"emoji":"😁","order":4,"group":0,"version":0.6},{"shortcodes":["laughing","lol","satisfied","squinting_face"],"annotation":"grinning squinting face","tags":["face","laugh","mouth","satisfied","smile"],"emoji":"😆","order":5,"group":0,"version":0.6,"emoticon":"XD"},{"shortcodes":["grinning_face_with_sweat","sweat_smile"],"annotation":"grinning face with sweat","tags":["cold","face","open","smile","sweat"],"emoji":"😅","order":6,"group":0,"version":0.6},{"shortcodes":["rofl"],"annotation":"rolling on the floor laughing","tags":["face","floor","laugh","rofl","rolling","rotfl"],"emoji":"🤣","order":7,"group":0,"version":3,"emoticon":":'D"},{"shortcodes":["joy","lmao","tears_of_joy"],"annotation":"face with tears of joy","tags":["face","joy","laugh","tear"],"emoji":"😂","order":8,"group":0,"version":0.6,"emoticon":":')"},{"shortcodes":["slightly_smiling_face"],"annotation":"slightly smiling face","tags":["face","smile"],"emoji":"🙂","order":9,"group":0,"version":1,"emoticon":":)"},{"shortcodes":["upside_down_face"],"annotation":"upside-down face","tags":["face","upside-down"],"emoji":"🙃","order":10,"group":0,"version":1},{"shortcodes":["melt","melting_face"],"annotation":"melting face","tags":["disappear","dissolve","liquid","melt"],"emoji":"🫠","order":11,"group":0,"version":14},{"shortcodes":["wink","winking_face"],"annotation":"winking face","tags":["face","wink"],"emoji":"😉","order":12,"group":0,"version":0.6,"emoticon":";)"},{"shortcodes":["blush","smiling_face_with_closed_eyes"],"annotation":"smiling face with smiling eyes","tags":["blush","eye","face","smile"],"emoji":"😊","order":13,"group":0,"version":0.6,"emoticon":":>"},{"shortcodes":["halo","innocent"],"annotation":"smiling face with halo","tags":["angel","face","fantasy","halo","innocent"],"emoji":"😇","order":14,"group":0,"version":1,"emoticon":"O:)"},{"shortcodes":["smiling_face_with_3_hearts"],"annotation":"smiling face with hearts","tags":["adore","crush","hearts","in love"],"emoji":"🥰","order":15,"group":0,"version":11},{"shortcodes":["heart_eyes","smiling_face_with_heart_eyes"],"annotation":"smiling face with heart-eyes","tags":["eye","face","love","smile"],"emoji":"😍","order":16,"group":0,"version":0.6},{"shortcodes":["star_struck"],"annotation":"star-struck","tags":["eyes","face","grinning","star"],"emoji":"🤩","order":17,"group":0,"version":5},{"shortcodes":["blowing_a_kiss","kissing_heart"],"annotation":"face blowing a kiss","tags":["face","kiss"],"emoji":"😘","order":18,"group":0,"version":0.6,"emoticon":":X"},{"shortcodes":["kissing","kissing_face"],"annotation":"kissing face","tags":["face","kiss"],"emoji":"😗","order":19,"group":0,"version":1},{"shortcodes":["relaxed","smiling_face"],"annotation":"smiling face","tags":["face","outlined","relaxed","smile"],"emoji":"☺️","order":21,"group":0,"version":0.6},{"shortcodes":["kissing_closed_eyes","kissing_face_with_closed_eyes"],"annotation":"kissing face with closed eyes","tags":["closed","eye","face","kiss"],"emoji":"😚","order":22,"group":0,"version":0.6,"emoticon":":*"},{"shortcodes":["kissing_face_with_smiling_eyes","kissing_smiling_eyes"],"annotation":"kissing face with smiling eyes","tags":["eye","face","kiss","smile"],"emoji":"😙","order":23,"group":0,"version":1},{"shortcodes":["smiling_face_with_tear"],"annotation":"smiling face with tear","tags":["grateful","proud","relieved","smiling","tear","touched"],"emoji":"🥲","order":24,"group":0,"version":13},{"shortcodes":["savoring_food","yum"],"annotation":"face savoring food","tags":["delicious","face","savouring","smile","yum"],"emoji":"😋","order":25,"group":0,"version":0.6},{"shortcodes":["face_with_tongue","stuck_out_tongue"],"annotation":"face with tongue","tags":["face","tongue"],"emoji":"😛","order":26,"group":0,"version":1,"emoticon":":P"},{"shortcodes":["stuck_out_tongue_winking_eye"],"annotation":"winking face with tongue","tags":["eye","face","joke","tongue","wink"],"emoji":"😜","order":27,"group":0,"version":0.6,"emoticon":";P"},{"shortcodes":["zany","zany_face"],"annotation":"zany face","tags":["eye","goofy","large","small"],"emoji":"🤪","order":28,"group":0,"version":5},{"shortcodes":["stuck_out_tongue_closed_eyes"],"annotation":"squinting face with tongue","tags":["eye","face","horrible","taste","tongue"],"emoji":"😝","order":29,"group":0,"version":0.6,"emoticon":"XP"},{"shortcodes":["money_mouth_face"],"annotation":"money-mouth face","tags":["face","money","mouth"],"emoji":"🤑","order":30,"group":0,"version":1},{"shortcodes":["hug","hugging","hugging_face"],"annotation":"smiling face with open hands","tags":["face","hug","hugging","open hands","smiling face"],"emoji":"🤗","order":31,"group":0,"version":1},{"shortcodes":["face_with_hand_over_mouth","hand_over_mouth"],"annotation":"face with hand over mouth","tags":["whoops"],"emoji":"🤭","order":32,"group":0,"version":5},{"shortcodes":["face_with_open_eyes_hand_over_mouth","gasp"],"annotation":"face with open eyes and hand over mouth","tags":["amazement","awe","disbelief","embarrass","scared","surprise"],"emoji":"🫢","order":33,"group":0,"version":14},{"shortcodes":["face_with_peeking_eye","peek"],"annotation":"face with peeking eye","tags":["captivated","peep","stare"],"emoji":"🫣","order":34,"group":0,"version":14},{"shortcodes":["shush","shushing_face"],"annotation":"shushing face","tags":["quiet","shush"],"emoji":"🤫","order":35,"group":0,"version":5},{"shortcodes":["thinking","thinking_face","wtf"],"annotation":"thinking face","tags":["face","thinking"],"emoji":"🤔","order":36,"group":0,"version":1,"emoticon":":L"},{"shortcodes":["salute","saluting_face"],"annotation":"saluting face","tags":["ok","salute","sunny","troops","yes"],"emoji":"🫡","order":37,"group":0,"version":14},{"shortcodes":["zipper_mouth","zipper_mouth_face"],"annotation":"zipper-mouth face","tags":["face","mouth","zipper"],"emoji":"🤐","order":38,"group":0,"version":1,"emoticon":":Z"},{"shortcodes":["face_with_raised_eyebrow","raised_eyebrow"],"annotation":"face with raised eyebrow","tags":["distrust","skeptic"],"emoji":"🤨","order":39,"group":0,"version":5},{"shortcodes":["neutral","neutral_face"],"annotation":"neutral face","tags":["deadpan","face","meh","neutral"],"emoji":"😐️","order":40,"group":0,"version":0.7,"emoticon":":|"},{"shortcodes":["expressionless","expressionless_face"],"annotation":"expressionless face","tags":["expressionless","face","inexpressive","meh","unexpressive"],"emoji":"😑","order":41,"group":0,"version":1},{"shortcodes":["no_mouth"],"annotation":"face without mouth","tags":["face","mouth","quiet","silent"],"emoji":"😶","order":42,"group":0,"version":1,"emoticon":":#"},{"shortcodes":["dotted_line_face"],"annotation":"dotted line face","tags":["depressed","disappear","hide","introvert","invisible"],"emoji":"🫥","order":43,"group":0,"version":14},{"shortcodes":["in_clouds"],"annotation":"face in clouds","tags":["absentminded","face in the fog","head in clouds"],"emoji":"😶‍🌫️","order":44,"group":0,"version":13.1},{"shortcodes":["smirk","smirking","smirking_face"],"annotation":"smirking face","tags":["face","smirk"],"emoji":"😏","order":46,"group":0,"version":0.6,"emoticon":":j"},{"shortcodes":["unamused","unamused_face"],"annotation":"unamused face","tags":["face","unamused","unhappy"],"emoji":"😒","order":47,"group":0,"version":0.6,"emoticon":":?"},{"shortcodes":["rolling_eyes"],"annotation":"face with rolling eyes","tags":["eyeroll","eyes","face","rolling"],"emoji":"🙄","order":48,"group":0,"version":1},{"shortcodes":["grimacing","grimacing_face"],"annotation":"grimacing face","tags":["face","grimace"],"emoji":"😬","order":49,"group":0,"version":1,"emoticon":"8D"},{"shortcodes":["exhale","exhaling"],"annotation":"face exhaling","tags":["exhale","gasp","groan","relief","whisper","whistle"],"emoji":"😮‍💨","order":50,"group":0,"version":13.1},{"shortcodes":["lying","lying_face"],"annotation":"lying face","tags":["face","lie","pinocchio"],"emoji":"🤥","order":51,"group":0,"version":3},{"shortcodes":["relieved","relieved_face"],"annotation":"relieved face","tags":["face","relieved"],"emoji":"😌","order":52,"group":0,"version":0.6},{"shortcodes":["pensive","pensive_face"],"annotation":"pensive face","tags":["dejected","face","pensive"],"emoji":"😔","order":53,"group":0,"version":0.6},{"shortcodes":["sleepy","sleepy_face"],"annotation":"sleepy face","tags":["face","good night","sleep"],"emoji":"😪","order":54,"group":0,"version":0.6},{"shortcodes":["drooling","drooling_face"],"annotation":"drooling face","tags":["drooling","face"],"emoji":"🤤","order":55,"group":0,"version":3},{"shortcodes":["sleeping","sleeping_face"],"annotation":"sleeping face","tags":["face","good night","sleep","zzz"],"emoji":"😴","order":56,"group":0,"version":1},{"shortcodes":["mask","medical_mask"],"annotation":"face with medical mask","tags":["cold","doctor","face","mask","sick"],"emoji":"😷","order":57,"group":0,"version":0.6},{"shortcodes":["face_with_thermometer"],"annotation":"face with thermometer","tags":["face","ill","sick","thermometer"],"emoji":"🤒","order":58,"group":0,"version":1},{"shortcodes":["face_with_head_bandage"],"annotation":"face with head-bandage","tags":["bandage","face","hurt","injury"],"emoji":"🤕","order":59,"group":0,"version":1},{"shortcodes":["nauseated","nauseated_face"],"annotation":"nauseated face","tags":["face","nauseated","vomit"],"emoji":"🤢","order":60,"group":0,"version":3,"emoticon":"%("},{"shortcodes":["face_vomiting","vomiting"],"annotation":"face vomiting","tags":["puke","sick","vomit"],"emoji":"🤮","order":61,"group":0,"version":5},{"shortcodes":["sneezing","sneezing_face"],"annotation":"sneezing face","tags":["face","gesundheit","sneeze"],"emoji":"🤧","order":62,"group":0,"version":3},{"shortcodes":["hot","hot_face"],"annotation":"hot face","tags":["feverish","heat stroke","hot","red-faced","sweating"],"emoji":"🥵","order":63,"group":0,"version":11},{"shortcodes":["cold","cold_face"],"annotation":"cold face","tags":["blue-faced","cold","freezing","frostbite","icicles"],"emoji":"🥶","order":64,"group":0,"version":11},{"shortcodes":["woozy","woozy_face"],"annotation":"woozy face","tags":["dizzy","intoxicated","tipsy","uneven eyes","wavy mouth"],"emoji":"🥴","order":65,"group":0,"version":11,"emoticon":":&"},{"shortcodes":["dizzy_face","knocked_out"],"annotation":"face with crossed-out eyes","tags":["crossed-out eyes","dead","face","knocked out"],"emoji":"😵","order":66,"group":0,"version":0.6,"emoticon":"XO"},{"shortcodes":["dizzy_eyes"],"annotation":"face with spiral eyes","tags":["dizzy","hypnotized","spiral","trouble","whoa"],"emoji":"😵‍💫","order":67,"group":0,"version":13.1},{"shortcodes":["exploding_head"],"annotation":"exploding head","tags":["mind blown","shocked"],"emoji":"🤯","order":68,"group":0,"version":5},{"shortcodes":["cowboy","cowboy_face"],"annotation":"cowboy hat face","tags":["cowboy","cowgirl","face","hat"],"emoji":"🤠","order":69,"group":0,"version":3},{"shortcodes":["hooray","partying","partying_face"],"annotation":"partying face","tags":["celebration","hat","horn","party"],"emoji":"🥳","order":70,"group":0,"version":11},{"shortcodes":["disguised","disguised_face"],"annotation":"disguised face","tags":["disguise","face","glasses","incognito","nose"],"emoji":"🥸","order":71,"group":0,"version":13},{"shortcodes":["smiling_face_with_sunglasses","sunglasses_cool","too_cool"],"annotation":"smiling face with sunglasses","tags":["bright","cool","face","sun","sunglasses"],"emoji":"😎","order":72,"group":0,"version":1,"emoticon":"8)"},{"shortcodes":["nerd","nerd_face"],"annotation":"nerd face","tags":["face","geek","nerd"],"emoji":"🤓","order":73,"group":0,"version":1,"emoticon":":B"},{"shortcodes":["face_with_monocle"],"annotation":"face with monocle","tags":["face","monocle","stuffy"],"emoji":"🧐","order":74,"group":0,"version":5},{"shortcodes":["confused","confused_face"],"annotation":"confused face","tags":["confused","face","meh"],"emoji":"😕","order":75,"group":0,"version":1,"emoticon":":/"},{"shortcodes":["face_with_diagonal_mouth"],"annotation":"face with diagonal mouth","tags":["disappointed","meh","skeptical","unsure"],"emoji":"🫤","order":76,"group":0,"version":14},{"shortcodes":["worried","worried_face"],"annotation":"worried face","tags":["face","worried"],"emoji":"😟","order":77,"group":0,"version":1},{"shortcodes":["slightly_frowning_face"],"annotation":"slightly frowning face","tags":["face","frown"],"emoji":"🙁","order":78,"group":0,"version":1},{"shortcodes":["white_frowning_face"],"annotation":"frowning face","tags":["face","frown"],"emoji":"☹️","order":80,"group":0,"version":0.7,"emoticon":":("},{"shortcodes":["face_with_open_mouth","open_mouth"],"annotation":"face with open mouth","tags":["face","mouth","open","sympathy"],"emoji":"😮","order":81,"group":0,"version":1},{"shortcodes":["hushed","hushed_face"],"annotation":"hushed face","tags":["face","hushed","stunned","surprised"],"emoji":"😯","order":82,"group":0,"version":1},{"shortcodes":["astonished","astonished_face"],"annotation":"astonished face","tags":["astonished","face","shocked","totally"],"emoji":"😲","order":83,"group":0,"version":0.6,"emoticon":":O"},{"shortcodes":["flushed","flushed_face"],"annotation":"flushed face","tags":["dazed","face","flushed"],"emoji":"😳","order":84,"group":0,"version":0.6,"emoticon":":$"},{"shortcodes":["pleading","pleading_face"],"annotation":"pleading face","tags":["begging","mercy","puppy eyes"],"emoji":"🥺","order":85,"group":0,"version":11},{"shortcodes":["face_holding_back_tears","watery_eyes"],"annotation":"face holding back tears","tags":["angry","cry","proud","resist","sad"],"emoji":"🥹","order":86,"group":0,"version":14},{"shortcodes":["frowning","frowning_face"],"annotation":"frowning face with open mouth","tags":["face","frown","mouth","open"],"emoji":"😦","order":87,"group":0,"version":1},{"shortcodes":["anguished","anguished_face"],"annotation":"anguished face","tags":["anguished","face"],"emoji":"😧","order":88,"group":0,"version":1,"emoticon":":S"},{"shortcodes":["fearful","fearful_face"],"annotation":"fearful face","tags":["face","fear","fearful","scared"],"emoji":"😨","order":89,"group":0,"version":0.6},{"shortcodes":["anxious","anxious_face","cold_sweat"],"annotation":"anxious face with sweat","tags":["blue","cold","face","rushed","sweat"],"emoji":"😰","order":90,"group":0,"version":0.6},{"shortcodes":["disappointed_relieved","sad_relieved_face"],"annotation":"sad but relieved face","tags":["disappointed","face","relieved","whew"],"emoji":"😥","order":91,"group":0,"version":0.6},{"shortcodes":["cry","crying_face"],"annotation":"crying face","tags":["cry","face","sad","tear"],"emoji":"😢","order":92,"group":0,"version":0.6,"emoticon":":'("},{"shortcodes":["loudly_crying_face","sob"],"annotation":"loudly crying face","tags":["cry","face","sad","sob","tear"],"emoji":"😭","order":93,"group":0,"version":0.6,"emoticon":":'o"},{"shortcodes":["scream","screaming_in_fear"],"annotation":"face screaming in fear","tags":["face","fear","munch","scared","scream"],"emoji":"😱","order":94,"group":0,"version":0.6,"emoticon":"Dx"},{"shortcodes":["confounded","confounded_face"],"annotation":"confounded face","tags":["confounded","face"],"emoji":"😖","order":95,"group":0,"version":0.6,"emoticon":"X("},{"shortcodes":["persevere","persevering_face"],"annotation":"persevering face","tags":["face","persevere"],"emoji":"😣","order":96,"group":0,"version":0.6},{"shortcodes":["disappointed","disappointed_face"],"annotation":"disappointed face","tags":["disappointed","face"],"emoji":"😞","order":97,"group":0,"version":0.6},{"shortcodes":["downcast_face","sweat"],"annotation":"downcast face with sweat","tags":["cold","face","sweat"],"emoji":"😓","order":98,"group":0,"version":0.6,"emoticon":":<"},{"shortcodes":["weary","weary_face"],"annotation":"weary face","tags":["face","tired","weary"],"emoji":"😩","order":99,"group":0,"version":0.6,"emoticon":"D:"},{"shortcodes":["tired","tired_face"],"annotation":"tired face","tags":["face","tired"],"emoji":"😫","order":100,"group":0,"version":0.6,"emoticon":":C"},{"shortcodes":["yawn","yawning","yawning_face"],"annotation":"yawning face","tags":["bored","tired","yawn"],"emoji":"🥱","order":101,"group":0,"version":12},{"shortcodes":["nose_steam","triumph"],"annotation":"face with steam from nose","tags":["face","triumph","won"],"emoji":"😤","order":102,"group":0,"version":0.6},{"shortcodes":["pout","pouting_face","rage"],"annotation":"enraged face","tags":["angry","enraged","face","mad","pouting","rage","red"],"emoji":"😡","order":103,"group":0,"version":0.6,"emoticon":">:/"},{"shortcodes":["angry","angry_face"],"annotation":"angry face","tags":["anger","angry","face","mad"],"emoji":"😠","order":104,"group":0,"version":0.6},{"shortcodes":["censored","face_with_symbols_on_mouth"],"annotation":"face with symbols on mouth","tags":["swearing"],"emoji":"🤬","order":105,"group":0,"version":5,"emoticon":":@"},{"shortcodes":["smiling_imp"],"annotation":"smiling face with horns","tags":["face","fairy tale","fantasy","horns","smile"],"emoji":"😈","order":106,"group":0,"version":1,"emoticon":">:)"},{"shortcodes":["angry_imp","imp"],"annotation":"angry face with horns","tags":["demon","devil","face","fantasy","imp"],"emoji":"👿","order":107,"group":0,"version":0.6,"emoticon":">:("},{"shortcodes":["skull"],"annotation":"skull","tags":["death","face","fairy tale","monster"],"emoji":"💀","order":108,"group":0,"version":0.6},{"shortcodes":["skull_and_crossbones"],"annotation":"skull and crossbones","tags":["crossbones","death","face","monster","skull"],"emoji":"☠️","order":110,"group":0,"version":1},{"shortcodes":["poop","shit"],"annotation":"pile of poo","tags":["dung","face","monster","poo","poop"],"emoji":"💩","order":111,"group":0,"version":0.6},{"shortcodes":["clown","clown_face"],"annotation":"clown face","tags":["clown","face"],"emoji":"🤡","order":112,"group":0,"version":3},{"shortcodes":["japanese_ogre","ogre"],"annotation":"ogre","tags":["creature","face","fairy tale","fantasy","monster"],"emoji":"👹","order":113,"group":0,"version":0.6,"emoticon":">0)"},{"shortcodes":["goblin","japanese_goblin"],"annotation":"goblin","tags":["creature","face","fairy tale","fantasy","monster"],"emoji":"👺","order":114,"group":0,"version":0.6},{"shortcodes":["ghost"],"annotation":"ghost","tags":["creature","face","fairy tale","fantasy","monster"],"emoji":"👻","order":115,"group":0,"version":0.6},{"shortcodes":["alien"],"annotation":"alien","tags":["creature","extraterrestrial","face","fantasy","ufo"],"emoji":"👽️","order":116,"group":0,"version":0.6},{"shortcodes":["alien_monster","space_invader"],"annotation":"alien monster","tags":["alien","creature","extraterrestrial","face","monster","ufo"],"emoji":"👾","order":117,"group":0,"version":0.6},{"shortcodes":["robot","robot_face"],"annotation":"robot","tags":["face","monster"],"emoji":"🤖","order":118,"group":0,"version":1},{"shortcodes":["grinning_cat","smiley_cat"],"annotation":"grinning cat","tags":["cat","face","grinning","mouth","open","smile"],"emoji":"😺","order":119,"group":0,"version":0.6},{"shortcodes":["grinning_cat_with_closed_eyes","smile_cat"],"annotation":"grinning cat with smiling eyes","tags":["cat","eye","face","grin","smile"],"emoji":"😸","order":120,"group":0,"version":0.6},{"shortcodes":["joy_cat","tears_of_joy_cat"],"annotation":"cat with tears of joy","tags":["cat","face","joy","tear"],"emoji":"😹","order":121,"group":0,"version":0.6},{"shortcodes":["heart_eyes_cat","smiling_cat_with_heart_eyes"],"annotation":"smiling cat with heart-eyes","tags":["cat","eye","face","heart","love","smile"],"emoji":"😻","order":122,"group":0,"version":0.6},{"shortcodes":["smirk_cat","wry_smile_cat"],"annotation":"cat with wry smile","tags":["cat","face","ironic","smile","wry"],"emoji":"😼","order":123,"group":0,"version":0.6},{"shortcodes":["kissing_cat"],"annotation":"kissing cat","tags":["cat","eye","face","kiss"],"emoji":"😽","order":124,"group":0,"version":0.6,"emoticon":":3"},{"shortcodes":["scream_cat","weary_cat"],"annotation":"weary cat","tags":["cat","face","oh","surprised","weary"],"emoji":"🙀","order":125,"group":0,"version":0.6},{"shortcodes":["crying_cat"],"annotation":"crying cat","tags":["cat","cry","face","sad","tear"],"emoji":"😿","order":126,"group":0,"version":0.6},{"shortcodes":["pouting_cat"],"annotation":"pouting cat","tags":["cat","face","pouting"],"emoji":"😾","order":127,"group":0,"version":0.6},{"shortcodes":["see_no_evil"],"annotation":"see-no-evil monkey","tags":["evil","face","forbidden","monkey","see"],"emoji":"🙈","order":128,"group":0,"version":0.6},{"shortcodes":["hear_no_evil"],"annotation":"hear-no-evil monkey","tags":["evil","face","forbidden","hear","monkey"],"emoji":"🙉","order":129,"group":0,"version":0.6},{"shortcodes":["speak_no_evil"],"annotation":"speak-no-evil monkey","tags":["evil","face","forbidden","monkey","speak"],"emoji":"🙊","order":130,"group":0,"version":0.6},{"shortcodes":["kiss"],"annotation":"kiss mark","tags":["kiss","lips"],"emoji":"💋","order":131,"group":0,"version":0.6},{"shortcodes":["love_letter"],"annotation":"love letter","tags":["heart","letter","love","mail"],"emoji":"💌","order":132,"group":0,"version":0.6},{"shortcodes":["cupid","heart_with_arrow"],"annotation":"heart with arrow","tags":["arrow","cupid"],"emoji":"💘","order":133,"group":0,"version":0.6},{"shortcodes":["gift_heart","heart_with_ribbon"],"annotation":"heart with ribbon","tags":["ribbon","valentine"],"emoji":"💝","order":134,"group":0,"version":0.6},{"shortcodes":["sparkling_heart"],"annotation":"sparkling heart","tags":["excited","sparkle"],"emoji":"💖","order":135,"group":0,"version":0.6},{"shortcodes":["growing_heart","heartpulse"],"annotation":"growing heart","tags":["excited","growing","nervous","pulse"],"emoji":"💗","order":136,"group":0,"version":0.6},{"shortcodes":["beating_heart","heartbeat"],"annotation":"beating heart","tags":["beating","heartbeat","pulsating"],"emoji":"💓","order":137,"group":0,"version":0.6},{"shortcodes":["revolving_hearts"],"annotation":"revolving hearts","tags":["revolving"],"emoji":"💞","order":138,"group":0,"version":0.6},{"shortcodes":["two_hearts"],"annotation":"two hearts","tags":["love"],"emoji":"💕","order":139,"group":0,"version":0.6},{"shortcodes":["heart_decoration"],"annotation":"heart decoration","tags":["heart"],"emoji":"💟","order":140,"group":0,"version":0.6},{"shortcodes":["heart_exclamation"],"annotation":"heart exclamation","tags":["exclamation","mark","punctuation"],"emoji":"❣️","order":142,"group":0,"version":1},{"shortcodes":["broken_heart"],"annotation":"broken heart","tags":["break","broken"],"emoji":"💔","order":143,"group":0,"version":0.6,"emoticon":"","skins":[{"emoji":"🧙🏻‍♂️","version":5,"tone":1},{"emoji":"🧙🏼‍♂️","version":5,"tone":2},{"emoji":"🧙🏽‍♂️","version":5,"tone":3},{"emoji":"🧙🏾‍♂️","version":5,"tone":4},{"emoji":"🧙🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_mage"],"annotation":"woman mage","tags":["sorceress","witch"],"emoji":"🧙‍♀️","order":1726,"group":1,"version":5,"skins":[{"emoji":"🧙🏻‍♀️","version":5,"tone":1},{"emoji":"🧙🏼‍♀️","version":5,"tone":2},{"emoji":"🧙🏽‍♀️","version":5,"tone":3},{"emoji":"🧙🏾‍♀️","version":5,"tone":4},{"emoji":"🧙🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["fairy"],"annotation":"fairy","tags":["oberon","puck","titania"],"emoji":"🧚","order":1738,"group":1,"version":5,"skins":[{"emoji":"🧚🏻","version":5,"tone":1},{"emoji":"🧚🏼","version":5,"tone":2},{"emoji":"🧚🏽","version":5,"tone":3},{"emoji":"🧚🏾","version":5,"tone":4},{"emoji":"🧚🏿","version":5,"tone":5}]},{"shortcodes":["man_fairy"],"annotation":"man fairy","tags":["oberon","puck"],"emoji":"🧚‍♂️","order":1744,"group":1,"version":5,"skins":[{"emoji":"🧚🏻‍♂️","version":5,"tone":1},{"emoji":"🧚🏼‍♂️","version":5,"tone":2},{"emoji":"🧚🏽‍♂️","version":5,"tone":3},{"emoji":"🧚🏾‍♂️","version":5,"tone":4},{"emoji":"🧚🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_fairy"],"annotation":"woman fairy","tags":["titania"],"emoji":"🧚‍♀️","order":1756,"group":1,"version":5,"skins":[{"emoji":"🧚🏻‍♀️","version":5,"tone":1},{"emoji":"🧚🏼‍♀️","version":5,"tone":2},{"emoji":"🧚🏽‍♀️","version":5,"tone":3},{"emoji":"🧚🏾‍♀️","version":5,"tone":4},{"emoji":"🧚🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["vampire"],"annotation":"vampire","tags":["dracula","undead"],"emoji":"🧛","order":1768,"group":1,"version":5,"emoticon":":E","skins":[{"emoji":"🧛🏻","version":5,"tone":1},{"emoji":"🧛🏼","version":5,"tone":2},{"emoji":"🧛🏽","version":5,"tone":3},{"emoji":"🧛🏾","version":5,"tone":4},{"emoji":"🧛🏿","version":5,"tone":5}]},{"shortcodes":["man_vampire"],"annotation":"man vampire","tags":["dracula","undead"],"emoji":"🧛‍♂️","order":1774,"group":1,"version":5,"skins":[{"emoji":"🧛🏻‍♂️","version":5,"tone":1},{"emoji":"🧛🏼‍♂️","version":5,"tone":2},{"emoji":"🧛🏽‍♂️","version":5,"tone":3},{"emoji":"🧛🏾‍♂️","version":5,"tone":4},{"emoji":"🧛🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_vampire"],"annotation":"woman vampire","tags":["undead"],"emoji":"🧛‍♀️","order":1786,"group":1,"version":5,"skins":[{"emoji":"🧛🏻‍♀️","version":5,"tone":1},{"emoji":"🧛🏼‍♀️","version":5,"tone":2},{"emoji":"🧛🏽‍♀️","version":5,"tone":3},{"emoji":"🧛🏾‍♀️","version":5,"tone":4},{"emoji":"🧛🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["merperson"],"annotation":"merperson","tags":["mermaid","merman","merwoman"],"emoji":"🧜","order":1798,"group":1,"version":5,"skins":[{"emoji":"🧜🏻","version":5,"tone":1},{"emoji":"🧜🏼","version":5,"tone":2},{"emoji":"🧜🏽","version":5,"tone":3},{"emoji":"🧜🏾","version":5,"tone":4},{"emoji":"🧜🏿","version":5,"tone":5}]},{"shortcodes":["merman"],"annotation":"merman","tags":["triton"],"emoji":"🧜‍♂️","order":1804,"group":1,"version":5,"skins":[{"emoji":"🧜🏻‍♂️","version":5,"tone":1},{"emoji":"🧜🏼‍♂️","version":5,"tone":2},{"emoji":"🧜🏽‍♂️","version":5,"tone":3},{"emoji":"🧜🏾‍♂️","version":5,"tone":4},{"emoji":"🧜🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["mermaid"],"annotation":"mermaid","tags":["merwoman"],"emoji":"🧜‍♀️","order":1816,"group":1,"version":5,"skins":[{"emoji":"🧜🏻‍♀️","version":5,"tone":1},{"emoji":"🧜🏼‍♀️","version":5,"tone":2},{"emoji":"🧜🏽‍♀️","version":5,"tone":3},{"emoji":"🧜🏾‍♀️","version":5,"tone":4},{"emoji":"🧜🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["elf"],"annotation":"elf","tags":["magical"],"emoji":"🧝","order":1828,"group":1,"version":5,"skins":[{"emoji":"🧝🏻","version":5,"tone":1},{"emoji":"🧝🏼","version":5,"tone":2},{"emoji":"🧝🏽","version":5,"tone":3},{"emoji":"🧝🏾","version":5,"tone":4},{"emoji":"🧝🏿","version":5,"tone":5}]},{"shortcodes":["man_elf"],"annotation":"man elf","tags":["magical"],"emoji":"🧝‍♂️","order":1834,"group":1,"version":5,"skins":[{"emoji":"🧝🏻‍♂️","version":5,"tone":1},{"emoji":"🧝🏼‍♂️","version":5,"tone":2},{"emoji":"🧝🏽‍♂️","version":5,"tone":3},{"emoji":"🧝🏾‍♂️","version":5,"tone":4},{"emoji":"🧝🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_elf"],"annotation":"woman elf","tags":["magical"],"emoji":"🧝‍♀️","order":1846,"group":1,"version":5,"skins":[{"emoji":"🧝🏻‍♀️","version":5,"tone":1},{"emoji":"🧝🏼‍♀️","version":5,"tone":2},{"emoji":"🧝🏽‍♀️","version":5,"tone":3},{"emoji":"🧝🏾‍♀️","version":5,"tone":4},{"emoji":"🧝🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["genie"],"annotation":"genie","tags":["djinn"],"emoji":"🧞","order":1858,"group":1,"version":5},{"shortcodes":["man_genie"],"annotation":"man genie","tags":["djinn"],"emoji":"🧞‍♂️","order":1859,"group":1,"version":5},{"shortcodes":["woman_genie"],"annotation":"woman genie","tags":["djinn"],"emoji":"🧞‍♀️","order":1861,"group":1,"version":5},{"shortcodes":["zombie"],"annotation":"zombie","tags":["undead","walking dead"],"emoji":"🧟","order":1863,"group":1,"version":5,"emoticon":"8#"},{"shortcodes":["man_zombie"],"annotation":"man zombie","tags":["undead","walking dead"],"emoji":"🧟‍♂️","order":1864,"group":1,"version":5},{"shortcodes":["woman_zombie"],"annotation":"woman zombie","tags":["undead","walking dead"],"emoji":"🧟‍♀️","order":1866,"group":1,"version":5},{"shortcodes":["troll"],"annotation":"troll","tags":["fairy tale","fantasy","monster"],"emoji":"🧌","order":1868,"group":1,"version":14},{"shortcodes":["massage","person_getting_massage"],"annotation":"person getting massage","tags":["face","massage","salon"],"emoji":"💆","order":1869,"group":1,"version":0.6,"skins":[{"emoji":"💆🏻","version":1,"tone":1},{"emoji":"💆🏼","version":1,"tone":2},{"emoji":"💆🏽","version":1,"tone":3},{"emoji":"💆🏾","version":1,"tone":4},{"emoji":"💆🏿","version":1,"tone":5}]},{"shortcodes":["man_getting_massage"],"annotation":"man getting massage","tags":["face","man","massage"],"emoji":"💆‍♂️","order":1875,"group":1,"version":4,"skins":[{"emoji":"💆🏻‍♂️","version":4,"tone":1},{"emoji":"💆🏼‍♂️","version":4,"tone":2},{"emoji":"💆🏽‍♂️","version":4,"tone":3},{"emoji":"💆🏾‍♂️","version":4,"tone":4},{"emoji":"💆🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_getting_massage"],"annotation":"woman getting massage","tags":["face","massage","woman"],"emoji":"💆‍♀️","order":1887,"group":1,"version":4,"skins":[{"emoji":"💆🏻‍♀️","version":4,"tone":1},{"emoji":"💆🏼‍♀️","version":4,"tone":2},{"emoji":"💆🏽‍♀️","version":4,"tone":3},{"emoji":"💆🏾‍♀️","version":4,"tone":4},{"emoji":"💆🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["haircut","person_getting_haircut"],"annotation":"person getting haircut","tags":["barber","beauty","haircut","parlor"],"emoji":"💇","order":1899,"group":1,"version":0.6,"skins":[{"emoji":"💇🏻","version":1,"tone":1},{"emoji":"💇🏼","version":1,"tone":2},{"emoji":"💇🏽","version":1,"tone":3},{"emoji":"💇🏾","version":1,"tone":4},{"emoji":"💇🏿","version":1,"tone":5}]},{"shortcodes":["man_getting_haircut"],"annotation":"man getting haircut","tags":["haircut","man"],"emoji":"💇‍♂️","order":1905,"group":1,"version":4,"skins":[{"emoji":"💇🏻‍♂️","version":4,"tone":1},{"emoji":"💇🏼‍♂️","version":4,"tone":2},{"emoji":"💇🏽‍♂️","version":4,"tone":3},{"emoji":"💇🏾‍♂️","version":4,"tone":4},{"emoji":"💇🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_getting_haircut"],"annotation":"woman getting haircut","tags":["haircut","woman"],"emoji":"💇‍♀️","order":1917,"group":1,"version":4,"skins":[{"emoji":"💇🏻‍♀️","version":4,"tone":1},{"emoji":"💇🏼‍♀️","version":4,"tone":2},{"emoji":"💇🏽‍♀️","version":4,"tone":3},{"emoji":"💇🏾‍♀️","version":4,"tone":4},{"emoji":"💇🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_walking","walking"],"annotation":"person walking","tags":["hike","walk","walking"],"emoji":"🚶","order":1929,"group":1,"version":0.6,"skins":[{"emoji":"🚶🏻","version":1,"tone":1},{"emoji":"🚶🏼","version":1,"tone":2},{"emoji":"🚶🏽","version":1,"tone":3},{"emoji":"🚶🏾","version":1,"tone":4},{"emoji":"🚶🏿","version":1,"tone":5}]},{"shortcodes":["man_walking"],"annotation":"man walking","tags":["hike","man","walk"],"emoji":"🚶‍♂️","order":1935,"group":1,"version":4,"skins":[{"emoji":"🚶🏻‍♂️","version":4,"tone":1},{"emoji":"🚶🏼‍♂️","version":4,"tone":2},{"emoji":"🚶🏽‍♂️","version":4,"tone":3},{"emoji":"🚶🏾‍♂️","version":4,"tone":4},{"emoji":"🚶🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_walking"],"annotation":"woman walking","tags":["hike","walk","woman"],"emoji":"🚶‍♀️","order":1947,"group":1,"version":4,"skins":[{"emoji":"🚶🏻‍♀️","version":4,"tone":1},{"emoji":"🚶🏼‍♀️","version":4,"tone":2},{"emoji":"🚶🏽‍♀️","version":4,"tone":3},{"emoji":"🚶🏾‍♀️","version":4,"tone":4},{"emoji":"🚶🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_standing","standing"],"annotation":"person standing","tags":["stand","standing"],"emoji":"🧍","order":1959,"group":1,"version":12,"skins":[{"emoji":"🧍🏻","version":12,"tone":1},{"emoji":"🧍🏼","version":12,"tone":2},{"emoji":"🧍🏽","version":12,"tone":3},{"emoji":"🧍🏾","version":12,"tone":4},{"emoji":"🧍🏿","version":12,"tone":5}]},{"shortcodes":["man_standing"],"annotation":"man standing","tags":["man","standing"],"emoji":"🧍‍♂️","order":1965,"group":1,"version":12,"skins":[{"emoji":"🧍🏻‍♂️","version":12,"tone":1},{"emoji":"🧍🏼‍♂️","version":12,"tone":2},{"emoji":"🧍🏽‍♂️","version":12,"tone":3},{"emoji":"🧍🏾‍♂️","version":12,"tone":4},{"emoji":"🧍🏿‍♂️","version":12,"tone":5}]},{"shortcodes":["woman_standing"],"annotation":"woman standing","tags":["standing","woman"],"emoji":"🧍‍♀️","order":1977,"group":1,"version":12,"skins":[{"emoji":"🧍🏻‍♀️","version":12,"tone":1},{"emoji":"🧍🏼‍♀️","version":12,"tone":2},{"emoji":"🧍🏽‍♀️","version":12,"tone":3},{"emoji":"🧍🏾‍♀️","version":12,"tone":4},{"emoji":"🧍🏿‍♀️","version":12,"tone":5}]},{"shortcodes":["kneeling","person_kneeling"],"annotation":"person kneeling","tags":["kneel","kneeling"],"emoji":"🧎","order":1989,"group":1,"version":12,"skins":[{"emoji":"🧎🏻","version":12,"tone":1},{"emoji":"🧎🏼","version":12,"tone":2},{"emoji":"🧎🏽","version":12,"tone":3},{"emoji":"🧎🏾","version":12,"tone":4},{"emoji":"🧎🏿","version":12,"tone":5}]},{"shortcodes":["man_kneeling"],"annotation":"man kneeling","tags":["kneeling","man"],"emoji":"🧎‍♂️","order":1995,"group":1,"version":12,"skins":[{"emoji":"🧎🏻‍♂️","version":12,"tone":1},{"emoji":"🧎🏼‍♂️","version":12,"tone":2},{"emoji":"🧎🏽‍♂️","version":12,"tone":3},{"emoji":"🧎🏾‍♂️","version":12,"tone":4},{"emoji":"🧎🏿‍♂️","version":12,"tone":5}]},{"shortcodes":["woman_kneeling"],"annotation":"woman kneeling","tags":["kneeling","woman"],"emoji":"🧎‍♀️","order":2007,"group":1,"version":12,"skins":[{"emoji":"🧎🏻‍♀️","version":12,"tone":1},{"emoji":"🧎🏼‍♀️","version":12,"tone":2},{"emoji":"🧎🏽‍♀️","version":12,"tone":3},{"emoji":"🧎🏾‍♀️","version":12,"tone":4},{"emoji":"🧎🏿‍♀️","version":12,"tone":5}]},{"shortcodes":["person_with_probing_cane","person_with_white_cane"],"annotation":"person with white cane","tags":["accessibility","blind"],"emoji":"🧑‍🦯","order":2019,"group":1,"version":12.1,"skins":[{"emoji":"🧑🏻‍🦯","version":12.1,"tone":1},{"emoji":"🧑🏼‍🦯","version":12.1,"tone":2},{"emoji":"🧑🏽‍🦯","version":12.1,"tone":3},{"emoji":"🧑🏾‍🦯","version":12.1,"tone":4},{"emoji":"🧑🏿‍🦯","version":12.1,"tone":5}]},{"shortcodes":["man_with_probing_cane","man_with_white_cane"],"annotation":"man with white cane","tags":["accessibility","blind","man"],"emoji":"👨‍🦯","order":2025,"group":1,"version":12,"skins":[{"emoji":"👨🏻‍🦯","version":12,"tone":1},{"emoji":"👨🏼‍🦯","version":12,"tone":2},{"emoji":"👨🏽‍🦯","version":12,"tone":3},{"emoji":"👨🏾‍🦯","version":12,"tone":4},{"emoji":"👨🏿‍🦯","version":12,"tone":5}]},{"shortcodes":["woman_with_probing_cane","woman_with_white_cane"],"annotation":"woman with white cane","tags":["accessibility","blind","woman"],"emoji":"👩‍🦯","order":2031,"group":1,"version":12,"skins":[{"emoji":"👩🏻‍🦯","version":12,"tone":1},{"emoji":"👩🏼‍🦯","version":12,"tone":2},{"emoji":"👩🏽‍🦯","version":12,"tone":3},{"emoji":"👩🏾‍🦯","version":12,"tone":4},{"emoji":"👩🏿‍🦯","version":12,"tone":5}]},{"shortcodes":["person_in_motorized_wheelchair"],"annotation":"person in motorized wheelchair","tags":["accessibility","wheelchair"],"emoji":"🧑‍🦼","order":2037,"group":1,"version":12.1,"skins":[{"emoji":"🧑🏻‍🦼","version":12.1,"tone":1},{"emoji":"🧑🏼‍🦼","version":12.1,"tone":2},{"emoji":"🧑🏽‍🦼","version":12.1,"tone":3},{"emoji":"🧑🏾‍🦼","version":12.1,"tone":4},{"emoji":"🧑🏿‍🦼","version":12.1,"tone":5}]},{"shortcodes":["man_in_motorized_wheelchair"],"annotation":"man in motorized wheelchair","tags":["accessibility","man","wheelchair"],"emoji":"👨‍🦼","order":2043,"group":1,"version":12,"skins":[{"emoji":"👨🏻‍🦼","version":12,"tone":1},{"emoji":"👨🏼‍🦼","version":12,"tone":2},{"emoji":"👨🏽‍🦼","version":12,"tone":3},{"emoji":"👨🏾‍🦼","version":12,"tone":4},{"emoji":"👨🏿‍🦼","version":12,"tone":5}]},{"shortcodes":["woman_in_motorized_wheelchair"],"annotation":"woman in motorized wheelchair","tags":["accessibility","wheelchair","woman"],"emoji":"👩‍🦼","order":2049,"group":1,"version":12,"skins":[{"emoji":"👩🏻‍🦼","version":12,"tone":1},{"emoji":"👩🏼‍🦼","version":12,"tone":2},{"emoji":"👩🏽‍🦼","version":12,"tone":3},{"emoji":"👩🏾‍🦼","version":12,"tone":4},{"emoji":"👩🏿‍🦼","version":12,"tone":5}]},{"shortcodes":["person_in_manual_wheelchair"],"annotation":"person in manual wheelchair","tags":["accessibility","wheelchair"],"emoji":"🧑‍🦽","order":2055,"group":1,"version":12.1,"skins":[{"emoji":"🧑🏻‍🦽","version":12.1,"tone":1},{"emoji":"🧑🏼‍🦽","version":12.1,"tone":2},{"emoji":"🧑🏽‍🦽","version":12.1,"tone":3},{"emoji":"🧑🏾‍🦽","version":12.1,"tone":4},{"emoji":"🧑🏿‍🦽","version":12.1,"tone":5}]},{"shortcodes":["man_in_manual_wheelchair"],"annotation":"man in manual wheelchair","tags":["accessibility","man","wheelchair"],"emoji":"👨‍🦽","order":2061,"group":1,"version":12,"skins":[{"emoji":"👨🏻‍🦽","version":12,"tone":1},{"emoji":"👨🏼‍🦽","version":12,"tone":2},{"emoji":"👨🏽‍🦽","version":12,"tone":3},{"emoji":"👨🏾‍🦽","version":12,"tone":4},{"emoji":"👨🏿‍🦽","version":12,"tone":5}]},{"shortcodes":["woman_in_manual_wheelchair"],"annotation":"woman in manual wheelchair","tags":["accessibility","wheelchair","woman"],"emoji":"👩‍🦽","order":2067,"group":1,"version":12,"skins":[{"emoji":"👩🏻‍🦽","version":12,"tone":1},{"emoji":"👩🏼‍🦽","version":12,"tone":2},{"emoji":"👩🏽‍🦽","version":12,"tone":3},{"emoji":"👩🏾‍🦽","version":12,"tone":4},{"emoji":"👩🏿‍🦽","version":12,"tone":5}]},{"shortcodes":["person_running","running"],"annotation":"person running","tags":["marathon","running"],"emoji":"🏃","order":2073,"group":1,"version":0.6,"skins":[{"emoji":"🏃🏻","version":1,"tone":1},{"emoji":"🏃🏼","version":1,"tone":2},{"emoji":"🏃🏽","version":1,"tone":3},{"emoji":"🏃🏾","version":1,"tone":4},{"emoji":"🏃🏿","version":1,"tone":5}]},{"shortcodes":["man_running"],"annotation":"man running","tags":["man","marathon","racing","running"],"emoji":"🏃‍♂️","order":2079,"group":1,"version":4,"skins":[{"emoji":"🏃🏻‍♂️","version":4,"tone":1},{"emoji":"🏃🏼‍♂️","version":4,"tone":2},{"emoji":"🏃🏽‍♂️","version":4,"tone":3},{"emoji":"🏃🏾‍♂️","version":4,"tone":4},{"emoji":"🏃🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_running"],"annotation":"woman running","tags":["marathon","racing","running","woman"],"emoji":"🏃‍♀️","order":2091,"group":1,"version":4,"skins":[{"emoji":"🏃🏻‍♀️","version":4,"tone":1},{"emoji":"🏃🏼‍♀️","version":4,"tone":2},{"emoji":"🏃🏽‍♀️","version":4,"tone":3},{"emoji":"🏃🏾‍♀️","version":4,"tone":4},{"emoji":"🏃🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["dancer","woman_dancing"],"annotation":"woman dancing","tags":["dance","dancing","woman"],"emoji":"💃","order":2103,"group":1,"version":0.6,"skins":[{"emoji":"💃🏻","version":1,"tone":1},{"emoji":"💃🏼","version":1,"tone":2},{"emoji":"💃🏽","version":1,"tone":3},{"emoji":"💃🏾","version":1,"tone":4},{"emoji":"💃🏿","version":1,"tone":5}]},{"shortcodes":["man_dancing"],"annotation":"man dancing","tags":["dance","dancing","man"],"emoji":"🕺","order":2109,"group":1,"version":3,"skins":[{"emoji":"🕺🏻","version":3,"tone":1},{"emoji":"🕺🏼","version":3,"tone":2},{"emoji":"🕺🏽","version":3,"tone":3},{"emoji":"🕺🏾","version":3,"tone":4},{"emoji":"🕺🏿","version":3,"tone":5}]},{"shortcodes":["levitate","levitating","person_in_suit_levitating"],"annotation":"person in suit levitating","tags":["business","person","suit"],"emoji":"🕴️","order":2116,"group":1,"version":0.7,"skins":[{"emoji":"🕴🏻","version":4,"tone":1},{"emoji":"🕴🏼","version":4,"tone":2},{"emoji":"🕴🏽","version":4,"tone":3},{"emoji":"🕴🏾","version":4,"tone":4},{"emoji":"🕴🏿","version":4,"tone":5}]},{"shortcodes":["dancers","people_with_bunny_ears_partying"],"annotation":"people with bunny ears","tags":["bunny ear","dancer","partying"],"emoji":"👯","order":2122,"group":1,"version":0.6},{"shortcodes":["men_with_bunny_ears_partying"],"annotation":"men with bunny ears","tags":["bunny ear","dancer","men","partying"],"emoji":"👯‍♂️","order":2123,"group":1,"version":4},{"shortcodes":["women_with_bunny_ears_partying"],"annotation":"women with bunny ears","tags":["bunny ear","dancer","partying","women"],"emoji":"👯‍♀️","order":2125,"group":1,"version":4},{"shortcodes":["person_in_steamy_room"],"annotation":"person in steamy room","tags":["sauna","steam room"],"emoji":"🧖","order":2127,"group":1,"version":5,"skins":[{"emoji":"🧖🏻","version":5,"tone":1},{"emoji":"🧖🏼","version":5,"tone":2},{"emoji":"🧖🏽","version":5,"tone":3},{"emoji":"🧖🏾","version":5,"tone":4},{"emoji":"🧖🏿","version":5,"tone":5}]},{"shortcodes":["man_in_steamy_room"],"annotation":"man in steamy room","tags":["sauna","steam room"],"emoji":"🧖‍♂️","order":2133,"group":1,"version":5,"skins":[{"emoji":"🧖🏻‍♂️","version":5,"tone":1},{"emoji":"🧖🏼‍♂️","version":5,"tone":2},{"emoji":"🧖🏽‍♂️","version":5,"tone":3},{"emoji":"🧖🏾‍♂️","version":5,"tone":4},{"emoji":"🧖🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_in_steamy_room"],"annotation":"woman in steamy room","tags":["sauna","steam room"],"emoji":"🧖‍♀️","order":2145,"group":1,"version":5,"skins":[{"emoji":"🧖🏻‍♀️","version":5,"tone":1},{"emoji":"🧖🏼‍♀️","version":5,"tone":2},{"emoji":"🧖🏽‍♀️","version":5,"tone":3},{"emoji":"🧖🏾‍♀️","version":5,"tone":4},{"emoji":"🧖🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["climbing","person_climbing"],"annotation":"person climbing","tags":["climber"],"emoji":"🧗","order":2157,"group":1,"version":5,"skins":[{"emoji":"🧗🏻","version":5,"tone":1},{"emoji":"🧗🏼","version":5,"tone":2},{"emoji":"🧗🏽","version":5,"tone":3},{"emoji":"🧗🏾","version":5,"tone":4},{"emoji":"🧗🏿","version":5,"tone":5}]},{"shortcodes":["man_climbing"],"annotation":"man climbing","tags":["climber"],"emoji":"🧗‍♂️","order":2163,"group":1,"version":5,"skins":[{"emoji":"🧗🏻‍♂️","version":5,"tone":1},{"emoji":"🧗🏼‍♂️","version":5,"tone":2},{"emoji":"🧗🏽‍♂️","version":5,"tone":3},{"emoji":"🧗🏾‍♂️","version":5,"tone":4},{"emoji":"🧗🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_climbing"],"annotation":"woman climbing","tags":["climber"],"emoji":"🧗‍♀️","order":2175,"group":1,"version":5,"skins":[{"emoji":"🧗🏻‍♀️","version":5,"tone":1},{"emoji":"🧗🏼‍♀️","version":5,"tone":2},{"emoji":"🧗🏽‍♀️","version":5,"tone":3},{"emoji":"🧗🏾‍♀️","version":5,"tone":4},{"emoji":"🧗🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["fencer","fencing","person_fencing"],"annotation":"person fencing","tags":["fencer","fencing","sword"],"emoji":"🤺","order":2187,"group":1,"version":3},{"shortcodes":["horse_racing"],"annotation":"horse racing","tags":["horse","jockey","racehorse","racing"],"emoji":"🏇","order":2188,"group":1,"version":1,"skins":[{"emoji":"🏇🏻","version":1,"tone":1},{"emoji":"🏇🏼","version":1,"tone":2},{"emoji":"🏇🏽","version":1,"tone":3},{"emoji":"🏇🏾","version":1,"tone":4},{"emoji":"🏇🏿","version":1,"tone":5}]},{"shortcodes":["person_skiing","skier","skiing"],"annotation":"skier","tags":["ski","snow"],"emoji":"⛷️","order":2195,"group":1,"version":0.7},{"shortcodes":["person_snowboarding","snowboarder","snowboarding"],"annotation":"snowboarder","tags":["ski","snow","snowboard"],"emoji":"🏂️","order":2196,"group":1,"version":0.6,"skins":[{"emoji":"🏂🏻","version":1,"tone":1},{"emoji":"🏂🏼","version":1,"tone":2},{"emoji":"🏂🏽","version":1,"tone":3},{"emoji":"🏂🏾","version":1,"tone":4},{"emoji":"🏂🏿","version":1,"tone":5}]},{"shortcodes":["golfer","golfing","person_golfing"],"annotation":"person golfing","tags":["ball","golf"],"emoji":"🏌️","order":2203,"group":1,"version":0.7,"skins":[{"emoji":"🏌🏻","version":4,"tone":1},{"emoji":"🏌🏼","version":4,"tone":2},{"emoji":"🏌🏽","version":4,"tone":3},{"emoji":"🏌🏾","version":4,"tone":4},{"emoji":"🏌🏿","version":4,"tone":5}]},{"shortcodes":["man_golfing"],"annotation":"man golfing","tags":["golf","man"],"emoji":"🏌️‍♂️","order":2209,"group":1,"version":4,"skins":[{"emoji":"🏌🏻‍♂️","version":4,"tone":1},{"emoji":"🏌🏼‍♂️","version":4,"tone":2},{"emoji":"🏌🏽‍♂️","version":4,"tone":3},{"emoji":"🏌🏾‍♂️","version":4,"tone":4},{"emoji":"🏌🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_golfing"],"annotation":"woman golfing","tags":["golf","woman"],"emoji":"🏌️‍♀️","order":2223,"group":1,"version":4,"skins":[{"emoji":"🏌🏻‍♀️","version":4,"tone":1},{"emoji":"🏌🏼‍♀️","version":4,"tone":2},{"emoji":"🏌🏽‍♀️","version":4,"tone":3},{"emoji":"🏌🏾‍♀️","version":4,"tone":4},{"emoji":"🏌🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_surfing","surfer","surfing"],"annotation":"person surfing","tags":["surfing"],"emoji":"🏄️","order":2237,"group":1,"version":0.6,"skins":[{"emoji":"🏄🏻","version":1,"tone":1},{"emoji":"🏄🏼","version":1,"tone":2},{"emoji":"🏄🏽","version":1,"tone":3},{"emoji":"🏄🏾","version":1,"tone":4},{"emoji":"🏄🏿","version":1,"tone":5}]},{"shortcodes":["man_surfing"],"annotation":"man surfing","tags":["man","surfing"],"emoji":"🏄‍♂️","order":2243,"group":1,"version":4,"skins":[{"emoji":"🏄🏻‍♂️","version":4,"tone":1},{"emoji":"🏄🏼‍♂️","version":4,"tone":2},{"emoji":"🏄🏽‍♂️","version":4,"tone":3},{"emoji":"🏄🏾‍♂️","version":4,"tone":4},{"emoji":"🏄🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_surfing"],"annotation":"woman surfing","tags":["surfing","woman"],"emoji":"🏄‍♀️","order":2255,"group":1,"version":4,"skins":[{"emoji":"🏄🏻‍♀️","version":4,"tone":1},{"emoji":"🏄🏼‍♀️","version":4,"tone":2},{"emoji":"🏄🏽‍♀️","version":4,"tone":3},{"emoji":"🏄🏾‍♀️","version":4,"tone":4},{"emoji":"🏄🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_rowing_boat","rowboat"],"annotation":"person rowing boat","tags":["boat","rowboat"],"emoji":"🚣","order":2267,"group":1,"version":1,"skins":[{"emoji":"🚣🏻","version":1,"tone":1},{"emoji":"🚣🏼","version":1,"tone":2},{"emoji":"🚣🏽","version":1,"tone":3},{"emoji":"🚣🏾","version":1,"tone":4},{"emoji":"🚣🏿","version":1,"tone":5}]},{"shortcodes":["man_rowing_boat"],"annotation":"man rowing boat","tags":["boat","man","rowboat"],"emoji":"🚣‍♂️","order":2273,"group":1,"version":4,"skins":[{"emoji":"🚣🏻‍♂️","version":4,"tone":1},{"emoji":"🚣🏼‍♂️","version":4,"tone":2},{"emoji":"🚣🏽‍♂️","version":4,"tone":3},{"emoji":"🚣🏾‍♂️","version":4,"tone":4},{"emoji":"🚣🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_rowing_boat"],"annotation":"woman rowing boat","tags":["boat","rowboat","woman"],"emoji":"🚣‍♀️","order":2285,"group":1,"version":4,"skins":[{"emoji":"🚣🏻‍♀️","version":4,"tone":1},{"emoji":"🚣🏼‍♀️","version":4,"tone":2},{"emoji":"🚣🏽‍♀️","version":4,"tone":3},{"emoji":"🚣🏾‍♀️","version":4,"tone":4},{"emoji":"🚣🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_swimming","swimmer","swimming"],"annotation":"person swimming","tags":["swim"],"emoji":"🏊️","order":2297,"group":1,"version":0.6,"skins":[{"emoji":"🏊🏻","version":1,"tone":1},{"emoji":"🏊🏼","version":1,"tone":2},{"emoji":"🏊🏽","version":1,"tone":3},{"emoji":"🏊🏾","version":1,"tone":4},{"emoji":"🏊🏿","version":1,"tone":5}]},{"shortcodes":["man_swimming"],"annotation":"man swimming","tags":["man","swim"],"emoji":"🏊‍♂️","order":2303,"group":1,"version":4,"skins":[{"emoji":"🏊🏻‍♂️","version":4,"tone":1},{"emoji":"🏊🏼‍♂️","version":4,"tone":2},{"emoji":"🏊🏽‍♂️","version":4,"tone":3},{"emoji":"🏊🏾‍♂️","version":4,"tone":4},{"emoji":"🏊🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_swimming"],"annotation":"woman swimming","tags":["swim","woman"],"emoji":"🏊‍♀️","order":2315,"group":1,"version":4,"skins":[{"emoji":"🏊🏻‍♀️","version":4,"tone":1},{"emoji":"🏊🏼‍♀️","version":4,"tone":2},{"emoji":"🏊🏽‍♀️","version":4,"tone":3},{"emoji":"🏊🏾‍♀️","version":4,"tone":4},{"emoji":"🏊🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_bouncing_ball"],"annotation":"person bouncing ball","tags":["ball"],"emoji":"⛹️","order":2328,"group":1,"version":0.7,"skins":[{"emoji":"⛹🏻","version":2,"tone":1},{"emoji":"⛹🏼","version":2,"tone":2},{"emoji":"⛹🏽","version":2,"tone":3},{"emoji":"⛹🏾","version":2,"tone":4},{"emoji":"⛹🏿","version":2,"tone":5}]},{"shortcodes":["man_bouncing_ball"],"annotation":"man bouncing ball","tags":["ball","man"],"emoji":"⛹️‍♂️","order":2334,"group":1,"version":4,"skins":[{"emoji":"⛹🏻‍♂️","version":4,"tone":1},{"emoji":"⛹🏼‍♂️","version":4,"tone":2},{"emoji":"⛹🏽‍♂️","version":4,"tone":3},{"emoji":"⛹🏾‍♂️","version":4,"tone":4},{"emoji":"⛹🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_bouncing_ball"],"annotation":"woman bouncing ball","tags":["ball","woman"],"emoji":"⛹️‍♀️","order":2348,"group":1,"version":4,"skins":[{"emoji":"⛹🏻‍♀️","version":4,"tone":1},{"emoji":"⛹🏼‍♀️","version":4,"tone":2},{"emoji":"⛹🏽‍♀️","version":4,"tone":3},{"emoji":"⛹🏾‍♀️","version":4,"tone":4},{"emoji":"⛹🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_lifting_weights","weight_lifter","weight_lifting"],"annotation":"person lifting weights","tags":["lifter","weight"],"emoji":"🏋️","order":2363,"group":1,"version":0.7,"skins":[{"emoji":"🏋🏻","version":2,"tone":1},{"emoji":"🏋🏼","version":2,"tone":2},{"emoji":"🏋🏽","version":2,"tone":3},{"emoji":"🏋🏾","version":2,"tone":4},{"emoji":"🏋🏿","version":2,"tone":5}]},{"shortcodes":["man_lifting_weights"],"annotation":"man lifting weights","tags":["man","weight lifter"],"emoji":"🏋️‍♂️","order":2369,"group":1,"version":4,"skins":[{"emoji":"🏋🏻‍♂️","version":4,"tone":1},{"emoji":"🏋🏼‍♂️","version":4,"tone":2},{"emoji":"🏋🏽‍♂️","version":4,"tone":3},{"emoji":"🏋🏾‍♂️","version":4,"tone":4},{"emoji":"🏋🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_lifting_weights"],"annotation":"woman lifting weights","tags":["weight lifter","woman"],"emoji":"🏋️‍♀️","order":2383,"group":1,"version":4,"skins":[{"emoji":"🏋🏻‍♀️","version":4,"tone":1},{"emoji":"🏋🏼‍♀️","version":4,"tone":2},{"emoji":"🏋🏽‍♀️","version":4,"tone":3},{"emoji":"🏋🏾‍♀️","version":4,"tone":4},{"emoji":"🏋🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["bicyclist","biking","person_biking"],"annotation":"person biking","tags":["bicycle","biking","cyclist"],"emoji":"🚴","order":2397,"group":1,"version":1,"skins":[{"emoji":"🚴🏻","version":1,"tone":1},{"emoji":"🚴🏼","version":1,"tone":2},{"emoji":"🚴🏽","version":1,"tone":3},{"emoji":"🚴🏾","version":1,"tone":4},{"emoji":"🚴🏿","version":1,"tone":5}]},{"shortcodes":["man_biking"],"annotation":"man biking","tags":["bicycle","biking","cyclist","man"],"emoji":"🚴‍♂️","order":2403,"group":1,"version":4,"skins":[{"emoji":"🚴🏻‍♂️","version":4,"tone":1},{"emoji":"🚴🏼‍♂️","version":4,"tone":2},{"emoji":"🚴🏽‍♂️","version":4,"tone":3},{"emoji":"🚴🏾‍♂️","version":4,"tone":4},{"emoji":"🚴🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_biking"],"annotation":"woman biking","tags":["bicycle","biking","cyclist","woman"],"emoji":"🚴‍♀️","order":2415,"group":1,"version":4,"skins":[{"emoji":"🚴🏻‍♀️","version":4,"tone":1},{"emoji":"🚴🏼‍♀️","version":4,"tone":2},{"emoji":"🚴🏽‍♀️","version":4,"tone":3},{"emoji":"🚴🏾‍♀️","version":4,"tone":4},{"emoji":"🚴🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["mountain_bicyclist","mountain_biking","person_mountain_biking"],"annotation":"person mountain biking","tags":["bicycle","bicyclist","bike","cyclist","mountain"],"emoji":"🚵","order":2427,"group":1,"version":1,"skins":[{"emoji":"🚵🏻","version":1,"tone":1},{"emoji":"🚵🏼","version":1,"tone":2},{"emoji":"🚵🏽","version":1,"tone":3},{"emoji":"🚵🏾","version":1,"tone":4},{"emoji":"🚵🏿","version":1,"tone":5}]},{"shortcodes":["man_mountain_biking"],"annotation":"man mountain biking","tags":["bicycle","bike","cyclist","man","mountain"],"emoji":"🚵‍♂️","order":2433,"group":1,"version":4,"skins":[{"emoji":"🚵🏻‍♂️","version":4,"tone":1},{"emoji":"🚵🏼‍♂️","version":4,"tone":2},{"emoji":"🚵🏽‍♂️","version":4,"tone":3},{"emoji":"🚵🏾‍♂️","version":4,"tone":4},{"emoji":"🚵🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_mountain_biking"],"annotation":"woman mountain biking","tags":["bicycle","bike","biking","cyclist","mountain","woman"],"emoji":"🚵‍♀️","order":2445,"group":1,"version":4,"skins":[{"emoji":"🚵🏻‍♀️","version":4,"tone":1},{"emoji":"🚵🏼‍♀️","version":4,"tone":2},{"emoji":"🚵🏽‍♀️","version":4,"tone":3},{"emoji":"🚵🏾‍♀️","version":4,"tone":4},{"emoji":"🚵🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["cartwheeling","person_cartwheel"],"annotation":"person cartwheeling","tags":["cartwheel","gymnastics"],"emoji":"🤸","order":2457,"group":1,"version":3,"skins":[{"emoji":"🤸🏻","version":3,"tone":1},{"emoji":"🤸🏼","version":3,"tone":2},{"emoji":"🤸🏽","version":3,"tone":3},{"emoji":"🤸🏾","version":3,"tone":4},{"emoji":"🤸🏿","version":3,"tone":5}]},{"shortcodes":["man_cartwheeling"],"annotation":"man cartwheeling","tags":["cartwheel","gymnastics","man"],"emoji":"🤸‍♂️","order":2463,"group":1,"version":4,"skins":[{"emoji":"🤸🏻‍♂️","version":4,"tone":1},{"emoji":"🤸🏼‍♂️","version":4,"tone":2},{"emoji":"🤸🏽‍♂️","version":4,"tone":3},{"emoji":"🤸🏾‍♂️","version":4,"tone":4},{"emoji":"🤸🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_cartwheeling"],"annotation":"woman cartwheeling","tags":["cartwheel","gymnastics","woman"],"emoji":"🤸‍♀️","order":2475,"group":1,"version":4,"skins":[{"emoji":"🤸🏻‍♀️","version":4,"tone":1},{"emoji":"🤸🏼‍♀️","version":4,"tone":2},{"emoji":"🤸🏽‍♀️","version":4,"tone":3},{"emoji":"🤸🏾‍♀️","version":4,"tone":4},{"emoji":"🤸🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["people_wrestling","wrestlers","wrestling"],"annotation":"people wrestling","tags":["wrestle","wrestler"],"emoji":"🤼","order":2487,"group":1,"version":3},{"shortcodes":["men_wrestling"],"annotation":"men wrestling","tags":["men","wrestle"],"emoji":"🤼‍♂️","order":2488,"group":1,"version":4},{"shortcodes":["women_wrestling"],"annotation":"women wrestling","tags":["women","wrestle"],"emoji":"🤼‍♀️","order":2490,"group":1,"version":4},{"shortcodes":["person_playing_water_polo","water_polo"],"annotation":"person playing water polo","tags":["polo","water"],"emoji":"🤽","order":2492,"group":1,"version":3,"skins":[{"emoji":"🤽🏻","version":3,"tone":1},{"emoji":"🤽🏼","version":3,"tone":2},{"emoji":"🤽🏽","version":3,"tone":3},{"emoji":"🤽🏾","version":3,"tone":4},{"emoji":"🤽🏿","version":3,"tone":5}]},{"shortcodes":["man_playing_water_polo"],"annotation":"man playing water polo","tags":["man","water polo"],"emoji":"🤽‍♂️","order":2498,"group":1,"version":4,"skins":[{"emoji":"🤽🏻‍♂️","version":4,"tone":1},{"emoji":"🤽🏼‍♂️","version":4,"tone":2},{"emoji":"🤽🏽‍♂️","version":4,"tone":3},{"emoji":"🤽🏾‍♂️","version":4,"tone":4},{"emoji":"🤽🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_playing_water_polo"],"annotation":"woman playing water polo","tags":["water polo","woman"],"emoji":"🤽‍♀️","order":2510,"group":1,"version":4,"skins":[{"emoji":"🤽🏻‍♀️","version":4,"tone":1},{"emoji":"🤽🏼‍♀️","version":4,"tone":2},{"emoji":"🤽🏽‍♀️","version":4,"tone":3},{"emoji":"🤽🏾‍♀️","version":4,"tone":4},{"emoji":"🤽🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["handball","person_playing_handball"],"annotation":"person playing handball","tags":["ball","handball"],"emoji":"🤾","order":2522,"group":1,"version":3,"skins":[{"emoji":"🤾🏻","version":3,"tone":1},{"emoji":"🤾🏼","version":3,"tone":2},{"emoji":"🤾🏽","version":3,"tone":3},{"emoji":"🤾🏾","version":3,"tone":4},{"emoji":"🤾🏿","version":3,"tone":5}]},{"shortcodes":["man_playing_handball"],"annotation":"man playing handball","tags":["handball","man"],"emoji":"🤾‍♂️","order":2528,"group":1,"version":4,"skins":[{"emoji":"🤾🏻‍♂️","version":4,"tone":1},{"emoji":"🤾🏼‍♂️","version":4,"tone":2},{"emoji":"🤾🏽‍♂️","version":4,"tone":3},{"emoji":"🤾🏾‍♂️","version":4,"tone":4},{"emoji":"🤾🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_playing_handball"],"annotation":"woman playing handball","tags":["handball","woman"],"emoji":"🤾‍♀️","order":2540,"group":1,"version":4,"skins":[{"emoji":"🤾🏻‍♀️","version":4,"tone":1},{"emoji":"🤾🏼‍♀️","version":4,"tone":2},{"emoji":"🤾🏽‍♀️","version":4,"tone":3},{"emoji":"🤾🏾‍♀️","version":4,"tone":4},{"emoji":"🤾🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["juggler","juggling","person_juggling"],"annotation":"person juggling","tags":["balance","juggle","multitask","skill"],"emoji":"🤹","order":2552,"group":1,"version":3,"skins":[{"emoji":"🤹🏻","version":3,"tone":1},{"emoji":"🤹🏼","version":3,"tone":2},{"emoji":"🤹🏽","version":3,"tone":3},{"emoji":"🤹🏾","version":3,"tone":4},{"emoji":"🤹🏿","version":3,"tone":5}]},{"shortcodes":["man_juggling"],"annotation":"man juggling","tags":["juggling","man","multitask"],"emoji":"🤹‍♂️","order":2558,"group":1,"version":4,"skins":[{"emoji":"🤹🏻‍♂️","version":4,"tone":1},{"emoji":"🤹🏼‍♂️","version":4,"tone":2},{"emoji":"🤹🏽‍♂️","version":4,"tone":3},{"emoji":"🤹🏾‍♂️","version":4,"tone":4},{"emoji":"🤹🏿‍♂️","version":4,"tone":5}]},{"shortcodes":["woman_juggling"],"annotation":"woman juggling","tags":["juggling","multitask","woman"],"emoji":"🤹‍♀️","order":2570,"group":1,"version":4,"skins":[{"emoji":"🤹🏻‍♀️","version":4,"tone":1},{"emoji":"🤹🏼‍♀️","version":4,"tone":2},{"emoji":"🤹🏽‍♀️","version":4,"tone":3},{"emoji":"🤹🏾‍♀️","version":4,"tone":4},{"emoji":"🤹🏿‍♀️","version":4,"tone":5}]},{"shortcodes":["person_in_lotus_position"],"annotation":"person in lotus position","tags":["meditation","yoga"],"emoji":"🧘","order":2582,"group":1,"version":5,"skins":[{"emoji":"🧘🏻","version":5,"tone":1},{"emoji":"🧘🏼","version":5,"tone":2},{"emoji":"🧘🏽","version":5,"tone":3},{"emoji":"🧘🏾","version":5,"tone":4},{"emoji":"🧘🏿","version":5,"tone":5}]},{"shortcodes":["man_in_lotus_position"],"annotation":"man in lotus position","tags":["meditation","yoga"],"emoji":"🧘‍♂️","order":2588,"group":1,"version":5,"skins":[{"emoji":"🧘🏻‍♂️","version":5,"tone":1},{"emoji":"🧘🏼‍♂️","version":5,"tone":2},{"emoji":"🧘🏽‍♂️","version":5,"tone":3},{"emoji":"🧘🏾‍♂️","version":5,"tone":4},{"emoji":"🧘🏿‍♂️","version":5,"tone":5}]},{"shortcodes":["woman_in_lotus_position"],"annotation":"woman in lotus position","tags":["meditation","yoga"],"emoji":"🧘‍♀️","order":2600,"group":1,"version":5,"skins":[{"emoji":"🧘🏻‍♀️","version":5,"tone":1},{"emoji":"🧘🏼‍♀️","version":5,"tone":2},{"emoji":"🧘🏽‍♀️","version":5,"tone":3},{"emoji":"🧘🏾‍♀️","version":5,"tone":4},{"emoji":"🧘🏿‍♀️","version":5,"tone":5}]},{"shortcodes":["bath","person_taking_bath"],"annotation":"person taking bath","tags":["bath","bathtub"],"emoji":"🛀","order":2612,"group":1,"version":0.6,"skins":[{"emoji":"🛀🏻","version":1,"tone":1},{"emoji":"🛀🏼","version":1,"tone":2},{"emoji":"🛀🏽","version":1,"tone":3},{"emoji":"🛀🏾","version":1,"tone":4},{"emoji":"🛀🏿","version":1,"tone":5}]},{"shortcodes":["person_in_bed","sleeping_accommodation"],"annotation":"person in bed","tags":["good night","hotel","sleep"],"emoji":"🛌","order":2618,"group":1,"version":1,"skins":[{"emoji":"🛌🏻","version":4,"tone":1},{"emoji":"🛌🏼","version":4,"tone":2},{"emoji":"🛌🏽","version":4,"tone":3},{"emoji":"🛌🏾","version":4,"tone":4},{"emoji":"🛌🏿","version":4,"tone":5}]},{"shortcodes":["people_holding_hands"],"annotation":"people holding hands","tags":["couple","hand","hold","holding hands","person"],"emoji":"🧑‍🤝‍🧑","order":2624,"group":1,"version":12,"skins":[{"emoji":"🧑🏻‍🤝‍🧑🏻","version":12,"tone":1},{"emoji":"🧑🏻‍🤝‍🧑🏼","version":12.1,"tone":[1,2]},{"emoji":"🧑🏻‍🤝‍🧑🏽","version":12.1,"tone":[1,3]},{"emoji":"🧑🏻‍🤝‍🧑🏾","version":12.1,"tone":[1,4]},{"emoji":"🧑🏻‍🤝‍🧑🏿","version":12.1,"tone":[1,5]},{"emoji":"🧑🏼‍🤝‍🧑🏻","version":12,"tone":[2,1]},{"emoji":"🧑🏼‍🤝‍🧑🏼","version":12,"tone":2},{"emoji":"🧑🏼‍🤝‍🧑🏽","version":12.1,"tone":[2,3]},{"emoji":"🧑🏼‍🤝‍🧑🏾","version":12.1,"tone":[2,4]},{"emoji":"🧑🏼‍🤝‍🧑🏿","version":12.1,"tone":[2,5]},{"emoji":"🧑🏽‍🤝‍🧑🏻","version":12,"tone":[3,1]},{"emoji":"🧑🏽‍🤝‍🧑🏼","version":12,"tone":[3,2]},{"emoji":"🧑🏽‍🤝‍🧑🏽","version":12,"tone":3},{"emoji":"🧑🏽‍🤝‍🧑🏾","version":12.1,"tone":[3,4]},{"emoji":"🧑🏽‍🤝‍🧑🏿","version":12.1,"tone":[3,5]},{"emoji":"🧑🏾‍🤝‍🧑🏻","version":12,"tone":[4,1]},{"emoji":"🧑🏾‍🤝‍🧑🏼","version":12,"tone":[4,2]},{"emoji":"🧑🏾‍🤝‍🧑🏽","version":12,"tone":[4,3]},{"emoji":"🧑🏾‍🤝‍🧑🏾","version":12,"tone":4},{"emoji":"🧑🏾‍🤝‍🧑🏿","version":12.1,"tone":[4,5]},{"emoji":"🧑🏿‍🤝‍🧑🏻","version":12,"tone":[5,1]},{"emoji":"🧑🏿‍🤝‍🧑🏼","version":12,"tone":[5,2]},{"emoji":"🧑🏿‍🤝‍🧑🏽","version":12,"tone":[5,3]},{"emoji":"🧑🏿‍🤝‍🧑🏾","version":12,"tone":[5,4]},{"emoji":"🧑🏿‍🤝‍🧑🏿","version":12,"tone":5}]},{"shortcodes":["two_women_holding_hands"],"annotation":"women holding hands","tags":["couple","hand","holding hands","women"],"emoji":"👭","order":2650,"group":1,"version":1,"skins":[{"emoji":"👭🏻","version":12,"tone":1},{"emoji":"👭🏼","version":12,"tone":2},{"emoji":"👭🏽","version":12,"tone":3},{"emoji":"👭🏾","version":12,"tone":4},{"emoji":"👭🏿","version":12,"tone":5},{"emoji":"👩🏻‍🤝‍👩🏼","version":12.1,"tone":[1,2]},{"emoji":"👩🏻‍🤝‍👩🏽","version":12.1,"tone":[1,3]},{"emoji":"👩🏻‍🤝‍👩🏾","version":12.1,"tone":[1,4]},{"emoji":"👩🏻‍🤝‍👩🏿","version":12.1,"tone":[1,5]},{"emoji":"👩🏼‍🤝‍👩🏻","version":12,"tone":[2,1]},{"emoji":"👩🏼‍🤝‍👩🏽","version":12.1,"tone":[2,3]},{"emoji":"👩🏼‍🤝‍👩🏾","version":12.1,"tone":[2,4]},{"emoji":"👩🏼‍🤝‍👩🏿","version":12.1,"tone":[2,5]},{"emoji":"👩🏽‍🤝‍👩🏻","version":12,"tone":[3,1]},{"emoji":"👩🏽‍🤝‍👩🏼","version":12,"tone":[3,2]},{"emoji":"👩🏽‍🤝‍👩🏾","version":12.1,"tone":[3,4]},{"emoji":"👩🏽‍🤝‍👩🏿","version":12.1,"tone":[3,5]},{"emoji":"👩🏾‍🤝‍👩🏻","version":12,"tone":[4,1]},{"emoji":"👩🏾‍🤝‍👩🏼","version":12,"tone":[4,2]},{"emoji":"👩🏾‍🤝‍👩🏽","version":12,"tone":[4,3]},{"emoji":"👩🏾‍🤝‍👩🏿","version":12.1,"tone":[4,5]},{"emoji":"👩🏿‍🤝‍👩🏻","version":12,"tone":[5,1]},{"emoji":"👩🏿‍🤝‍👩🏼","version":12,"tone":[5,2]},{"emoji":"👩🏿‍🤝‍👩🏽","version":12,"tone":[5,3]},{"emoji":"👩🏿‍🤝‍👩🏾","version":12,"tone":[5,4]}]},{"shortcodes":["couple"],"annotation":"woman and man holding hands","tags":["couple","hand","hold","holding hands","man","woman"],"emoji":"👫","order":2676,"group":1,"version":0.6,"skins":[{"emoji":"👫🏻","version":12,"tone":1},{"emoji":"👫🏼","version":12,"tone":2},{"emoji":"👫🏽","version":12,"tone":3},{"emoji":"👫🏾","version":12,"tone":4},{"emoji":"👫🏿","version":12,"tone":5},{"emoji":"👩🏻‍🤝‍👨🏼","version":12,"tone":[1,2]},{"emoji":"👩🏻‍🤝‍👨🏽","version":12,"tone":[1,3]},{"emoji":"👩🏻‍🤝‍👨🏾","version":12,"tone":[1,4]},{"emoji":"👩🏻‍🤝‍👨🏿","version":12,"tone":[1,5]},{"emoji":"👩🏼‍🤝‍👨🏻","version":12,"tone":[2,1]},{"emoji":"👩🏼‍🤝‍👨🏽","version":12,"tone":[2,3]},{"emoji":"👩🏼‍🤝‍👨🏾","version":12,"tone":[2,4]},{"emoji":"👩🏼‍🤝‍👨🏿","version":12,"tone":[2,5]},{"emoji":"👩🏽‍🤝‍👨🏻","version":12,"tone":[3,1]},{"emoji":"👩🏽‍🤝‍👨🏼","version":12,"tone":[3,2]},{"emoji":"👩🏽‍🤝‍👨🏾","version":12,"tone":[3,4]},{"emoji":"👩🏽‍🤝‍👨🏿","version":12,"tone":[3,5]},{"emoji":"👩🏾‍🤝‍👨🏻","version":12,"tone":[4,1]},{"emoji":"👩🏾‍🤝‍👨🏼","version":12,"tone":[4,2]},{"emoji":"👩🏾‍🤝‍👨🏽","version":12,"tone":[4,3]},{"emoji":"👩🏾‍🤝‍👨🏿","version":12,"tone":[4,5]},{"emoji":"👩🏿‍🤝‍👨🏻","version":12,"tone":[5,1]},{"emoji":"👩🏿‍🤝‍👨🏼","version":12,"tone":[5,2]},{"emoji":"👩🏿‍🤝‍👨🏽","version":12,"tone":[5,3]},{"emoji":"👩🏿‍🤝‍👨🏾","version":12,"tone":[5,4]}]},{"shortcodes":["two_men_holding_hands"],"annotation":"men holding hands","tags":["couple","gemini","holding hands","man","men","twins","zodiac"],"emoji":"👬","order":2702,"group":1,"version":1,"skins":[{"emoji":"👬🏻","version":12,"tone":1},{"emoji":"👬🏼","version":12,"tone":2},{"emoji":"👬🏽","version":12,"tone":3},{"emoji":"👬🏾","version":12,"tone":4},{"emoji":"👬🏿","version":12,"tone":5},{"emoji":"👨🏻‍🤝‍👨🏼","version":12.1,"tone":[1,2]},{"emoji":"👨🏻‍🤝‍👨🏽","version":12.1,"tone":[1,3]},{"emoji":"👨🏻‍🤝‍👨🏾","version":12.1,"tone":[1,4]},{"emoji":"👨🏻‍🤝‍👨🏿","version":12.1,"tone":[1,5]},{"emoji":"👨🏼‍🤝‍👨🏻","version":12,"tone":[2,1]},{"emoji":"👨🏼‍🤝‍👨🏽","version":12.1,"tone":[2,3]},{"emoji":"👨🏼‍🤝‍👨🏾","version":12.1,"tone":[2,4]},{"emoji":"👨🏼‍🤝‍👨🏿","version":12.1,"tone":[2,5]},{"emoji":"👨🏽‍🤝‍👨🏻","version":12,"tone":[3,1]},{"emoji":"👨🏽‍🤝‍👨🏼","version":12,"tone":[3,2]},{"emoji":"👨🏽‍🤝‍👨🏾","version":12.1,"tone":[3,4]},{"emoji":"👨🏽‍🤝‍👨🏿","version":12.1,"tone":[3,5]},{"emoji":"👨🏾‍🤝‍👨🏻","version":12,"tone":[4,1]},{"emoji":"👨🏾‍🤝‍👨🏼","version":12,"tone":[4,2]},{"emoji":"👨🏾‍🤝‍👨🏽","version":12,"tone":[4,3]},{"emoji":"👨🏾‍🤝‍👨🏿","version":12.1,"tone":[4,5]},{"emoji":"👨🏿‍🤝‍👨🏻","version":12,"tone":[5,1]},{"emoji":"👨🏿‍🤝‍👨🏼","version":12,"tone":[5,2]},{"emoji":"👨🏿‍🤝‍👨🏽","version":12,"tone":[5,3]},{"emoji":"👨🏿‍🤝‍👨🏾","version":12,"tone":[5,4]}]},{"shortcodes":["couple_kiss","couplekiss"],"annotation":"kiss","tags":["couple"],"emoji":"💏","order":2728,"group":1,"version":0.6,"skins":[{"emoji":"💏🏻","version":13.1,"tone":1},{"emoji":"💏🏼","version":13.1,"tone":2},{"emoji":"💏🏽","version":13.1,"tone":3},{"emoji":"💏🏾","version":13.1,"tone":4},{"emoji":"💏🏿","version":13.1,"tone":5},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏼","version":13.1,"tone":[1,2]},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏽","version":13.1,"tone":[1,3]},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏾","version":13.1,"tone":[1,4]},{"emoji":"🧑🏻‍❤️‍💋‍🧑🏿","version":13.1,"tone":[1,5]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏻","version":13.1,"tone":[2,1]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏽","version":13.1,"tone":[2,3]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏾","version":13.1,"tone":[2,4]},{"emoji":"🧑🏼‍❤️‍💋‍🧑🏿","version":13.1,"tone":[2,5]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏻","version":13.1,"tone":[3,1]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏼","version":13.1,"tone":[3,2]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏾","version":13.1,"tone":[3,4]},{"emoji":"🧑🏽‍❤️‍💋‍🧑🏿","version":13.1,"tone":[3,5]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏻","version":13.1,"tone":[4,1]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏼","version":13.1,"tone":[4,2]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏽","version":13.1,"tone":[4,3]},{"emoji":"🧑🏾‍❤️‍💋‍🧑🏿","version":13.1,"tone":[4,5]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏻","version":13.1,"tone":[5,1]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏼","version":13.1,"tone":[5,2]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏽","version":13.1,"tone":[5,3]},{"emoji":"🧑🏿‍❤️‍💋‍🧑🏾","version":13.1,"tone":[5,4]}]},{"shortcodes":["kiss_mw","kiss_wm"],"annotation":"kiss: woman, man","tags":["couple","kiss","man","woman"],"emoji":"👩‍❤️‍💋‍👨","order":2774,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍💋‍👨🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍💋‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍💋‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍💋‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍💋‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍💋‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍💋‍👨🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍💋‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍💋‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍💋‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍💋‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍💋‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍💋‍👨🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍💋‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍💋‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍💋‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍💋‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍💋‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍💋‍👨🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍💋‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍💋‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍💋‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍💋‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍💋‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍💋‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["kiss_mm"],"annotation":"kiss: man, man","tags":["couple","kiss","man"],"emoji":"👨‍❤️‍💋‍👨","order":2826,"group":1,"version":2,"skins":[{"emoji":"👨🏻‍❤️‍💋‍👨🏻","version":13.1,"tone":1},{"emoji":"👨🏻‍❤️‍💋‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👨🏻‍❤️‍💋‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👨🏻‍❤️‍💋‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👨🏻‍❤️‍💋‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👨🏼‍❤️‍💋‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👨🏼‍❤️‍💋‍👨🏼","version":13.1,"tone":2},{"emoji":"👨🏼‍❤️‍💋‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👨🏼‍❤️‍💋‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👨🏼‍❤️‍💋‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👨🏽‍❤️‍💋‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👨🏽‍❤️‍💋‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👨🏽‍❤️‍💋‍👨🏽","version":13.1,"tone":3},{"emoji":"👨🏽‍❤️‍💋‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👨🏽‍❤️‍💋‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👨🏾‍❤️‍💋‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👨🏾‍❤️‍💋‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👨🏾‍❤️‍💋‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👨🏾‍❤️‍💋‍👨🏾","version":13.1,"tone":4},{"emoji":"👨🏾‍❤️‍💋‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👨🏿‍❤️‍💋‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👨🏿‍❤️‍💋‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👨🏿‍❤️‍💋‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👨🏿‍❤️‍💋‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👨🏿‍❤️‍💋‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["kiss_ww"],"annotation":"kiss: woman, woman","tags":["couple","kiss","woman"],"emoji":"👩‍❤️‍💋‍👩","order":2878,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍💋‍👩🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍💋‍👩🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍💋‍👩🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍💋‍👩🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍💋‍👩🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍💋‍👩🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍💋‍👩🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍💋‍👩🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍💋‍👩🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍💋‍👩🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍💋‍👩🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍💋‍👩🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍💋‍👩🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍💋‍👩🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍💋‍👩🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍💋‍👩🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍💋‍👩🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍💋‍👩🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍💋‍👩🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍💋‍👩🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍💋‍👩🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍💋‍👩🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍💋‍👩🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍💋‍👩🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍💋‍👩🏿","version":13.1,"tone":5}]},{"shortcodes":["couple_with_heart"],"annotation":"couple with heart","tags":["couple","love"],"emoji":"💑","order":2930,"group":1,"version":0.6,"skins":[{"emoji":"💑🏻","version":13.1,"tone":1},{"emoji":"💑🏼","version":13.1,"tone":2},{"emoji":"💑🏽","version":13.1,"tone":3},{"emoji":"💑🏾","version":13.1,"tone":4},{"emoji":"💑🏿","version":13.1,"tone":5},{"emoji":"🧑🏻‍❤️‍🧑🏼","version":13.1,"tone":[1,2]},{"emoji":"🧑🏻‍❤️‍🧑🏽","version":13.1,"tone":[1,3]},{"emoji":"🧑🏻‍❤️‍🧑🏾","version":13.1,"tone":[1,4]},{"emoji":"🧑🏻‍❤️‍🧑🏿","version":13.1,"tone":[1,5]},{"emoji":"🧑🏼‍❤️‍🧑🏻","version":13.1,"tone":[2,1]},{"emoji":"🧑🏼‍❤️‍🧑🏽","version":13.1,"tone":[2,3]},{"emoji":"🧑🏼‍❤️‍🧑🏾","version":13.1,"tone":[2,4]},{"emoji":"🧑🏼‍❤️‍🧑🏿","version":13.1,"tone":[2,5]},{"emoji":"🧑🏽‍❤️‍🧑🏻","version":13.1,"tone":[3,1]},{"emoji":"🧑🏽‍❤️‍🧑🏼","version":13.1,"tone":[3,2]},{"emoji":"🧑🏽‍❤️‍🧑🏾","version":13.1,"tone":[3,4]},{"emoji":"🧑🏽‍❤️‍🧑🏿","version":13.1,"tone":[3,5]},{"emoji":"🧑🏾‍❤️‍🧑🏻","version":13.1,"tone":[4,1]},{"emoji":"🧑🏾‍❤️‍🧑🏼","version":13.1,"tone":[4,2]},{"emoji":"🧑🏾‍❤️‍🧑🏽","version":13.1,"tone":[4,3]},{"emoji":"🧑🏾‍❤️‍🧑🏿","version":13.1,"tone":[4,5]},{"emoji":"🧑🏿‍❤️‍🧑🏻","version":13.1,"tone":[5,1]},{"emoji":"🧑🏿‍❤️‍🧑🏼","version":13.1,"tone":[5,2]},{"emoji":"🧑🏿‍❤️‍🧑🏽","version":13.1,"tone":[5,3]},{"emoji":"🧑🏿‍❤️‍🧑🏾","version":13.1,"tone":[5,4]}]},{"shortcodes":["couple_with_heart_mw","couple_with_heart_wm"],"annotation":"couple with heart: woman, man","tags":["couple","couple with heart","love","man","woman"],"emoji":"👩‍❤️‍👨","order":2976,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍👨🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍👨🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍👨🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍👨🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["couple_with_heart_mm"],"annotation":"couple with heart: man, man","tags":["couple","couple with heart","love","man"],"emoji":"👨‍❤️‍👨","order":3028,"group":1,"version":2,"skins":[{"emoji":"👨🏻‍❤️‍👨🏻","version":13.1,"tone":1},{"emoji":"👨🏻‍❤️‍👨🏼","version":13.1,"tone":[1,2]},{"emoji":"👨🏻‍❤️‍👨🏽","version":13.1,"tone":[1,3]},{"emoji":"👨🏻‍❤️‍👨🏾","version":13.1,"tone":[1,4]},{"emoji":"👨🏻‍❤️‍👨🏿","version":13.1,"tone":[1,5]},{"emoji":"👨🏼‍❤️‍👨🏻","version":13.1,"tone":[2,1]},{"emoji":"👨🏼‍❤️‍👨🏼","version":13.1,"tone":2},{"emoji":"👨🏼‍❤️‍👨🏽","version":13.1,"tone":[2,3]},{"emoji":"👨🏼‍❤️‍👨🏾","version":13.1,"tone":[2,4]},{"emoji":"👨🏼‍❤️‍👨🏿","version":13.1,"tone":[2,5]},{"emoji":"👨🏽‍❤️‍👨🏻","version":13.1,"tone":[3,1]},{"emoji":"👨🏽‍❤️‍👨🏼","version":13.1,"tone":[3,2]},{"emoji":"👨🏽‍❤️‍👨🏽","version":13.1,"tone":3},{"emoji":"👨🏽‍❤️‍👨🏾","version":13.1,"tone":[3,4]},{"emoji":"👨🏽‍❤️‍👨🏿","version":13.1,"tone":[3,5]},{"emoji":"👨🏾‍❤️‍👨🏻","version":13.1,"tone":[4,1]},{"emoji":"👨🏾‍❤️‍👨🏼","version":13.1,"tone":[4,2]},{"emoji":"👨🏾‍❤️‍👨🏽","version":13.1,"tone":[4,3]},{"emoji":"👨🏾‍❤️‍👨🏾","version":13.1,"tone":4},{"emoji":"👨🏾‍❤️‍👨🏿","version":13.1,"tone":[4,5]},{"emoji":"👨🏿‍❤️‍👨🏻","version":13.1,"tone":[5,1]},{"emoji":"👨🏿‍❤️‍👨🏼","version":13.1,"tone":[5,2]},{"emoji":"👨🏿‍❤️‍👨🏽","version":13.1,"tone":[5,3]},{"emoji":"👨🏿‍❤️‍👨🏾","version":13.1,"tone":[5,4]},{"emoji":"👨🏿‍❤️‍👨🏿","version":13.1,"tone":5}]},{"shortcodes":["couple_with_heart_ww"],"annotation":"couple with heart: woman, woman","tags":["couple","couple with heart","love","woman"],"emoji":"👩‍❤️‍👩","order":3080,"group":1,"version":2,"skins":[{"emoji":"👩🏻‍❤️‍👩🏻","version":13.1,"tone":1},{"emoji":"👩🏻‍❤️‍👩🏼","version":13.1,"tone":[1,2]},{"emoji":"👩🏻‍❤️‍👩🏽","version":13.1,"tone":[1,3]},{"emoji":"👩🏻‍❤️‍👩🏾","version":13.1,"tone":[1,4]},{"emoji":"👩🏻‍❤️‍👩🏿","version":13.1,"tone":[1,5]},{"emoji":"👩🏼‍❤️‍👩🏻","version":13.1,"tone":[2,1]},{"emoji":"👩🏼‍❤️‍👩🏼","version":13.1,"tone":2},{"emoji":"👩🏼‍❤️‍👩🏽","version":13.1,"tone":[2,3]},{"emoji":"👩🏼‍❤️‍👩🏾","version":13.1,"tone":[2,4]},{"emoji":"👩🏼‍❤️‍👩🏿","version":13.1,"tone":[2,5]},{"emoji":"👩🏽‍❤️‍👩🏻","version":13.1,"tone":[3,1]},{"emoji":"👩🏽‍❤️‍👩🏼","version":13.1,"tone":[3,2]},{"emoji":"👩🏽‍❤️‍👩🏽","version":13.1,"tone":3},{"emoji":"👩🏽‍❤️‍👩🏾","version":13.1,"tone":[3,4]},{"emoji":"👩🏽‍❤️‍👩🏿","version":13.1,"tone":[3,5]},{"emoji":"👩🏾‍❤️‍👩🏻","version":13.1,"tone":[4,1]},{"emoji":"👩🏾‍❤️‍👩🏼","version":13.1,"tone":[4,2]},{"emoji":"👩🏾‍❤️‍👩🏽","version":13.1,"tone":[4,3]},{"emoji":"👩🏾‍❤️‍👩🏾","version":13.1,"tone":4},{"emoji":"👩🏾‍❤️‍👩🏿","version":13.1,"tone":[4,5]},{"emoji":"👩🏿‍❤️‍👩🏻","version":13.1,"tone":[5,1]},{"emoji":"👩🏿‍❤️‍👩🏼","version":13.1,"tone":[5,2]},{"emoji":"👩🏿‍❤️‍👩🏽","version":13.1,"tone":[5,3]},{"emoji":"👩🏿‍❤️‍👩🏾","version":13.1,"tone":[5,4]},{"emoji":"👩🏿‍❤️‍👩🏿","version":13.1,"tone":5}]},{"shortcodes":["family"],"annotation":"family","tags":["family"],"emoji":"👪️","order":3132,"group":1,"version":0.6},{"shortcodes":["family_mwb"],"annotation":"family: man, woman, boy","tags":["boy","family","man","woman"],"emoji":"👨‍👩‍👦","order":3133,"group":1,"version":2},{"shortcodes":["family_mwg"],"annotation":"family: man, woman, girl","tags":["family","girl","man","woman"],"emoji":"👨‍👩‍👧","order":3134,"group":1,"version":2},{"shortcodes":["family_mwgb"],"annotation":"family: man, woman, girl, boy","tags":["boy","family","girl","man","woman"],"emoji":"👨‍👩‍👧‍👦","order":3135,"group":1,"version":2},{"shortcodes":["family_mwbb"],"annotation":"family: man, woman, boy, boy","tags":["boy","family","man","woman"],"emoji":"👨‍👩‍👦‍👦","order":3136,"group":1,"version":2},{"shortcodes":["family_mwgg"],"annotation":"family: man, woman, girl, girl","tags":["family","girl","man","woman"],"emoji":"👨‍👩‍👧‍👧","order":3137,"group":1,"version":2},{"shortcodes":["family_mmb"],"annotation":"family: man, man, boy","tags":["boy","family","man"],"emoji":"👨‍👨‍👦","order":3138,"group":1,"version":2},{"shortcodes":["family_mmg"],"annotation":"family: man, man, girl","tags":["family","girl","man"],"emoji":"👨‍👨‍👧","order":3139,"group":1,"version":2},{"shortcodes":["family_mmgb"],"annotation":"family: man, man, girl, boy","tags":["boy","family","girl","man"],"emoji":"👨‍👨‍👧‍👦","order":3140,"group":1,"version":2},{"shortcodes":["family_mmbb"],"annotation":"family: man, man, boy, boy","tags":["boy","family","man"],"emoji":"👨‍👨‍👦‍👦","order":3141,"group":1,"version":2},{"shortcodes":["family_mmgg"],"annotation":"family: man, man, girl, girl","tags":["family","girl","man"],"emoji":"👨‍👨‍👧‍👧","order":3142,"group":1,"version":2},{"shortcodes":["family_wwb"],"annotation":"family: woman, woman, boy","tags":["boy","family","woman"],"emoji":"👩‍👩‍👦","order":3143,"group":1,"version":2},{"shortcodes":["family_wwg"],"annotation":"family: woman, woman, girl","tags":["family","girl","woman"],"emoji":"👩‍👩‍👧","order":3144,"group":1,"version":2},{"shortcodes":["family_wwgb"],"annotation":"family: woman, woman, girl, boy","tags":["boy","family","girl","woman"],"emoji":"👩‍👩‍👧‍👦","order":3145,"group":1,"version":2},{"shortcodes":["family_wwbb"],"annotation":"family: woman, woman, boy, boy","tags":["boy","family","woman"],"emoji":"👩‍👩‍👦‍👦","order":3146,"group":1,"version":2},{"shortcodes":["family_wwgg"],"annotation":"family: woman, woman, girl, girl","tags":["family","girl","woman"],"emoji":"👩‍👩‍👧‍👧","order":3147,"group":1,"version":2},{"shortcodes":["family_mb"],"annotation":"family: man, boy","tags":["boy","family","man"],"emoji":"👨‍👦","order":3148,"group":1,"version":4},{"shortcodes":["family_mbb"],"annotation":"family: man, boy, boy","tags":["boy","family","man"],"emoji":"👨‍👦‍👦","order":3149,"group":1,"version":4},{"shortcodes":["family_mg"],"annotation":"family: man, girl","tags":["family","girl","man"],"emoji":"👨‍👧","order":3150,"group":1,"version":4},{"shortcodes":["family_mgb"],"annotation":"family: man, girl, boy","tags":["boy","family","girl","man"],"emoji":"👨‍👧‍👦","order":3151,"group":1,"version":4},{"shortcodes":["family_mgg"],"annotation":"family: man, girl, girl","tags":["family","girl","man"],"emoji":"👨‍👧‍👧","order":3152,"group":1,"version":4},{"shortcodes":["family_wb"],"annotation":"family: woman, boy","tags":["boy","family","woman"],"emoji":"👩‍👦","order":3153,"group":1,"version":4},{"shortcodes":["family_wbb"],"annotation":"family: woman, boy, boy","tags":["boy","family","woman"],"emoji":"👩‍👦‍👦","order":3154,"group":1,"version":4},{"shortcodes":["family_wg"],"annotation":"family: woman, girl","tags":["family","girl","woman"],"emoji":"👩‍👧","order":3155,"group":1,"version":4},{"shortcodes":["family_wgb"],"annotation":"family: woman, girl, boy","tags":["boy","family","girl","woman"],"emoji":"👩‍👧‍👦","order":3156,"group":1,"version":4},{"shortcodes":["family_wgg"],"annotation":"family: woman, girl, girl","tags":["family","girl","woman"],"emoji":"👩‍👧‍👧","order":3157,"group":1,"version":4},{"shortcodes":["speaking_head"],"annotation":"speaking head","tags":["face","head","silhouette","speak","speaking"],"emoji":"🗣️","order":3159,"group":1,"version":0.7},{"shortcodes":["bust_in_silhouette"],"annotation":"bust in silhouette","tags":["bust","silhouette"],"emoji":"👤","order":3160,"group":1,"version":0.6},{"shortcodes":["busts_in_silhouette"],"annotation":"busts in silhouette","tags":["bust","silhouette"],"emoji":"👥","order":3161,"group":1,"version":1},{"shortcodes":["people_hugging"],"annotation":"people hugging","tags":["goodbye","hello","hug","thanks"],"emoji":"🫂","order":3162,"group":1,"version":13},{"shortcodes":["footprints"],"annotation":"footprints","tags":["clothing","footprint","print"],"emoji":"👣","order":3163,"group":1,"version":0.6},{"shortcodes":["tone1","tone_light"],"annotation":"light skin tone","tags":["skin tone","type 1–2"],"emoji":"🏻","order":3164,"group":2,"version":1},{"shortcodes":["tone2","tone_medium_light"],"annotation":"medium-light skin tone","tags":["skin tone","type 3"],"emoji":"🏼","order":3165,"group":2,"version":1},{"shortcodes":["tone3","tone_medium"],"annotation":"medium skin tone","tags":["skin tone","type 4"],"emoji":"🏽","order":3166,"group":2,"version":1},{"shortcodes":["tone4","tone_medium_dark"],"annotation":"medium-dark skin tone","tags":["skin tone","type 5"],"emoji":"🏾","order":3167,"group":2,"version":1},{"shortcodes":["tone5","tone_dark"],"annotation":"dark skin tone","tags":["skin tone","type 6"],"emoji":"🏿","order":3168,"group":2,"version":1},{"shortcodes":["red_hair"],"annotation":"red hair","tags":["ginger","redhead"],"emoji":"🦰","order":3169,"group":2,"version":11},{"shortcodes":["curly_hair"],"annotation":"curly hair","tags":["afro","curly","ringlets"],"emoji":"🦱","order":3170,"group":2,"version":11},{"shortcodes":["white_hair"],"annotation":"white hair","tags":["gray","hair","old","white"],"emoji":"🦳","order":3171,"group":2,"version":11},{"shortcodes":["no_hair"],"annotation":"bald","tags":["chemotherapy","hairless","no hair","shaven"],"emoji":"🦲","order":3172,"group":2,"version":11},{"shortcodes":["monkey_face"],"annotation":"monkey face","tags":["face","monkey"],"emoji":"🐵","order":3173,"group":3,"version":0.6},{"shortcodes":["monkey"],"annotation":"monkey","tags":["monkey"],"emoji":"🐒","order":3174,"group":3,"version":0.6},{"shortcodes":["gorilla"],"annotation":"gorilla","tags":["gorilla"],"emoji":"🦍","order":3175,"group":3,"version":3},{"shortcodes":["orangutan"],"annotation":"orangutan","tags":["ape"],"emoji":"🦧","order":3176,"group":3,"version":12},{"shortcodes":["dog_face"],"annotation":"dog face","tags":["dog","face","pet"],"emoji":"🐶","order":3177,"group":3,"version":0.6},{"shortcodes":["dog"],"annotation":"dog","tags":["pet"],"emoji":"🐕️","order":3178,"group":3,"version":0.7},{"shortcodes":["guide_dog"],"annotation":"guide dog","tags":["accessibility","blind","guide"],"emoji":"🦮","order":3179,"group":3,"version":12},{"shortcodes":["service_dog"],"annotation":"service dog","tags":["accessibility","assistance","dog","service"],"emoji":"🐕‍🦺","order":3180,"group":3,"version":12},{"shortcodes":["poodle"],"annotation":"poodle","tags":["dog"],"emoji":"🐩","order":3181,"group":3,"version":0.6},{"shortcodes":["wolf","wolf_face"],"annotation":"wolf","tags":["face"],"emoji":"🐺","order":3182,"group":3,"version":0.6},{"shortcodes":["fox","fox_face"],"annotation":"fox","tags":["face"],"emoji":"🦊","order":3183,"group":3,"version":3},{"shortcodes":["raccoon"],"annotation":"raccoon","tags":["curious","sly"],"emoji":"🦝","order":3184,"group":3,"version":11},{"shortcodes":["cat_face"],"annotation":"cat face","tags":["cat","face","pet"],"emoji":"🐱","order":3185,"group":3,"version":0.6},{"shortcodes":["cat"],"annotation":"cat","tags":["pet"],"emoji":"🐈️","order":3186,"group":3,"version":0.7},{"shortcodes":["black_cat"],"annotation":"black cat","tags":["black","cat","unlucky"],"emoji":"🐈‍⬛","order":3187,"group":3,"version":13},{"shortcodes":["lion","lion_face"],"annotation":"lion","tags":["face","leo","zodiac"],"emoji":"🦁","order":3188,"group":3,"version":1},{"shortcodes":["tiger_face"],"annotation":"tiger face","tags":["face","tiger"],"emoji":"🐯","order":3189,"group":3,"version":0.6},{"shortcodes":["tiger"],"annotation":"tiger","tags":["tiger"],"emoji":"🐅","order":3190,"group":3,"version":1},{"shortcodes":["leopard"],"annotation":"leopard","tags":["leopard"],"emoji":"🐆","order":3191,"group":3,"version":1},{"shortcodes":["horse_face"],"annotation":"horse face","tags":["face","horse"],"emoji":"🐴","order":3192,"group":3,"version":0.6},{"shortcodes":["horse","racehorse"],"annotation":"horse","tags":["equestrian","racehorse","racing"],"emoji":"🐎","order":3193,"group":3,"version":0.6},{"shortcodes":["unicorn","unicorn_face"],"annotation":"unicorn","tags":["face"],"emoji":"🦄","order":3194,"group":3,"version":1},{"shortcodes":["zebra"],"annotation":"zebra","tags":["stripe"],"emoji":"🦓","order":3195,"group":3,"version":5},{"shortcodes":["deer"],"annotation":"deer","tags":["deer"],"emoji":"🦌","order":3196,"group":3,"version":3},{"shortcodes":["bison"],"annotation":"bison","tags":["buffalo","herd","wisent"],"emoji":"🦬","order":3197,"group":3,"version":13},{"shortcodes":["cow_face"],"annotation":"cow face","tags":["cow","face"],"emoji":"🐮","order":3198,"group":3,"version":0.6},{"shortcodes":["ox"],"annotation":"ox","tags":["bull","taurus","zodiac"],"emoji":"🐂","order":3199,"group":3,"version":1},{"shortcodes":["water_buffalo"],"annotation":"water buffalo","tags":["buffalo","water"],"emoji":"🐃","order":3200,"group":3,"version":1},{"shortcodes":["cow"],"annotation":"cow","tags":["cow"],"emoji":"🐄","order":3201,"group":3,"version":1},{"shortcodes":["pig_face"],"annotation":"pig face","tags":["face","pig"],"emoji":"🐷","order":3202,"group":3,"version":0.6},{"shortcodes":["pig"],"annotation":"pig","tags":["sow"],"emoji":"🐖","order":3203,"group":3,"version":1},{"shortcodes":["boar"],"annotation":"boar","tags":["pig"],"emoji":"🐗","order":3204,"group":3,"version":0.6},{"shortcodes":["pig_nose"],"annotation":"pig nose","tags":["face","nose","pig"],"emoji":"🐽","order":3205,"group":3,"version":0.6},{"shortcodes":["ram"],"annotation":"ram","tags":["aries","male","sheep","zodiac"],"emoji":"🐏","order":3206,"group":3,"version":1},{"shortcodes":["ewe","sheep"],"annotation":"ewe","tags":["female","sheep"],"emoji":"🐑","order":3207,"group":3,"version":0.6},{"shortcodes":["goat"],"annotation":"goat","tags":["capricorn","zodiac"],"emoji":"🐐","order":3208,"group":3,"version":1},{"shortcodes":["dromedary_camel"],"annotation":"camel","tags":["dromedary","hump"],"emoji":"🐪","order":3209,"group":3,"version":1},{"shortcodes":["camel"],"annotation":"two-hump camel","tags":["bactrian","camel","hump"],"emoji":"🐫","order":3210,"group":3,"version":0.6},{"shortcodes":["llama"],"annotation":"llama","tags":["alpaca","guanaco","vicuña","wool"],"emoji":"🦙","order":3211,"group":3,"version":11},{"shortcodes":["giraffe"],"annotation":"giraffe","tags":["spots"],"emoji":"🦒","order":3212,"group":3,"version":5},{"shortcodes":["elephant"],"annotation":"elephant","tags":["elephant"],"emoji":"🐘","order":3213,"group":3,"version":0.6},{"shortcodes":["mammoth"],"annotation":"mammoth","tags":["extinction","large","tusk","woolly"],"emoji":"🦣","order":3214,"group":3,"version":13},{"shortcodes":["rhino","rhinoceros"],"annotation":"rhinoceros","tags":["rhinoceros"],"emoji":"🦏","order":3215,"group":3,"version":3},{"shortcodes":["hippo"],"annotation":"hippopotamus","tags":["hippo"],"emoji":"🦛","order":3216,"group":3,"version":11},{"shortcodes":["mouse_face"],"annotation":"mouse face","tags":["face","mouse"],"emoji":"🐭","order":3217,"group":3,"version":0.6},{"shortcodes":["mouse"],"annotation":"mouse","tags":["mouse"],"emoji":"🐁","order":3218,"group":3,"version":1},{"shortcodes":["rat"],"annotation":"rat","tags":["rat"],"emoji":"🐀","order":3219,"group":3,"version":1},{"shortcodes":["hamster","hamster_face"],"annotation":"hamster","tags":["face","pet"],"emoji":"🐹","order":3220,"group":3,"version":0.6},{"shortcodes":["rabbit_face"],"annotation":"rabbit face","tags":["bunny","face","pet","rabbit"],"emoji":"🐰","order":3221,"group":3,"version":0.6},{"shortcodes":["rabbit"],"annotation":"rabbit","tags":["bunny","pet"],"emoji":"🐇","order":3222,"group":3,"version":1},{"shortcodes":["chipmunk"],"annotation":"chipmunk","tags":["squirrel"],"emoji":"🐿️","order":3224,"group":3,"version":0.7},{"shortcodes":["beaver"],"annotation":"beaver","tags":["dam"],"emoji":"🦫","order":3225,"group":3,"version":13},{"shortcodes":["hedgehog"],"annotation":"hedgehog","tags":["spiny"],"emoji":"🦔","order":3226,"group":3,"version":5},{"shortcodes":["bat"],"annotation":"bat","tags":["vampire"],"emoji":"🦇","order":3227,"group":3,"version":3},{"shortcodes":["bear","bear_face"],"annotation":"bear","tags":["face"],"emoji":"🐻","order":3228,"group":3,"version":0.6},{"shortcodes":["polar_bear","polar_bear_face"],"annotation":"polar bear","tags":["arctic","bear","white"],"emoji":"🐻‍❄️","order":3229,"group":3,"version":13},{"shortcodes":["koala","koala_face"],"annotation":"koala","tags":["face","marsupial"],"emoji":"🐨","order":3231,"group":3,"version":0.6},{"shortcodes":["panda","panda_face"],"annotation":"panda","tags":["face"],"emoji":"🐼","order":3232,"group":3,"version":0.6},{"shortcodes":["sloth"],"annotation":"sloth","tags":["lazy","slow"],"emoji":"🦥","order":3233,"group":3,"version":12},{"shortcodes":["otter"],"annotation":"otter","tags":["fishing","playful"],"emoji":"🦦","order":3234,"group":3,"version":12},{"shortcodes":["skunk"],"annotation":"skunk","tags":["stink"],"emoji":"🦨","order":3235,"group":3,"version":12},{"shortcodes":["kangaroo"],"annotation":"kangaroo","tags":["australia","joey","jump","marsupial"],"emoji":"🦘","order":3236,"group":3,"version":11},{"shortcodes":["badger"],"annotation":"badger","tags":["honey badger","pester"],"emoji":"🦡","order":3237,"group":3,"version":11},{"shortcodes":["paw_prints"],"annotation":"paw prints","tags":["feet","paw","print"],"emoji":"🐾","order":3238,"group":3,"version":0.6},{"shortcodes":["turkey"],"annotation":"turkey","tags":["bird"],"emoji":"🦃","order":3239,"group":3,"version":1},{"shortcodes":["chicken","chicken_face"],"annotation":"chicken","tags":["bird"],"emoji":"🐔","order":3240,"group":3,"version":0.6},{"shortcodes":["rooster"],"annotation":"rooster","tags":["bird"],"emoji":"🐓","order":3241,"group":3,"version":1},{"shortcodes":["hatching_chick"],"annotation":"hatching chick","tags":["baby","bird","chick","hatching"],"emoji":"🐣","order":3242,"group":3,"version":0.6},{"shortcodes":["baby_chick"],"annotation":"baby chick","tags":["baby","bird","chick"],"emoji":"🐤","order":3243,"group":3,"version":0.6},{"shortcodes":["hatched_chick"],"annotation":"front-facing baby chick","tags":["baby","bird","chick"],"emoji":"🐥","order":3244,"group":3,"version":0.6},{"shortcodes":["bird","bird_face"],"annotation":"bird","tags":["bird"],"emoji":"🐦️","order":3245,"group":3,"version":0.6},{"shortcodes":["penguin","penguin_face"],"annotation":"penguin","tags":["bird"],"emoji":"🐧","order":3246,"group":3,"version":0.6},{"shortcodes":["dove"],"annotation":"dove","tags":["bird","fly","peace"],"emoji":"🕊️","order":3248,"group":3,"version":0.7},{"shortcodes":["eagle"],"annotation":"eagle","tags":["bird"],"emoji":"🦅","order":3249,"group":3,"version":3},{"shortcodes":["duck"],"annotation":"duck","tags":["bird"],"emoji":"🦆","order":3250,"group":3,"version":3},{"shortcodes":["swan"],"annotation":"swan","tags":["bird","cygnet","ugly duckling"],"emoji":"🦢","order":3251,"group":3,"version":11},{"shortcodes":["owl"],"annotation":"owl","tags":["bird","wise"],"emoji":"🦉","order":3252,"group":3,"version":3},{"shortcodes":["dodo"],"annotation":"dodo","tags":["extinction","large","mauritius"],"emoji":"🦤","order":3253,"group":3,"version":13},{"shortcodes":["feather"],"annotation":"feather","tags":["bird","flight","light","plumage"],"emoji":"🪶","order":3254,"group":3,"version":13},{"shortcodes":["flamingo"],"annotation":"flamingo","tags":["flamboyant","tropical"],"emoji":"🦩","order":3255,"group":3,"version":12},{"shortcodes":["peacock"],"annotation":"peacock","tags":["bird","ostentatious","peahen","proud"],"emoji":"🦚","order":3256,"group":3,"version":11},{"shortcodes":["parrot"],"annotation":"parrot","tags":["bird","pirate","talk"],"emoji":"🦜","order":3257,"group":3,"version":11},{"shortcodes":["frog","frog_face"],"annotation":"frog","tags":["face"],"emoji":"🐸","order":3258,"group":3,"version":0.6},{"shortcodes":["crocodile"],"annotation":"crocodile","tags":["crocodile"],"emoji":"🐊","order":3259,"group":3,"version":1},{"shortcodes":["turtle"],"annotation":"turtle","tags":["terrapin","tortoise"],"emoji":"🐢","order":3260,"group":3,"version":0.6},{"shortcodes":["lizard"],"annotation":"lizard","tags":["reptile"],"emoji":"🦎","order":3261,"group":3,"version":3},{"shortcodes":["snake"],"annotation":"snake","tags":["bearer","ophiuchus","serpent","zodiac"],"emoji":"🐍","order":3262,"group":3,"version":0.6},{"shortcodes":["dragon_face"],"annotation":"dragon face","tags":["dragon","face","fairy tale"],"emoji":"🐲","order":3263,"group":3,"version":0.6},{"shortcodes":["dragon"],"annotation":"dragon","tags":["fairy tale"],"emoji":"🐉","order":3264,"group":3,"version":1},{"shortcodes":["sauropod"],"annotation":"sauropod","tags":["brachiosaurus","brontosaurus","diplodocus"],"emoji":"🦕","order":3265,"group":3,"version":5},{"shortcodes":["t-rex","trex"],"annotation":"T-Rex","tags":["t-rex","tyrannosaurus rex"],"emoji":"🦖","order":3266,"group":3,"version":5},{"shortcodes":["spouting_whale"],"annotation":"spouting whale","tags":["face","spouting","whale"],"emoji":"🐳","order":3267,"group":3,"version":0.6},{"shortcodes":["whale"],"annotation":"whale","tags":["whale"],"emoji":"🐋","order":3268,"group":3,"version":1},{"shortcodes":["dolphin"],"annotation":"dolphin","tags":["flipper"],"emoji":"🐬","order":3269,"group":3,"version":0.6},{"shortcodes":["seal"],"annotation":"seal","tags":["sea lion"],"emoji":"🦭","order":3270,"group":3,"version":13},{"shortcodes":["fish"],"annotation":"fish","tags":["pisces","zodiac"],"emoji":"🐟️","order":3271,"group":3,"version":0.6},{"shortcodes":["tropical_fish"],"annotation":"tropical fish","tags":["fish","tropical"],"emoji":"🐠","order":3272,"group":3,"version":0.6},{"shortcodes":["blowfish"],"annotation":"blowfish","tags":["fish"],"emoji":"🐡","order":3273,"group":3,"version":0.6},{"shortcodes":["shark"],"annotation":"shark","tags":["fish"],"emoji":"🦈","order":3274,"group":3,"version":3},{"shortcodes":["octopus"],"annotation":"octopus","tags":["octopus"],"emoji":"🐙","order":3275,"group":3,"version":0.6},{"shortcodes":["shell"],"annotation":"spiral shell","tags":["shell","spiral"],"emoji":"🐚","order":3276,"group":3,"version":0.6},{"shortcodes":["coral"],"annotation":"coral","tags":["ocean","reef"],"emoji":"🪸","order":3277,"group":3,"version":14},{"shortcodes":["snail"],"annotation":"snail","tags":["snail"],"emoji":"🐌","order":3278,"group":3,"version":0.6},{"shortcodes":["butterfly"],"annotation":"butterfly","tags":["insect","pretty"],"emoji":"🦋","order":3279,"group":3,"version":3},{"shortcodes":["bug"],"annotation":"bug","tags":["insect"],"emoji":"🐛","order":3280,"group":3,"version":0.6},{"shortcodes":["ant"],"annotation":"ant","tags":["insect"],"emoji":"🐜","order":3281,"group":3,"version":0.6},{"shortcodes":["bee"],"annotation":"honeybee","tags":["bee","insect"],"emoji":"🐝","order":3282,"group":3,"version":0.6},{"shortcodes":["beetle"],"annotation":"beetle","tags":["bug","insect"],"emoji":"🪲","order":3283,"group":3,"version":13},{"shortcodes":["lady_beetle"],"annotation":"lady beetle","tags":["beetle","insect","ladybird","ladybug"],"emoji":"🐞","order":3284,"group":3,"version":0.6},{"shortcodes":["cricket"],"annotation":"cricket","tags":["grasshopper"],"emoji":"🦗","order":3285,"group":3,"version":5},{"shortcodes":["cockroach"],"annotation":"cockroach","tags":["insect","pest","roach"],"emoji":"🪳","order":3286,"group":3,"version":13},{"shortcodes":["spider"],"annotation":"spider","tags":["insect"],"emoji":"🕷️","order":3288,"group":3,"version":0.7},{"shortcodes":["spider_web"],"annotation":"spider web","tags":["spider","web"],"emoji":"🕸️","order":3290,"group":3,"version":0.7},{"shortcodes":["scorpion"],"annotation":"scorpion","tags":["scorpio","zodiac"],"emoji":"🦂","order":3291,"group":3,"version":1},{"shortcodes":["mosquito"],"annotation":"mosquito","tags":["disease","fever","malaria","pest","virus"],"emoji":"🦟","order":3292,"group":3,"version":11},{"shortcodes":["fly"],"annotation":"fly","tags":["disease","maggot","pest","rotting"],"emoji":"🪰","order":3293,"group":3,"version":13},{"shortcodes":["worm"],"annotation":"worm","tags":["annelid","earthworm","parasite"],"emoji":"🪱","order":3294,"group":3,"version":13},{"shortcodes":["microbe"],"annotation":"microbe","tags":["amoeba","bacteria","virus"],"emoji":"🦠","order":3295,"group":3,"version":11},{"shortcodes":["bouquet"],"annotation":"bouquet","tags":["flower"],"emoji":"💐","order":3296,"group":3,"version":0.6},{"shortcodes":["cherry_blossom"],"annotation":"cherry blossom","tags":["blossom","cherry","flower"],"emoji":"🌸","order":3297,"group":3,"version":0.6},{"shortcodes":["white_flower"],"annotation":"white flower","tags":["flower"],"emoji":"💮","order":3298,"group":3,"version":0.6},{"shortcodes":["lotus"],"annotation":"lotus","tags":["buddhism","flower","hinduism","india","purity","vietnam"],"emoji":"🪷","order":3299,"group":3,"version":14},{"shortcodes":["rosette"],"annotation":"rosette","tags":["plant"],"emoji":"🏵️","order":3301,"group":3,"version":0.7},{"shortcodes":["rose"],"annotation":"rose","tags":["flower"],"emoji":"🌹","order":3302,"group":3,"version":0.6},{"shortcodes":["wilted_flower"],"annotation":"wilted flower","tags":["flower","wilted"],"emoji":"🥀","order":3303,"group":3,"version":3},{"shortcodes":["hibiscus"],"annotation":"hibiscus","tags":["flower"],"emoji":"🌺","order":3304,"group":3,"version":0.6},{"shortcodes":["sunflower"],"annotation":"sunflower","tags":["flower","sun"],"emoji":"🌻","order":3305,"group":3,"version":0.6},{"shortcodes":["blossom"],"annotation":"blossom","tags":["flower"],"emoji":"🌼","order":3306,"group":3,"version":0.6},{"shortcodes":["tulip"],"annotation":"tulip","tags":["flower"],"emoji":"🌷","order":3307,"group":3,"version":0.6},{"shortcodes":["seedling"],"annotation":"seedling","tags":["young"],"emoji":"🌱","order":3308,"group":3,"version":0.6},{"shortcodes":["potted_plant"],"annotation":"potted plant","tags":["boring","grow","house","nurturing","plant","useless"],"emoji":"🪴","order":3309,"group":3,"version":13},{"shortcodes":["evergreen_tree"],"annotation":"evergreen tree","tags":["tree"],"emoji":"🌲","order":3310,"group":3,"version":1},{"shortcodes":["deciduous_tree"],"annotation":"deciduous tree","tags":["deciduous","shedding","tree"],"emoji":"🌳","order":3311,"group":3,"version":1},{"shortcodes":["palm_tree"],"annotation":"palm tree","tags":["palm","tree"],"emoji":"🌴","order":3312,"group":3,"version":0.6},{"shortcodes":["cactus"],"annotation":"cactus","tags":["plant"],"emoji":"🌵","order":3313,"group":3,"version":0.6},{"shortcodes":["ear_of_rice","sheaf_of_rice"],"annotation":"sheaf of rice","tags":["ear","grain","rice"],"emoji":"🌾","order":3314,"group":3,"version":0.6},{"shortcodes":["herb"],"annotation":"herb","tags":["leaf"],"emoji":"🌿","order":3315,"group":3,"version":0.6},{"shortcodes":["shamrock"],"annotation":"shamrock","tags":["plant"],"emoji":"☘️","order":3317,"group":3,"version":1},{"shortcodes":["four_leaf_clover"],"annotation":"four leaf clover","tags":["4","clover","four","four-leaf clover","leaf"],"emoji":"🍀","order":3318,"group":3,"version":0.6},{"shortcodes":["maple_leaf"],"annotation":"maple leaf","tags":["falling","leaf","maple"],"emoji":"🍁","order":3319,"group":3,"version":0.6},{"shortcodes":["fallen_leaf"],"annotation":"fallen leaf","tags":["falling","leaf"],"emoji":"🍂","order":3320,"group":3,"version":0.6},{"shortcodes":["leaves"],"annotation":"leaf fluttering in wind","tags":["blow","flutter","leaf","wind"],"emoji":"🍃","order":3321,"group":3,"version":0.6},{"shortcodes":["empty_nest","nest"],"annotation":"empty nest","tags":["nesting"],"emoji":"🪹","order":3322,"group":3,"version":14},{"shortcodes":["nest_with_eggs"],"annotation":"nest with eggs","tags":["nesting"],"emoji":"🪺","order":3323,"group":3,"version":14},{"shortcodes":["grapes"],"annotation":"grapes","tags":["fruit","grape"],"emoji":"🍇","order":3324,"group":4,"version":0.6},{"shortcodes":["melon"],"annotation":"melon","tags":["fruit"],"emoji":"🍈","order":3325,"group":4,"version":0.6},{"shortcodes":["watermelon"],"annotation":"watermelon","tags":["fruit"],"emoji":"🍉","order":3326,"group":4,"version":0.6},{"shortcodes":["orange","tangerine"],"annotation":"tangerine","tags":["fruit","orange"],"emoji":"🍊","order":3327,"group":4,"version":0.6},{"shortcodes":["lemon"],"annotation":"lemon","tags":["citrus","fruit"],"emoji":"🍋","order":3328,"group":4,"version":1},{"shortcodes":["banana"],"annotation":"banana","tags":["fruit"],"emoji":"🍌","order":3329,"group":4,"version":0.6},{"shortcodes":["pineapple"],"annotation":"pineapple","tags":["fruit"],"emoji":"🍍","order":3330,"group":4,"version":0.6},{"shortcodes":["mango"],"annotation":"mango","tags":["fruit","tropical"],"emoji":"🥭","order":3331,"group":4,"version":11},{"shortcodes":["apple","red_apple"],"annotation":"red apple","tags":["apple","fruit","red"],"emoji":"🍎","order":3332,"group":4,"version":0.6},{"shortcodes":["green_apple"],"annotation":"green apple","tags":["apple","fruit","green"],"emoji":"🍏","order":3333,"group":4,"version":0.6},{"shortcodes":["pear"],"annotation":"pear","tags":["fruit"],"emoji":"🍐","order":3334,"group":4,"version":1},{"shortcodes":["peach"],"annotation":"peach","tags":["fruit"],"emoji":"🍑","order":3335,"group":4,"version":0.6},{"shortcodes":["cherries"],"annotation":"cherries","tags":["berries","cherry","fruit","red"],"emoji":"🍒","order":3336,"group":4,"version":0.6},{"shortcodes":["strawberry"],"annotation":"strawberry","tags":["berry","fruit"],"emoji":"🍓","order":3337,"group":4,"version":0.6},{"shortcodes":["blueberries"],"annotation":"blueberries","tags":["berry","bilberry","blue","blueberry"],"emoji":"🫐","order":3338,"group":4,"version":13},{"shortcodes":["kiwi"],"annotation":"kiwi fruit","tags":["food","fruit","kiwi"],"emoji":"🥝","order":3339,"group":4,"version":3},{"shortcodes":["tomato"],"annotation":"tomato","tags":["fruit","vegetable"],"emoji":"🍅","order":3340,"group":4,"version":0.6},{"shortcodes":["olive"],"annotation":"olive","tags":["food"],"emoji":"🫒","order":3341,"group":4,"version":13},{"shortcodes":["coconut"],"annotation":"coconut","tags":["palm","piña colada"],"emoji":"🥥","order":3342,"group":4,"version":5},{"shortcodes":["avocado"],"annotation":"avocado","tags":["food","fruit"],"emoji":"🥑","order":3343,"group":4,"version":3},{"shortcodes":["eggplant"],"annotation":"eggplant","tags":["aubergine","vegetable"],"emoji":"🍆","order":3344,"group":4,"version":0.6},{"shortcodes":["potato"],"annotation":"potato","tags":["food","vegetable"],"emoji":"🥔","order":3345,"group":4,"version":3},{"shortcodes":["carrot"],"annotation":"carrot","tags":["food","vegetable"],"emoji":"🥕","order":3346,"group":4,"version":3},{"shortcodes":["corn","ear_of_corn"],"annotation":"ear of corn","tags":["corn","ear","maize","maze"],"emoji":"🌽","order":3347,"group":4,"version":0.6},{"shortcodes":["hot_pepper"],"annotation":"hot pepper","tags":["hot","pepper"],"emoji":"🌶️","order":3349,"group":4,"version":0.7},{"shortcodes":["bell_pepper"],"annotation":"bell pepper","tags":["capsicum","pepper","vegetable"],"emoji":"🫑","order":3350,"group":4,"version":13},{"shortcodes":["cucumber"],"annotation":"cucumber","tags":["food","pickle","vegetable"],"emoji":"🥒","order":3351,"group":4,"version":3},{"shortcodes":["leafy_green"],"annotation":"leafy green","tags":["bok choy","cabbage","kale","lettuce"],"emoji":"🥬","order":3352,"group":4,"version":11},{"shortcodes":["broccoli"],"annotation":"broccoli","tags":["wild cabbage"],"emoji":"🥦","order":3353,"group":4,"version":5},{"shortcodes":["garlic"],"annotation":"garlic","tags":["flavoring"],"emoji":"🧄","order":3354,"group":4,"version":12},{"shortcodes":["onion"],"annotation":"onion","tags":["flavoring"],"emoji":"🧅","order":3355,"group":4,"version":12},{"shortcodes":["mushroom"],"annotation":"mushroom","tags":["toadstool"],"emoji":"🍄","order":3356,"group":4,"version":0.6},{"shortcodes":["peanuts"],"annotation":"peanuts","tags":["food","nut","peanut","vegetable"],"emoji":"🥜","order":3357,"group":4,"version":3},{"shortcodes":["beans"],"annotation":"beans","tags":["food","kidney","legume"],"emoji":"🫘","order":3358,"group":4,"version":14},{"shortcodes":["chestnut"],"annotation":"chestnut","tags":["plant"],"emoji":"🌰","order":3359,"group":4,"version":0.6},{"shortcodes":["bread"],"annotation":"bread","tags":["loaf"],"emoji":"🍞","order":3360,"group":4,"version":0.6},{"shortcodes":["croissant"],"annotation":"croissant","tags":["bread","breakfast","food","french","roll"],"emoji":"🥐","order":3361,"group":4,"version":3},{"shortcodes":["baguette_bread"],"annotation":"baguette bread","tags":["baguette","bread","food","french"],"emoji":"🥖","order":3362,"group":4,"version":3},{"shortcodes":["flatbread"],"annotation":"flatbread","tags":["arepa","lavash","naan","pita"],"emoji":"🫓","order":3363,"group":4,"version":13},{"shortcodes":["pretzel"],"annotation":"pretzel","tags":["twisted"],"emoji":"🥨","order":3364,"group":4,"version":5},{"shortcodes":["bagel"],"annotation":"bagel","tags":["bakery","breakfast","schmear"],"emoji":"🥯","order":3365,"group":4,"version":11},{"shortcodes":["pancakes"],"annotation":"pancakes","tags":["breakfast","crêpe","food","hotcake","pancake"],"emoji":"🥞","order":3366,"group":4,"version":3},{"shortcodes":["waffle"],"annotation":"waffle","tags":["breakfast","indecisive","iron"],"emoji":"🧇","order":3367,"group":4,"version":12},{"shortcodes":["cheese"],"annotation":"cheese wedge","tags":["cheese"],"emoji":"🧀","order":3368,"group":4,"version":1},{"shortcodes":["meat_on_bone"],"annotation":"meat on bone","tags":["bone","meat"],"emoji":"🍖","order":3369,"group":4,"version":0.6},{"shortcodes":["poultry_leg"],"annotation":"poultry leg","tags":["bone","chicken","drumstick","leg","poultry"],"emoji":"🍗","order":3370,"group":4,"version":0.6},{"shortcodes":["cut_of_meat"],"annotation":"cut of meat","tags":["chop","lambchop","porkchop","steak"],"emoji":"🥩","order":3371,"group":4,"version":5},{"shortcodes":["bacon"],"annotation":"bacon","tags":["breakfast","food","meat"],"emoji":"🥓","order":3372,"group":4,"version":3},{"shortcodes":["hamburger"],"annotation":"hamburger","tags":["burger"],"emoji":"🍔","order":3373,"group":4,"version":0.6},{"shortcodes":["french_fries","fries"],"annotation":"french fries","tags":["french","fries"],"emoji":"🍟","order":3374,"group":4,"version":0.6},{"shortcodes":["pizza"],"annotation":"pizza","tags":["cheese","slice"],"emoji":"🍕","order":3375,"group":4,"version":0.6},{"shortcodes":["hotdog"],"annotation":"hot dog","tags":["frankfurter","hotdog","sausage"],"emoji":"🌭","order":3376,"group":4,"version":1},{"shortcodes":["sandwich"],"annotation":"sandwich","tags":["bread"],"emoji":"🥪","order":3377,"group":4,"version":5},{"shortcodes":["taco"],"annotation":"taco","tags":["mexican"],"emoji":"🌮","order":3378,"group":4,"version":1},{"shortcodes":["burrito"],"annotation":"burrito","tags":["mexican","wrap"],"emoji":"🌯","order":3379,"group":4,"version":1},{"shortcodes":["tamale"],"annotation":"tamale","tags":["mexican","wrapped"],"emoji":"🫔","order":3380,"group":4,"version":13},{"shortcodes":["stuffed_flatbread"],"annotation":"stuffed flatbread","tags":["falafel","flatbread","food","gyro","kebab","stuffed"],"emoji":"🥙","order":3381,"group":4,"version":3},{"shortcodes":["falafel"],"annotation":"falafel","tags":["chickpea","meatball"],"emoji":"🧆","order":3382,"group":4,"version":12},{"shortcodes":["egg"],"annotation":"egg","tags":["breakfast","food"],"emoji":"🥚","order":3383,"group":4,"version":3},{"shortcodes":["cooking","fried_egg"],"annotation":"cooking","tags":["breakfast","egg","frying","pan"],"emoji":"🍳","order":3384,"group":4,"version":0.6},{"shortcodes":["shallow_pan_of_food"],"annotation":"shallow pan of food","tags":["casserole","food","paella","pan","shallow"],"emoji":"🥘","order":3385,"group":4,"version":3},{"shortcodes":["pot_of_food","stew"],"annotation":"pot of food","tags":["pot","stew"],"emoji":"🍲","order":3386,"group":4,"version":0.6},{"shortcodes":["fondue"],"annotation":"fondue","tags":["cheese","chocolate","melted","pot","swiss"],"emoji":"🫕","order":3387,"group":4,"version":13},{"shortcodes":["bowl_with_spoon"],"annotation":"bowl with spoon","tags":["breakfast","cereal","congee"],"emoji":"🥣","order":3388,"group":4,"version":5},{"shortcodes":["green_salad","salad"],"annotation":"green salad","tags":["food","green","salad"],"emoji":"🥗","order":3389,"group":4,"version":3},{"shortcodes":["popcorn"],"annotation":"popcorn","tags":["popcorn"],"emoji":"🍿","order":3390,"group":4,"version":1},{"shortcodes":["butter"],"annotation":"butter","tags":["dairy"],"emoji":"🧈","order":3391,"group":4,"version":12},{"shortcodes":["salt"],"annotation":"salt","tags":["condiment","shaker"],"emoji":"🧂","order":3392,"group":4,"version":11},{"shortcodes":["canned_food"],"annotation":"canned food","tags":["can"],"emoji":"🥫","order":3393,"group":4,"version":5},{"shortcodes":["bento","bento_box"],"annotation":"bento box","tags":["bento","box"],"emoji":"🍱","order":3394,"group":4,"version":0.6},{"shortcodes":["rice_cracker"],"annotation":"rice cracker","tags":["cracker","rice"],"emoji":"🍘","order":3395,"group":4,"version":0.6},{"shortcodes":["rice_ball"],"annotation":"rice ball","tags":["ball","japanese","rice"],"emoji":"🍙","order":3396,"group":4,"version":0.6},{"shortcodes":["cooked_rice","rice"],"annotation":"cooked rice","tags":["cooked","rice"],"emoji":"🍚","order":3397,"group":4,"version":0.6},{"shortcodes":["curry","curry_rice"],"annotation":"curry rice","tags":["curry","rice"],"emoji":"🍛","order":3398,"group":4,"version":0.6},{"shortcodes":["ramen","steaming_bowl"],"annotation":"steaming bowl","tags":["bowl","noodle","ramen","steaming"],"emoji":"🍜","order":3399,"group":4,"version":0.6},{"shortcodes":["spaghetti"],"annotation":"spaghetti","tags":["pasta"],"emoji":"🍝","order":3400,"group":4,"version":0.6},{"shortcodes":["sweet_potato"],"annotation":"roasted sweet potato","tags":["potato","roasted","sweet"],"emoji":"🍠","order":3401,"group":4,"version":0.6},{"shortcodes":["oden"],"annotation":"oden","tags":["kebab","seafood","skewer","stick"],"emoji":"🍢","order":3402,"group":4,"version":0.6},{"shortcodes":["sushi"],"annotation":"sushi","tags":["sushi"],"emoji":"🍣","order":3403,"group":4,"version":0.6},{"shortcodes":["fried_shrimp"],"annotation":"fried shrimp","tags":["fried","prawn","shrimp","tempura"],"emoji":"🍤","order":3404,"group":4,"version":0.6},{"shortcodes":["fish_cake"],"annotation":"fish cake with swirl","tags":["cake","fish","pastry","swirl"],"emoji":"🍥","order":3405,"group":4,"version":0.6},{"shortcodes":["moon_cake"],"annotation":"moon cake","tags":["autumn","festival","yuèbǐng"],"emoji":"🥮","order":3406,"group":4,"version":11},{"shortcodes":["dango"],"annotation":"dango","tags":["dessert","japanese","skewer","stick","sweet"],"emoji":"🍡","order":3407,"group":4,"version":0.6},{"shortcodes":["dumpling"],"annotation":"dumpling","tags":["empanada","gyōza","jiaozi","pierogi","potsticker"],"emoji":"🥟","order":3408,"group":4,"version":5},{"shortcodes":["fortune_cookie"],"annotation":"fortune cookie","tags":["prophecy"],"emoji":"🥠","order":3409,"group":4,"version":5},{"shortcodes":["takeout_box"],"annotation":"takeout box","tags":["oyster pail"],"emoji":"🥡","order":3410,"group":4,"version":5},{"shortcodes":["crab"],"annotation":"crab","tags":["cancer","zodiac"],"emoji":"🦀","order":3411,"group":4,"version":1},{"shortcodes":["lobster"],"annotation":"lobster","tags":["bisque","claws","seafood"],"emoji":"🦞","order":3412,"group":4,"version":11},{"shortcodes":["shrimp"],"annotation":"shrimp","tags":["food","shellfish","small"],"emoji":"🦐","order":3413,"group":4,"version":3},{"shortcodes":["squid"],"annotation":"squid","tags":["food","molusc"],"emoji":"🦑","order":3414,"group":4,"version":3},{"shortcodes":["oyster"],"annotation":"oyster","tags":["diving","pearl"],"emoji":"🦪","order":3415,"group":4,"version":12},{"shortcodes":["icecream","soft_serve"],"annotation":"soft ice cream","tags":["cream","dessert","ice","icecream","soft","sweet"],"emoji":"🍦","order":3416,"group":4,"version":0.6},{"shortcodes":["shaved_ice"],"annotation":"shaved ice","tags":["dessert","ice","shaved","sweet"],"emoji":"🍧","order":3417,"group":4,"version":0.6},{"shortcodes":["ice_cream"],"annotation":"ice cream","tags":["cream","dessert","ice","sweet"],"emoji":"🍨","order":3418,"group":4,"version":0.6},{"shortcodes":["doughnut"],"annotation":"doughnut","tags":["breakfast","dessert","donut","sweet"],"emoji":"🍩","order":3419,"group":4,"version":0.6},{"shortcodes":["cookie"],"annotation":"cookie","tags":["dessert","sweet"],"emoji":"🍪","order":3420,"group":4,"version":0.6},{"shortcodes":["birthday","birthday_cake"],"annotation":"birthday cake","tags":["birthday","cake","celebration","dessert","pastry","sweet"],"emoji":"🎂","order":3421,"group":4,"version":0.6},{"shortcodes":["cake","shortcake"],"annotation":"shortcake","tags":["cake","dessert","pastry","slice","sweet"],"emoji":"🍰","order":3422,"group":4,"version":0.6},{"shortcodes":["cupcake"],"annotation":"cupcake","tags":["bakery","sweet"],"emoji":"🧁","order":3423,"group":4,"version":11},{"shortcodes":["pie"],"annotation":"pie","tags":["filling","pastry"],"emoji":"🥧","order":3424,"group":4,"version":5},{"shortcodes":["chocolate_bar"],"annotation":"chocolate bar","tags":["bar","chocolate","dessert","sweet"],"emoji":"🍫","order":3425,"group":4,"version":0.6},{"shortcodes":["candy"],"annotation":"candy","tags":["dessert","sweet"],"emoji":"🍬","order":3426,"group":4,"version":0.6},{"shortcodes":["lollipop"],"annotation":"lollipop","tags":["candy","dessert","sweet"],"emoji":"🍭","order":3427,"group":4,"version":0.6},{"shortcodes":["custard"],"annotation":"custard","tags":["dessert","pudding","sweet"],"emoji":"🍮","order":3428,"group":4,"version":0.6},{"shortcodes":["honey_pot"],"annotation":"honey pot","tags":["honey","honeypot","pot","sweet"],"emoji":"🍯","order":3429,"group":4,"version":0.6},{"shortcodes":["baby_bottle"],"annotation":"baby bottle","tags":["baby","bottle","drink","milk"],"emoji":"🍼","order":3430,"group":4,"version":1},{"shortcodes":["glass_of_milk","milk"],"annotation":"glass of milk","tags":["drink","glass","milk"],"emoji":"🥛","order":3431,"group":4,"version":3},{"shortcodes":["coffee"],"annotation":"hot beverage","tags":["beverage","coffee","drink","hot","steaming","tea"],"emoji":"☕️","order":3432,"group":4,"version":0.6},{"shortcodes":["teapot"],"annotation":"teapot","tags":["drink","pot","tea"],"emoji":"🫖","order":3433,"group":4,"version":13},{"shortcodes":["tea"],"annotation":"teacup without handle","tags":["beverage","cup","drink","tea","teacup"],"emoji":"🍵","order":3434,"group":4,"version":0.6},{"shortcodes":["sake"],"annotation":"sake","tags":["bar","beverage","bottle","cup","drink"],"emoji":"🍶","order":3435,"group":4,"version":0.6},{"shortcodes":["champagne"],"annotation":"bottle with popping cork","tags":["bar","bottle","cork","drink","popping"],"emoji":"🍾","order":3436,"group":4,"version":1},{"shortcodes":["wine_glass"],"annotation":"wine glass","tags":["bar","beverage","drink","glass","wine"],"emoji":"🍷","order":3437,"group":4,"version":0.6},{"shortcodes":["cocktail"],"annotation":"cocktail glass","tags":["bar","cocktail","drink","glass"],"emoji":"🍸️","order":3438,"group":4,"version":0.6},{"shortcodes":["tropical_drink"],"annotation":"tropical drink","tags":["bar","drink","tropical"],"emoji":"🍹","order":3439,"group":4,"version":0.6},{"shortcodes":["beer"],"annotation":"beer mug","tags":["bar","beer","drink","mug"],"emoji":"🍺","order":3440,"group":4,"version":0.6},{"shortcodes":["beers"],"annotation":"clinking beer mugs","tags":["bar","beer","clink","drink","mug"],"emoji":"🍻","order":3441,"group":4,"version":0.6},{"shortcodes":["clinking_glasses"],"annotation":"clinking glasses","tags":["celebrate","clink","drink","glass"],"emoji":"🥂","order":3442,"group":4,"version":3},{"shortcodes":["tumbler_glass","whisky"],"annotation":"tumbler glass","tags":["glass","liquor","shot","tumbler","whisky"],"emoji":"🥃","order":3443,"group":4,"version":3},{"shortcodes":["pour","pouring_liquid"],"annotation":"pouring liquid","tags":["drink","empty","glass","spill"],"emoji":"🫗","order":3444,"group":4,"version":14},{"shortcodes":["cup_with_straw"],"annotation":"cup with straw","tags":["juice","soda"],"emoji":"🥤","order":3445,"group":4,"version":5},{"shortcodes":["boba_drink","bubble_tea"],"annotation":"bubble tea","tags":["bubble","milk","pearl","tea"],"emoji":"🧋","order":3446,"group":4,"version":13},{"shortcodes":["beverage_box","juice_box"],"annotation":"beverage box","tags":["beverage","box","juice","straw","sweet"],"emoji":"🧃","order":3447,"group":4,"version":12},{"shortcodes":["mate"],"annotation":"mate","tags":["drink"],"emoji":"🧉","order":3448,"group":4,"version":12},{"shortcodes":["ice","ice_cube"],"annotation":"ice","tags":["cold","ice cube","iceberg"],"emoji":"🧊","order":3449,"group":4,"version":12},{"shortcodes":["chopsticks"],"annotation":"chopsticks","tags":["hashi"],"emoji":"🥢","order":3450,"group":4,"version":5},{"shortcodes":["fork_knife_plate"],"annotation":"fork and knife with plate","tags":["cooking","fork","knife","plate"],"emoji":"🍽️","order":3452,"group":4,"version":0.7},{"shortcodes":["fork_and_knife"],"annotation":"fork and knife","tags":["cooking","cutlery","fork","knife"],"emoji":"🍴","order":3453,"group":4,"version":0.6},{"shortcodes":["spoon"],"annotation":"spoon","tags":["tableware"],"emoji":"🥄","order":3454,"group":4,"version":3},{"shortcodes":["knife"],"annotation":"kitchen knife","tags":["cooking","hocho","knife","tool","weapon"],"emoji":"🔪","order":3455,"group":4,"version":0.6},{"shortcodes":["jar"],"annotation":"jar","tags":["condiment","container","empty","sauce","store"],"emoji":"🫙","order":3456,"group":4,"version":14},{"shortcodes":["amphora"],"annotation":"amphora","tags":["aquarius","cooking","drink","jug","zodiac"],"emoji":"🏺","order":3457,"group":4,"version":1},{"shortcodes":["earth_africa","earth_europe"],"annotation":"globe showing Europe-Africa","tags":["africa","earth","europe","globe","globe showing europe-africa","world"],"emoji":"🌍️","order":3458,"group":5,"version":0.7},{"shortcodes":["earth_americas"],"annotation":"globe showing Americas","tags":["americas","earth","globe","globe showing americas","world"],"emoji":"🌎️","order":3459,"group":5,"version":0.7},{"shortcodes":["earth_asia"],"annotation":"globe showing Asia-Australia","tags":["asia","australia","earth","globe","globe showing asia-australia","world"],"emoji":"🌏️","order":3460,"group":5,"version":0.6},{"shortcodes":["globe_with_meridians"],"annotation":"globe with meridians","tags":["earth","globe","meridians","world"],"emoji":"🌐","order":3461,"group":5,"version":1},{"shortcodes":["world_map"],"annotation":"world map","tags":["map","world"],"emoji":"🗺️","order":3463,"group":5,"version":0.7},{"shortcodes":["japan_map"],"annotation":"map of Japan","tags":["japan","map","map of japan"],"emoji":"🗾","order":3464,"group":5,"version":0.6},{"shortcodes":["compass"],"annotation":"compass","tags":["magnetic","navigation","orienteering"],"emoji":"🧭","order":3465,"group":5,"version":11},{"shortcodes":["mountain_snow"],"annotation":"snow-capped mountain","tags":["cold","mountain","snow"],"emoji":"🏔️","order":3467,"group":5,"version":0.7},{"shortcodes":["mountain"],"annotation":"mountain","tags":["mountain"],"emoji":"⛰️","order":3469,"group":5,"version":0.7},{"shortcodes":["volcano"],"annotation":"volcano","tags":["eruption","mountain"],"emoji":"🌋","order":3470,"group":5,"version":0.6},{"shortcodes":["mount_fuji"],"annotation":"mount fuji","tags":["fuji","mountain"],"emoji":"🗻","order":3471,"group":5,"version":0.6},{"shortcodes":["camping"],"annotation":"camping","tags":["camping"],"emoji":"🏕️","order":3473,"group":5,"version":0.7},{"shortcodes":["beach","beach_with_umbrella"],"annotation":"beach with umbrella","tags":["beach","umbrella"],"emoji":"🏖️","order":3475,"group":5,"version":0.7},{"shortcodes":["desert"],"annotation":"desert","tags":["desert"],"emoji":"🏜️","order":3477,"group":5,"version":0.7},{"shortcodes":["desert_island","island"],"annotation":"desert island","tags":["desert","island"],"emoji":"🏝️","order":3479,"group":5,"version":0.7},{"shortcodes":["national_park"],"annotation":"national park","tags":["park"],"emoji":"🏞️","order":3481,"group":5,"version":0.7},{"shortcodes":["stadium"],"annotation":"stadium","tags":["stadium"],"emoji":"🏟️","order":3483,"group":5,"version":0.7},{"shortcodes":["classical_building"],"annotation":"classical building","tags":["classical"],"emoji":"🏛️","order":3485,"group":5,"version":0.7},{"shortcodes":["building_construction","construction_site"],"annotation":"building construction","tags":["construction"],"emoji":"🏗️","order":3487,"group":5,"version":0.7},{"shortcodes":["bricks"],"annotation":"brick","tags":["bricks","clay","mortar","wall"],"emoji":"🧱","order":3488,"group":5,"version":11},{"shortcodes":["rock"],"annotation":"rock","tags":["boulder","heavy","solid","stone"],"emoji":"🪨","order":3489,"group":5,"version":13},{"shortcodes":["wood"],"annotation":"wood","tags":["log","lumber","timber"],"emoji":"🪵","order":3490,"group":5,"version":13},{"shortcodes":["hut"],"annotation":"hut","tags":["house","roundhouse","yurt"],"emoji":"🛖","order":3491,"group":5,"version":13},{"shortcodes":["homes","houses"],"annotation":"houses","tags":["houses"],"emoji":"🏘️","order":3493,"group":5,"version":0.7},{"shortcodes":["derelict_house","house_abandoned"],"annotation":"derelict house","tags":["derelict","house"],"emoji":"🏚️","order":3495,"group":5,"version":0.7},{"shortcodes":["house"],"annotation":"house","tags":["home"],"emoji":"🏠️","order":3496,"group":5,"version":0.6},{"shortcodes":["house_with_garden"],"annotation":"house with garden","tags":["garden","home","house"],"emoji":"🏡","order":3497,"group":5,"version":0.6},{"shortcodes":["office"],"annotation":"office building","tags":["building"],"emoji":"🏢","order":3498,"group":5,"version":0.6},{"shortcodes":["post_office"],"annotation":"Japanese post office","tags":["japanese","japanese post office","post"],"emoji":"🏣","order":3499,"group":5,"version":0.6},{"shortcodes":["european_post_office"],"annotation":"post office","tags":["european","post"],"emoji":"🏤","order":3500,"group":5,"version":1},{"shortcodes":["hospital"],"annotation":"hospital","tags":["doctor","medicine"],"emoji":"🏥","order":3501,"group":5,"version":0.6},{"shortcodes":["bank"],"annotation":"bank","tags":["building"],"emoji":"🏦","order":3502,"group":5,"version":0.6},{"shortcodes":["hotel"],"annotation":"hotel","tags":["building"],"emoji":"🏨","order":3503,"group":5,"version":0.6},{"shortcodes":["love_hotel"],"annotation":"love hotel","tags":["hotel","love"],"emoji":"🏩","order":3504,"group":5,"version":0.6},{"shortcodes":["convenience_store"],"annotation":"convenience store","tags":["convenience","store"],"emoji":"🏪","order":3505,"group":5,"version":0.6},{"shortcodes":["school"],"annotation":"school","tags":["building"],"emoji":"🏫","order":3506,"group":5,"version":0.6},{"shortcodes":["department_store"],"annotation":"department store","tags":["department","store"],"emoji":"🏬","order":3507,"group":5,"version":0.6},{"shortcodes":["factory"],"annotation":"factory","tags":["building"],"emoji":"🏭️","order":3508,"group":5,"version":0.6},{"shortcodes":["japanese_castle"],"annotation":"Japanese castle","tags":["castle","japanese"],"emoji":"🏯","order":3509,"group":5,"version":0.6},{"shortcodes":["castle","european_castle"],"annotation":"castle","tags":["european"],"emoji":"🏰","order":3510,"group":5,"version":0.6},{"shortcodes":["wedding"],"annotation":"wedding","tags":["chapel","romance"],"emoji":"💒","order":3511,"group":5,"version":0.6},{"shortcodes":["tokyo_tower"],"annotation":"Tokyo tower","tags":["tokyo","tower"],"emoji":"🗼","order":3512,"group":5,"version":0.6},{"shortcodes":["statue_of_liberty"],"annotation":"Statue of Liberty","tags":["liberty","statue","statue of liberty"],"emoji":"🗽","order":3513,"group":5,"version":0.6},{"shortcodes":["church"],"annotation":"church","tags":["christian","cross","religion"],"emoji":"⛪️","order":3514,"group":5,"version":0.6},{"shortcodes":["mosque"],"annotation":"mosque","tags":["islam","muslim","religion"],"emoji":"🕌","order":3515,"group":5,"version":1},{"shortcodes":["hindu_temple"],"annotation":"hindu temple","tags":["hindu","temple"],"emoji":"🛕","order":3516,"group":5,"version":12},{"shortcodes":["synagogue"],"annotation":"synagogue","tags":["jew","jewish","religion","temple"],"emoji":"🕍","order":3517,"group":5,"version":1},{"shortcodes":["shinto_shrine"],"annotation":"shinto shrine","tags":["religion","shinto","shrine"],"emoji":"⛩️","order":3519,"group":5,"version":0.7},{"shortcodes":["kaaba"],"annotation":"kaaba","tags":["islam","muslim","religion"],"emoji":"🕋","order":3520,"group":5,"version":1},{"shortcodes":["fountain"],"annotation":"fountain","tags":["fountain"],"emoji":"⛲️","order":3521,"group":5,"version":0.6},{"shortcodes":["tent"],"annotation":"tent","tags":["camping"],"emoji":"⛺️","order":3522,"group":5,"version":0.6},{"shortcodes":["foggy"],"annotation":"foggy","tags":["fog"],"emoji":"🌁","order":3523,"group":5,"version":0.6},{"shortcodes":["night_with_stars"],"annotation":"night with stars","tags":["night","star"],"emoji":"🌃","order":3524,"group":5,"version":0.6},{"shortcodes":["cityscape"],"annotation":"cityscape","tags":["city"],"emoji":"🏙️","order":3526,"group":5,"version":0.7},{"shortcodes":["sunrise_over_mountains"],"annotation":"sunrise over mountains","tags":["morning","mountain","sun","sunrise"],"emoji":"🌄","order":3527,"group":5,"version":0.6},{"shortcodes":["sunrise"],"annotation":"sunrise","tags":["morning","sun"],"emoji":"🌅","order":3528,"group":5,"version":0.6},{"shortcodes":["city_dusk"],"annotation":"cityscape at dusk","tags":["city","dusk","evening","landscape","sunset"],"emoji":"🌆","order":3529,"group":5,"version":0.6},{"shortcodes":["city_sunrise","city_sunset"],"annotation":"sunset","tags":["dusk","sun"],"emoji":"🌇","order":3530,"group":5,"version":0.6},{"shortcodes":["bridge_at_night"],"annotation":"bridge at night","tags":["bridge","night"],"emoji":"🌉","order":3531,"group":5,"version":0.6},{"shortcodes":["hotsprings"],"annotation":"hot springs","tags":["hot","hotsprings","springs","steaming"],"emoji":"♨️","order":3533,"group":5,"version":0.6},{"shortcodes":["carousel_horse"],"annotation":"carousel horse","tags":["carousel","horse"],"emoji":"🎠","order":3534,"group":5,"version":0.6},{"shortcodes":["playground_slide","slide"],"annotation":"playground slide","tags":["amusement park","play"],"emoji":"🛝","order":3535,"group":5,"version":14},{"shortcodes":["ferris_wheel"],"annotation":"ferris wheel","tags":["amusement park","ferris","wheel"],"emoji":"🎡","order":3536,"group":5,"version":0.6},{"shortcodes":["roller_coaster"],"annotation":"roller coaster","tags":["amusement park","coaster","roller"],"emoji":"🎢","order":3537,"group":5,"version":0.6},{"shortcodes":["barber","barber_pole"],"annotation":"barber pole","tags":["barber","haircut","pole"],"emoji":"💈","order":3538,"group":5,"version":0.6},{"shortcodes":["circus_tent"],"annotation":"circus tent","tags":["circus","tent"],"emoji":"🎪","order":3539,"group":5,"version":0.6},{"shortcodes":["steam_locomotive"],"annotation":"locomotive","tags":["engine","railway","steam","train"],"emoji":"🚂","order":3540,"group":5,"version":1},{"shortcodes":["railway_car"],"annotation":"railway car","tags":["car","electric","railway","train","tram","trolleybus"],"emoji":"🚃","order":3541,"group":5,"version":0.6},{"shortcodes":["bullettrain_side"],"annotation":"high-speed train","tags":["railway","shinkansen","speed","train"],"emoji":"🚄","order":3542,"group":5,"version":0.6},{"shortcodes":["bullettrain_front"],"annotation":"bullet train","tags":["bullet","railway","shinkansen","speed","train"],"emoji":"🚅","order":3543,"group":5,"version":0.6},{"shortcodes":["train"],"annotation":"train","tags":["railway"],"emoji":"🚆","order":3544,"group":5,"version":1},{"shortcodes":["metro"],"annotation":"metro","tags":["subway"],"emoji":"🚇️","order":3545,"group":5,"version":0.6},{"shortcodes":["light_rail"],"annotation":"light rail","tags":["railway"],"emoji":"🚈","order":3546,"group":5,"version":1},{"shortcodes":["station"],"annotation":"station","tags":["railway","train"],"emoji":"🚉","order":3547,"group":5,"version":0.6},{"shortcodes":["tram"],"annotation":"tram","tags":["trolleybus"],"emoji":"🚊","order":3548,"group":5,"version":1},{"shortcodes":["monorail"],"annotation":"monorail","tags":["vehicle"],"emoji":"🚝","order":3549,"group":5,"version":1},{"shortcodes":["mountain_railway"],"annotation":"mountain railway","tags":["car","mountain","railway"],"emoji":"🚞","order":3550,"group":5,"version":1},{"shortcodes":["tram_car"],"annotation":"tram car","tags":["car","tram","trolleybus"],"emoji":"🚋","order":3551,"group":5,"version":1},{"shortcodes":["bus"],"annotation":"bus","tags":["vehicle"],"emoji":"🚌","order":3552,"group":5,"version":0.6},{"shortcodes":["oncoming_bus"],"annotation":"oncoming bus","tags":["bus","oncoming"],"emoji":"🚍️","order":3553,"group":5,"version":0.7},{"shortcodes":["trolleybus"],"annotation":"trolleybus","tags":["bus","tram","trolley"],"emoji":"🚎","order":3554,"group":5,"version":1},{"shortcodes":["minibus"],"annotation":"minibus","tags":["bus"],"emoji":"🚐","order":3555,"group":5,"version":1},{"shortcodes":["ambulance"],"annotation":"ambulance","tags":["vehicle"],"emoji":"🚑️","order":3556,"group":5,"version":0.6},{"shortcodes":["fire_engine"],"annotation":"fire engine","tags":["engine","fire","truck"],"emoji":"🚒","order":3557,"group":5,"version":0.6},{"shortcodes":["police_car"],"annotation":"police car","tags":["car","patrol","police"],"emoji":"🚓","order":3558,"group":5,"version":0.6},{"shortcodes":["oncoming_police_car"],"annotation":"oncoming police car","tags":["car","oncoming","police"],"emoji":"🚔️","order":3559,"group":5,"version":0.7},{"shortcodes":["taxi"],"annotation":"taxi","tags":["vehicle"],"emoji":"🚕","order":3560,"group":5,"version":0.6},{"shortcodes":["oncoming_taxi"],"annotation":"oncoming taxi","tags":["oncoming","taxi"],"emoji":"🚖","order":3561,"group":5,"version":1},{"shortcodes":["car","red_car"],"annotation":"automobile","tags":["car"],"emoji":"🚗","order":3562,"group":5,"version":0.6},{"shortcodes":["oncoming_automobile"],"annotation":"oncoming automobile","tags":["automobile","car","oncoming"],"emoji":"🚘️","order":3563,"group":5,"version":0.7},{"shortcodes":["blue_car","suv"],"annotation":"sport utility vehicle","tags":["recreational","sport utility"],"emoji":"🚙","order":3564,"group":5,"version":0.6},{"shortcodes":["pickup_truck"],"annotation":"pickup truck","tags":["pick-up","pickup","truck"],"emoji":"🛻","order":3565,"group":5,"version":13},{"shortcodes":["delivery_truck","truck"],"annotation":"delivery truck","tags":["delivery","truck"],"emoji":"🚚","order":3566,"group":5,"version":0.6},{"shortcodes":["articulated_lorry"],"annotation":"articulated lorry","tags":["lorry","semi","truck"],"emoji":"🚛","order":3567,"group":5,"version":1},{"shortcodes":["tractor"],"annotation":"tractor","tags":["vehicle"],"emoji":"🚜","order":3568,"group":5,"version":1},{"shortcodes":["racing_car"],"annotation":"racing car","tags":["car","racing"],"emoji":"🏎️","order":3570,"group":5,"version":0.7},{"shortcodes":["motorcycle"],"annotation":"motorcycle","tags":["racing"],"emoji":"🏍️","order":3572,"group":5,"version":0.7},{"shortcodes":["motor_scooter"],"annotation":"motor scooter","tags":["motor","scooter"],"emoji":"🛵","order":3573,"group":5,"version":3},{"shortcodes":["manual_wheelchair"],"annotation":"manual wheelchair","tags":["accessibility"],"emoji":"🦽","order":3574,"group":5,"version":12},{"shortcodes":["motorized_wheelchair"],"annotation":"motorized wheelchair","tags":["accessibility"],"emoji":"🦼","order":3575,"group":5,"version":12},{"shortcodes":["auto_rickshaw"],"annotation":"auto rickshaw","tags":["tuk tuk"],"emoji":"🛺","order":3576,"group":5,"version":12},{"shortcodes":["bicycle","bike"],"annotation":"bicycle","tags":["bike"],"emoji":"🚲️","order":3577,"group":5,"version":0.6},{"shortcodes":["scooter"],"annotation":"kick scooter","tags":["kick","scooter"],"emoji":"🛴","order":3578,"group":5,"version":3},{"shortcodes":["skateboard"],"annotation":"skateboard","tags":["board"],"emoji":"🛹","order":3579,"group":5,"version":11},{"shortcodes":["roller_skate"],"annotation":"roller skate","tags":["roller","skate"],"emoji":"🛼","order":3580,"group":5,"version":13},{"shortcodes":["busstop"],"annotation":"bus stop","tags":["bus","stop"],"emoji":"🚏","order":3581,"group":5,"version":0.6},{"shortcodes":["motorway"],"annotation":"motorway","tags":["highway","road"],"emoji":"🛣️","order":3583,"group":5,"version":0.7},{"shortcodes":["railway_track"],"annotation":"railway track","tags":["railway","train"],"emoji":"🛤️","order":3585,"group":5,"version":0.7},{"shortcodes":["oil_drum"],"annotation":"oil drum","tags":["drum","oil"],"emoji":"🛢️","order":3587,"group":5,"version":0.7},{"shortcodes":["fuelpump"],"annotation":"fuel pump","tags":["diesel","fuel","fuelpump","gas","pump","station"],"emoji":"⛽️","order":3588,"group":5,"version":0.6},{"shortcodes":["wheel"],"annotation":"wheel","tags":["circle","tire","turn"],"emoji":"🛞","order":3589,"group":5,"version":14},{"shortcodes":["rotating_light"],"annotation":"police car light","tags":["beacon","car","light","police","revolving"],"emoji":"🚨","order":3590,"group":5,"version":0.6},{"shortcodes":["traffic_light"],"annotation":"horizontal traffic light","tags":["light","signal","traffic"],"emoji":"🚥","order":3591,"group":5,"version":0.6},{"shortcodes":["vertical_traffic_light"],"annotation":"vertical traffic light","tags":["light","signal","traffic"],"emoji":"🚦","order":3592,"group":5,"version":1},{"shortcodes":["octagonal_sign","stop_sign"],"annotation":"stop sign","tags":["octagonal","sign","stop"],"emoji":"🛑","order":3593,"group":5,"version":3},{"shortcodes":["construction"],"annotation":"construction","tags":["barrier"],"emoji":"🚧","order":3594,"group":5,"version":0.6},{"shortcodes":["anchor"],"annotation":"anchor","tags":["ship","tool"],"emoji":"⚓️","order":3595,"group":5,"version":0.6},{"shortcodes":["lifebuoy","ring_buoy"],"annotation":"ring buoy","tags":["float","life preserver","life saver","rescue","safety"],"emoji":"🛟","order":3596,"group":5,"version":14},{"shortcodes":["sailboat"],"annotation":"sailboat","tags":["boat","resort","sea","yacht"],"emoji":"⛵️","order":3597,"group":5,"version":0.6},{"shortcodes":["canoe"],"annotation":"canoe","tags":["boat"],"emoji":"🛶","order":3598,"group":5,"version":3},{"shortcodes":["speedboat"],"annotation":"speedboat","tags":["boat"],"emoji":"🚤","order":3599,"group":5,"version":0.6},{"shortcodes":["cruise_ship","passenger_ship"],"annotation":"passenger ship","tags":["passenger","ship"],"emoji":"🛳️","order":3601,"group":5,"version":0.7},{"shortcodes":["ferry"],"annotation":"ferry","tags":["boat","passenger"],"emoji":"⛴️","order":3603,"group":5,"version":0.7},{"shortcodes":["motorboat"],"annotation":"motor boat","tags":["boat","motorboat"],"emoji":"🛥️","order":3605,"group":5,"version":0.7},{"shortcodes":["ship"],"annotation":"ship","tags":["boat","passenger"],"emoji":"🚢","order":3606,"group":5,"version":0.6},{"shortcodes":["airplane"],"annotation":"airplane","tags":["aeroplane"],"emoji":"✈️","order":3608,"group":5,"version":0.6},{"shortcodes":["small_airplane"],"annotation":"small airplane","tags":["aeroplane","airplane"],"emoji":"🛩️","order":3610,"group":5,"version":0.7},{"shortcodes":["airplane_departure"],"annotation":"airplane departure","tags":["aeroplane","airplane","check-in","departure","departures"],"emoji":"🛫","order":3611,"group":5,"version":1},{"shortcodes":["airplane_arriving"],"annotation":"airplane arrival","tags":["aeroplane","airplane","arrivals","arriving","landing"],"emoji":"🛬","order":3612,"group":5,"version":1},{"shortcodes":["parachute"],"annotation":"parachute","tags":["hang-glide","parasail","skydive"],"emoji":"🪂","order":3613,"group":5,"version":12},{"shortcodes":["seat"],"annotation":"seat","tags":["chair"],"emoji":"💺","order":3614,"group":5,"version":0.6},{"shortcodes":["helicopter"],"annotation":"helicopter","tags":["vehicle"],"emoji":"🚁","order":3615,"group":5,"version":1},{"shortcodes":["suspension_railway"],"annotation":"suspension railway","tags":["railway","suspension"],"emoji":"🚟","order":3616,"group":5,"version":1},{"shortcodes":["mountain_cableway"],"annotation":"mountain cableway","tags":["cable","gondola","mountain"],"emoji":"🚠","order":3617,"group":5,"version":1},{"shortcodes":["aerial_tramway"],"annotation":"aerial tramway","tags":["aerial","cable","car","gondola","tramway"],"emoji":"🚡","order":3618,"group":5,"version":1},{"shortcodes":["satellite"],"annotation":"satellite","tags":["space"],"emoji":"🛰️","order":3620,"group":5,"version":0.7},{"shortcodes":["rocket"],"annotation":"rocket","tags":["space"],"emoji":"🚀","order":3621,"group":5,"version":0.6},{"shortcodes":["flying_saucer"],"annotation":"flying saucer","tags":["ufo"],"emoji":"🛸","order":3622,"group":5,"version":5},{"shortcodes":["bellhop"],"annotation":"bellhop bell","tags":["bell","bellhop","hotel"],"emoji":"🛎️","order":3624,"group":5,"version":0.7},{"shortcodes":["luggage"],"annotation":"luggage","tags":["packing","travel"],"emoji":"🧳","order":3625,"group":5,"version":11},{"shortcodes":["hourglass"],"annotation":"hourglass done","tags":["sand","timer"],"emoji":"⌛️","order":3626,"group":5,"version":0.6},{"shortcodes":["hourglass_flowing_sand"],"annotation":"hourglass not done","tags":["hourglass","sand","timer"],"emoji":"⏳️","order":3627,"group":5,"version":0.6},{"shortcodes":["watch"],"annotation":"watch","tags":["clock"],"emoji":"⌚️","order":3628,"group":5,"version":0.6},{"shortcodes":["alarm_clock"],"annotation":"alarm clock","tags":["alarm","clock"],"emoji":"⏰","order":3629,"group":5,"version":0.6},{"shortcodes":["stopwatch"],"annotation":"stopwatch","tags":["clock"],"emoji":"⏱️","order":3631,"group":5,"version":1},{"shortcodes":["timer_clock"],"annotation":"timer clock","tags":["clock","timer"],"emoji":"⏲️","order":3633,"group":5,"version":1},{"shortcodes":["clock"],"annotation":"mantelpiece clock","tags":["clock"],"emoji":"🕰️","order":3635,"group":5,"version":0.7},{"shortcodes":["clock12"],"annotation":"twelve o’clock","tags":["00","12","12:00","clock","o’clock","twelve"],"emoji":"🕛️","order":3636,"group":5,"version":0.6},{"shortcodes":["clock1230"],"annotation":"twelve-thirty","tags":["12","12:30","clock","thirty","twelve"],"emoji":"🕧️","order":3637,"group":5,"version":0.7},{"shortcodes":["clock1"],"annotation":"one o’clock","tags":["00","1","1:00","clock","one","o’clock"],"emoji":"🕐️","order":3638,"group":5,"version":0.6},{"shortcodes":["clock130"],"annotation":"one-thirty","tags":["1","1:30","clock","one","thirty"],"emoji":"🕜️","order":3639,"group":5,"version":0.7},{"shortcodes":["clock2"],"annotation":"two o’clock","tags":["00","2","2:00","clock","o’clock","two"],"emoji":"🕑️","order":3640,"group":5,"version":0.6},{"shortcodes":["clock230"],"annotation":"two-thirty","tags":["2","2:30","clock","thirty","two"],"emoji":"🕝️","order":3641,"group":5,"version":0.7},{"shortcodes":["clock3"],"annotation":"three o’clock","tags":["00","3","3:00","clock","o’clock","three"],"emoji":"🕒️","order":3642,"group":5,"version":0.6},{"shortcodes":["clock330"],"annotation":"three-thirty","tags":["3","3:30","clock","thirty","three"],"emoji":"🕞️","order":3643,"group":5,"version":0.7},{"shortcodes":["clock4"],"annotation":"four o’clock","tags":["00","4","4:00","clock","four","o’clock"],"emoji":"🕓️","order":3644,"group":5,"version":0.6},{"shortcodes":["clock430"],"annotation":"four-thirty","tags":["4","4:30","clock","four","thirty"],"emoji":"🕟️","order":3645,"group":5,"version":0.7},{"shortcodes":["clock5"],"annotation":"five o’clock","tags":["00","5","5:00","clock","five","o’clock"],"emoji":"🕔️","order":3646,"group":5,"version":0.6},{"shortcodes":["clock530"],"annotation":"five-thirty","tags":["5","5:30","clock","five","thirty"],"emoji":"🕠️","order":3647,"group":5,"version":0.7},{"shortcodes":["clock6"],"annotation":"six o’clock","tags":["00","6","6:00","clock","o’clock","six"],"emoji":"🕕️","order":3648,"group":5,"version":0.6},{"shortcodes":["clock630"],"annotation":"six-thirty","tags":["6","6:30","clock","six","thirty"],"emoji":"🕡️","order":3649,"group":5,"version":0.7},{"shortcodes":["clock7"],"annotation":"seven o’clock","tags":["00","7","7:00","clock","o’clock","seven"],"emoji":"🕖️","order":3650,"group":5,"version":0.6},{"shortcodes":["clock730"],"annotation":"seven-thirty","tags":["7","7:30","clock","seven","thirty"],"emoji":"🕢️","order":3651,"group":5,"version":0.7},{"shortcodes":["clock8"],"annotation":"eight o’clock","tags":["00","8","8:00","clock","eight","o’clock"],"emoji":"🕗️","order":3652,"group":5,"version":0.6},{"shortcodes":["clock830"],"annotation":"eight-thirty","tags":["8","8:30","clock","eight","thirty"],"emoji":"🕣️","order":3653,"group":5,"version":0.7},{"shortcodes":["clock9"],"annotation":"nine o’clock","tags":["00","9","9:00","clock","nine","o’clock"],"emoji":"🕘️","order":3654,"group":5,"version":0.6},{"shortcodes":["clock930"],"annotation":"nine-thirty","tags":["9","9:30","clock","nine","thirty"],"emoji":"🕤️","order":3655,"group":5,"version":0.7},{"shortcodes":["clock10"],"annotation":"ten o’clock","tags":["00","10","10:00","clock","o’clock","ten"],"emoji":"🕙️","order":3656,"group":5,"version":0.6},{"shortcodes":["clock1030"],"annotation":"ten-thirty","tags":["10","10:30","clock","ten","thirty"],"emoji":"🕥️","order":3657,"group":5,"version":0.7},{"shortcodes":["clock11"],"annotation":"eleven o’clock","tags":["00","11","11:00","clock","eleven","o’clock"],"emoji":"🕚️","order":3658,"group":5,"version":0.6},{"shortcodes":["clock1130"],"annotation":"eleven-thirty","tags":["11","11:30","clock","eleven","thirty"],"emoji":"🕦️","order":3659,"group":5,"version":0.7},{"shortcodes":["new_moon"],"annotation":"new moon","tags":["dark","moon"],"emoji":"🌑","order":3660,"group":5,"version":0.6},{"shortcodes":["waxing_crescent_moon"],"annotation":"waxing crescent moon","tags":["crescent","moon","waxing"],"emoji":"🌒","order":3661,"group":5,"version":1},{"shortcodes":["first_quarter_moon"],"annotation":"first quarter moon","tags":["moon","quarter"],"emoji":"🌓","order":3662,"group":5,"version":0.6},{"shortcodes":["waxing_gibbous_moon"],"annotation":"waxing gibbous moon","tags":["gibbous","moon","waxing"],"emoji":"🌔","order":3663,"group":5,"version":0.6},{"shortcodes":["full_moon"],"annotation":"full moon","tags":["full","moon"],"emoji":"🌕️","order":3664,"group":5,"version":0.6},{"shortcodes":["waning_gibbous_moon"],"annotation":"waning gibbous moon","tags":["gibbous","moon","waning"],"emoji":"🌖","order":3665,"group":5,"version":1},{"shortcodes":["last_quarter_moon"],"annotation":"last quarter moon","tags":["moon","quarter"],"emoji":"🌗","order":3666,"group":5,"version":1},{"shortcodes":["waning_crescent_moon"],"annotation":"waning crescent moon","tags":["crescent","moon","waning"],"emoji":"🌘","order":3667,"group":5,"version":1},{"shortcodes":["crescent_moon"],"annotation":"crescent moon","tags":["crescent","moon"],"emoji":"🌙","order":3668,"group":5,"version":0.6},{"shortcodes":["new_moon_with_face"],"annotation":"new moon face","tags":["face","moon"],"emoji":"🌚","order":3669,"group":5,"version":1},{"shortcodes":["first_quarter_moon_with_face"],"annotation":"first quarter moon face","tags":["face","moon","quarter"],"emoji":"🌛","order":3670,"group":5,"version":0.6},{"shortcodes":["last_quarter_moon_with_face"],"annotation":"last quarter moon face","tags":["face","moon","quarter"],"emoji":"🌜️","order":3671,"group":5,"version":0.7},{"shortcodes":["thermometer"],"annotation":"thermometer","tags":["weather"],"emoji":"🌡️","order":3673,"group":5,"version":0.7},{"shortcodes":["sun"],"annotation":"sun","tags":["bright","rays","sunny"],"emoji":"☀️","order":3675,"group":5,"version":0.6},{"shortcodes":["full_moon_with_face"],"annotation":"full moon face","tags":["bright","face","full","moon"],"emoji":"🌝","order":3676,"group":5,"version":1},{"shortcodes":["sun_with_face"],"annotation":"sun with face","tags":["bright","face","sun"],"emoji":"🌞","order":3677,"group":5,"version":1},{"shortcodes":["ringed_planet","saturn"],"annotation":"ringed planet","tags":["saturn","saturnine"],"emoji":"🪐","order":3678,"group":5,"version":12},{"shortcodes":["star"],"annotation":"star","tags":["star"],"emoji":"⭐️","order":3679,"group":5,"version":0.6},{"shortcodes":["glowing_star","star2"],"annotation":"glowing star","tags":["glittery","glow","shining","sparkle","star"],"emoji":"🌟","order":3680,"group":5,"version":0.6},{"shortcodes":["shooting_star","stars"],"annotation":"shooting star","tags":["falling","shooting","star"],"emoji":"🌠","order":3681,"group":5,"version":0.6},{"shortcodes":["milky_way"],"annotation":"milky way","tags":["space"],"emoji":"🌌","order":3682,"group":5,"version":0.6},{"shortcodes":["cloud"],"annotation":"cloud","tags":["weather"],"emoji":"☁️","order":3684,"group":5,"version":0.6},{"shortcodes":["partly_sunny","sun_behind_cloud"],"annotation":"sun behind cloud","tags":["cloud","sun"],"emoji":"⛅️","order":3685,"group":5,"version":0.6},{"shortcodes":["stormy","thunder_cloud_and_rain"],"annotation":"cloud with lightning and rain","tags":["cloud","rain","thunder"],"emoji":"⛈️","order":3687,"group":5,"version":0.7},{"shortcodes":["sun_behind_small_cloud","sunny"],"annotation":"sun behind small cloud","tags":["cloud","sun"],"emoji":"🌤️","order":3689,"group":5,"version":0.7},{"shortcodes":["cloudy","sun_behind_large_cloud"],"annotation":"sun behind large cloud","tags":["cloud","sun"],"emoji":"🌥️","order":3691,"group":5,"version":0.7},{"shortcodes":["sun_and_rain","sun_behind_rain_cloud"],"annotation":"sun behind rain cloud","tags":["cloud","rain","sun"],"emoji":"🌦️","order":3693,"group":5,"version":0.7},{"shortcodes":["cloud_with_rain","rainy"],"annotation":"cloud with rain","tags":["cloud","rain"],"emoji":"🌧️","order":3695,"group":5,"version":0.7},{"shortcodes":["cloud_with_snow","snowy"],"annotation":"cloud with snow","tags":["cloud","cold","snow"],"emoji":"🌨️","order":3697,"group":5,"version":0.7},{"shortcodes":["cloud_with_lightning","lightning"],"annotation":"cloud with lightning","tags":["cloud","lightning"],"emoji":"🌩️","order":3699,"group":5,"version":0.7},{"shortcodes":["tornado"],"annotation":"tornado","tags":["cloud","whirlwind"],"emoji":"🌪️","order":3701,"group":5,"version":0.7},{"shortcodes":["fog"],"annotation":"fog","tags":["cloud"],"emoji":"🌫️","order":3703,"group":5,"version":0.7},{"shortcodes":["wind_blowing_face"],"annotation":"wind face","tags":["blow","cloud","face","wind"],"emoji":"🌬️","order":3705,"group":5,"version":0.7},{"shortcodes":["cyclone"],"annotation":"cyclone","tags":["dizzy","hurricane","twister","typhoon"],"emoji":"🌀","order":3706,"group":5,"version":0.6},{"shortcodes":["rainbow"],"annotation":"rainbow","tags":["rain"],"emoji":"🌈","order":3707,"group":5,"version":0.6},{"shortcodes":["closed_umbrella"],"annotation":"closed umbrella","tags":["clothing","rain","umbrella"],"emoji":"🌂","order":3708,"group":5,"version":0.6},{"shortcodes":["umbrella"],"annotation":"umbrella","tags":["clothing","rain"],"emoji":"☂️","order":3710,"group":5,"version":0.7},{"shortcodes":["umbrella_with_rain"],"annotation":"umbrella with rain drops","tags":["clothing","drop","rain","umbrella"],"emoji":"☔️","order":3711,"group":5,"version":0.6},{"shortcodes":["beach_umbrella","umbrella_on_ground"],"annotation":"umbrella on ground","tags":["rain","sun","umbrella"],"emoji":"⛱️","order":3713,"group":5,"version":0.7},{"shortcodes":["high_voltage","zap"],"annotation":"high voltage","tags":["danger","electric","lightning","voltage","zap"],"emoji":"⚡️","order":3714,"group":5,"version":0.6},{"shortcodes":["snowflake"],"annotation":"snowflake","tags":["cold","snow"],"emoji":"❄️","order":3716,"group":5,"version":0.6},{"shortcodes":["snowman2"],"annotation":"snowman","tags":["cold","snow"],"emoji":"☃️","order":3718,"group":5,"version":0.7},{"shortcodes":["snowman"],"annotation":"snowman without snow","tags":["cold","snow","snowman"],"emoji":"⛄️","order":3719,"group":5,"version":0.6},{"shortcodes":["comet"],"annotation":"comet","tags":["space"],"emoji":"☄️","order":3721,"group":5,"version":1},{"shortcodes":["fire"],"annotation":"fire","tags":["flame","tool"],"emoji":"🔥","order":3722,"group":5,"version":0.6},{"shortcodes":["droplet"],"annotation":"droplet","tags":["cold","comic","drop","sweat"],"emoji":"💧","order":3723,"group":5,"version":0.6},{"shortcodes":["ocean","water_wave"],"annotation":"water wave","tags":["ocean","water","wave"],"emoji":"🌊","order":3724,"group":5,"version":0.6},{"shortcodes":["jack_o_lantern"],"annotation":"jack-o-lantern","tags":["celebration","halloween","jack","lantern"],"emoji":"🎃","order":3725,"group":6,"version":0.6},{"shortcodes":["christmas_tree"],"annotation":"Christmas tree","tags":["celebration","christmas","tree"],"emoji":"🎄","order":3726,"group":6,"version":0.6},{"shortcodes":["fireworks"],"annotation":"fireworks","tags":["celebration"],"emoji":"🎆","order":3727,"group":6,"version":0.6},{"shortcodes":["sparkler"],"annotation":"sparkler","tags":["celebration","fireworks","sparkle"],"emoji":"🎇","order":3728,"group":6,"version":0.6},{"shortcodes":["firecracker"],"annotation":"firecracker","tags":["dynamite","explosive","fireworks"],"emoji":"🧨","order":3729,"group":6,"version":11},{"shortcodes":["sparkles"],"annotation":"sparkles","tags":["*","sparkle","star"],"emoji":"✨","order":3730,"group":6,"version":0.6},{"shortcodes":["balloon"],"annotation":"balloon","tags":["celebration"],"emoji":"🎈","order":3731,"group":6,"version":0.6},{"shortcodes":["party","party_popper","tada"],"annotation":"party popper","tags":["celebration","party","popper","tada"],"emoji":"🎉","order":3732,"group":6,"version":0.6},{"shortcodes":["confetti_ball"],"annotation":"confetti ball","tags":["ball","celebration","confetti"],"emoji":"🎊","order":3733,"group":6,"version":0.6},{"shortcodes":["tanabata_tree"],"annotation":"tanabata tree","tags":["banner","celebration","japanese","tree"],"emoji":"🎋","order":3734,"group":6,"version":0.6},{"shortcodes":["bamboo"],"annotation":"pine decoration","tags":["bamboo","celebration","japanese","pine"],"emoji":"🎍","order":3735,"group":6,"version":0.6},{"shortcodes":["dolls"],"annotation":"Japanese dolls","tags":["celebration","doll","festival","japanese","japanese dolls"],"emoji":"🎎","order":3736,"group":6,"version":0.6},{"shortcodes":["carp_streamer","flags"],"annotation":"carp streamer","tags":["carp","celebration","streamer"],"emoji":"🎏","order":3737,"group":6,"version":0.6},{"shortcodes":["wind_chime"],"annotation":"wind chime","tags":["bell","celebration","chime","wind"],"emoji":"🎐","order":3738,"group":6,"version":0.6},{"shortcodes":["moon_ceremony","rice_scene"],"annotation":"moon viewing ceremony","tags":["celebration","ceremony","moon"],"emoji":"🎑","order":3739,"group":6,"version":0.6},{"shortcodes":["red_envelope"],"annotation":"red envelope","tags":["gift","good luck","hóngbāo","lai see","money"],"emoji":"🧧","order":3740,"group":6,"version":11},{"shortcodes":["ribbon"],"annotation":"ribbon","tags":["celebration"],"emoji":"🎀","order":3741,"group":6,"version":0.6},{"shortcodes":["gift"],"annotation":"wrapped gift","tags":["box","celebration","gift","present","wrapped"],"emoji":"🎁","order":3742,"group":6,"version":0.6},{"shortcodes":["reminder_ribbon"],"annotation":"reminder ribbon","tags":["celebration","reminder","ribbon"],"emoji":"🎗️","order":3744,"group":6,"version":0.7},{"shortcodes":["admission_tickets","tickets"],"annotation":"admission tickets","tags":["admission","ticket"],"emoji":"🎟️","order":3746,"group":6,"version":0.7},{"shortcodes":["ticket"],"annotation":"ticket","tags":["admission"],"emoji":"🎫","order":3747,"group":6,"version":0.6},{"shortcodes":["military_medal"],"annotation":"military medal","tags":["celebration","medal","military"],"emoji":"🎖️","order":3749,"group":6,"version":0.7},{"shortcodes":["trophy"],"annotation":"trophy","tags":["prize"],"emoji":"🏆️","order":3750,"group":6,"version":0.6},{"shortcodes":["sports_medal"],"annotation":"sports medal","tags":["medal"],"emoji":"🏅","order":3751,"group":6,"version":1},{"shortcodes":["1st","first_place_medal"],"annotation":"1st place medal","tags":["first","gold","medal"],"emoji":"🥇","order":3752,"group":6,"version":3},{"shortcodes":["2nd","second_place_medal"],"annotation":"2nd place medal","tags":["medal","second","silver"],"emoji":"🥈","order":3753,"group":6,"version":3},{"shortcodes":["3rd","third_place_medal"],"annotation":"3rd place medal","tags":["bronze","medal","third"],"emoji":"🥉","order":3754,"group":6,"version":3},{"shortcodes":["soccer"],"annotation":"soccer ball","tags":["ball","football","soccer"],"emoji":"⚽️","order":3755,"group":6,"version":0.6},{"shortcodes":["baseball"],"annotation":"baseball","tags":["ball"],"emoji":"⚾️","order":3756,"group":6,"version":0.6},{"shortcodes":["softball"],"annotation":"softball","tags":["ball","glove","underarm"],"emoji":"🥎","order":3757,"group":6,"version":11},{"shortcodes":["basketball"],"annotation":"basketball","tags":["ball","hoop"],"emoji":"🏀","order":3758,"group":6,"version":0.6},{"shortcodes":["volleyball"],"annotation":"volleyball","tags":["ball","game"],"emoji":"🏐","order":3759,"group":6,"version":1},{"shortcodes":["football"],"annotation":"american football","tags":["american","ball","football"],"emoji":"🏈","order":3760,"group":6,"version":0.6},{"shortcodes":["rugby_football"],"annotation":"rugby football","tags":["ball","football","rugby"],"emoji":"🏉","order":3761,"group":6,"version":1},{"shortcodes":["tennis"],"annotation":"tennis","tags":["ball","racquet"],"emoji":"🎾","order":3762,"group":6,"version":0.6},{"shortcodes":["flying_disc"],"annotation":"flying disc","tags":["ultimate"],"emoji":"🥏","order":3763,"group":6,"version":11},{"shortcodes":["bowling"],"annotation":"bowling","tags":["ball","game"],"emoji":"🎳","order":3764,"group":6,"version":0.6},{"shortcodes":["cricket_game"],"annotation":"cricket game","tags":["ball","bat","game"],"emoji":"🏏","order":3765,"group":6,"version":1},{"shortcodes":["field_hockey"],"annotation":"field hockey","tags":["ball","field","game","hockey","stick"],"emoji":"🏑","order":3766,"group":6,"version":1},{"shortcodes":["hockey"],"annotation":"ice hockey","tags":["game","hockey","ice","puck","stick"],"emoji":"🏒","order":3767,"group":6,"version":1},{"shortcodes":["lacrosse"],"annotation":"lacrosse","tags":["ball","goal","stick"],"emoji":"🥍","order":3768,"group":6,"version":11},{"shortcodes":["ping_pong"],"annotation":"ping pong","tags":["ball","bat","game","paddle","table tennis"],"emoji":"🏓","order":3769,"group":6,"version":1},{"shortcodes":["badminton"],"annotation":"badminton","tags":["birdie","game","racquet","shuttlecock"],"emoji":"🏸","order":3770,"group":6,"version":1},{"shortcodes":["boxing_glove"],"annotation":"boxing glove","tags":["boxing","glove"],"emoji":"🥊","order":3771,"group":6,"version":3},{"shortcodes":["martial_arts_uniform"],"annotation":"martial arts uniform","tags":["judo","karate","martial arts","taekwondo","uniform"],"emoji":"🥋","order":3772,"group":6,"version":3},{"shortcodes":["goal_net"],"annotation":"goal net","tags":["goal","net"],"emoji":"🥅","order":3773,"group":6,"version":3},{"shortcodes":["golf"],"annotation":"flag in hole","tags":["golf","hole"],"emoji":"⛳️","order":3774,"group":6,"version":0.6},{"shortcodes":["ice_skate"],"annotation":"ice skate","tags":["ice","skate"],"emoji":"⛸️","order":3776,"group":6,"version":0.7},{"shortcodes":["fishing_pole","fishing_pole_and_fish"],"annotation":"fishing pole","tags":["fish","pole"],"emoji":"🎣","order":3777,"group":6,"version":0.6},{"shortcodes":["diving_mask"],"annotation":"diving mask","tags":["diving","scuba","snorkeling"],"emoji":"🤿","order":3778,"group":6,"version":12},{"shortcodes":["running_shirt","running_shirt_with_sash"],"annotation":"running shirt","tags":["athletics","running","sash","shirt"],"emoji":"🎽","order":3779,"group":6,"version":0.6},{"shortcodes":["ski"],"annotation":"skis","tags":["ski","snow"],"emoji":"🎿","order":3780,"group":6,"version":0.6},{"shortcodes":["sled"],"annotation":"sled","tags":["sledge","sleigh"],"emoji":"🛷","order":3781,"group":6,"version":5},{"shortcodes":["curling_stone"],"annotation":"curling stone","tags":["game","rock"],"emoji":"🥌","order":3782,"group":6,"version":5},{"shortcodes":["bullseye","dart","direct_hit"],"annotation":"bullseye","tags":["dart","direct hit","game","hit","target"],"emoji":"🎯","order":3783,"group":6,"version":0.6},{"shortcodes":["yo_yo"],"annotation":"yo-yo","tags":["fluctuate","toy"],"emoji":"🪀","order":3784,"group":6,"version":12},{"shortcodes":["kite"],"annotation":"kite","tags":["fly","soar"],"emoji":"🪁","order":3785,"group":6,"version":12},{"shortcodes":["8ball","billiards"],"annotation":"pool 8 ball","tags":["8","ball","billiard","eight","game"],"emoji":"🎱","order":3786,"group":6,"version":0.6},{"shortcodes":["crystal_ball"],"annotation":"crystal ball","tags":["ball","crystal","fairy tale","fantasy","fortune","tool"],"emoji":"🔮","order":3787,"group":6,"version":0.6},{"shortcodes":["magic_wand"],"annotation":"magic wand","tags":["magic","witch","wizard"],"emoji":"🪄","order":3788,"group":6,"version":13},{"shortcodes":["nazar_amulet"],"annotation":"nazar amulet","tags":["bead","charm","evil-eye","nazar","talisman"],"emoji":"🧿","order":3789,"group":6,"version":11},{"shortcodes":["hamsa"],"annotation":"hamsa","tags":["amulet","fatima","hand","mary","miriam","protection"],"emoji":"🪬","order":3790,"group":6,"version":14},{"shortcodes":["controller","video_game"],"annotation":"video game","tags":["controller","game"],"emoji":"🎮️","order":3791,"group":6,"version":0.6},{"shortcodes":["joystick"],"annotation":"joystick","tags":["game","video game"],"emoji":"🕹️","order":3793,"group":6,"version":0.7},{"shortcodes":["slot_machine"],"annotation":"slot machine","tags":["game","slot"],"emoji":"🎰","order":3794,"group":6,"version":0.6},{"shortcodes":["game_die"],"annotation":"game die","tags":["dice","die","game"],"emoji":"🎲","order":3795,"group":6,"version":0.6},{"shortcodes":["jigsaw","puzzle_piece"],"annotation":"puzzle piece","tags":["clue","interlocking","jigsaw","piece","puzzle"],"emoji":"🧩","order":3796,"group":6,"version":11},{"shortcodes":["teddy_bear"],"annotation":"teddy bear","tags":["plaything","plush","stuffed","toy"],"emoji":"🧸","order":3797,"group":6,"version":11},{"shortcodes":["pinata"],"annotation":"piñata","tags":["celebration","party"],"emoji":"🪅","order":3798,"group":6,"version":13},{"shortcodes":["disco","disco_ball","mirror_ball"],"annotation":"mirror ball","tags":["dance","disco","glitter","party"],"emoji":"🪩","order":3799,"group":6,"version":14},{"shortcodes":["nesting_dolls"],"annotation":"nesting dolls","tags":["doll","nesting","russia"],"emoji":"🪆","order":3800,"group":6,"version":13},{"shortcodes":["spades"],"annotation":"spade suit","tags":["card","game"],"emoji":"♠️","order":3802,"group":6,"version":0.6},{"shortcodes":["hearts"],"annotation":"heart suit","tags":["card","game"],"emoji":"♥️","order":3804,"group":6,"version":0.6},{"shortcodes":["diamonds"],"annotation":"diamond suit","tags":["card","game"],"emoji":"♦️","order":3806,"group":6,"version":0.6},{"shortcodes":["clubs"],"annotation":"club suit","tags":["card","game"],"emoji":"♣️","order":3808,"group":6,"version":0.6},{"shortcodes":["chess_pawn"],"annotation":"chess pawn","tags":["chess","dupe","expendable"],"emoji":"♟️","order":3810,"group":6,"version":11},{"shortcodes":["black_joker"],"annotation":"joker","tags":["card","game","wildcard"],"emoji":"🃏","order":3811,"group":6,"version":0.6},{"shortcodes":["mahjong"],"annotation":"mahjong red dragon","tags":["game","mahjong","red"],"emoji":"🀄️","order":3812,"group":6,"version":0.6},{"shortcodes":["flower_playing_cards"],"annotation":"flower playing cards","tags":["card","flower","game","japanese","playing"],"emoji":"🎴","order":3813,"group":6,"version":0.6},{"shortcodes":["performing_arts"],"annotation":"performing arts","tags":["art","mask","performing","theater","theatre"],"emoji":"🎭️","order":3814,"group":6,"version":0.6},{"shortcodes":["frame_with_picture","framed_picture"],"annotation":"framed picture","tags":["art","frame","museum","painting","picture"],"emoji":"🖼️","order":3816,"group":6,"version":0.7},{"shortcodes":["art","palette"],"annotation":"artist palette","tags":["art","museum","painting","palette"],"emoji":"🎨","order":3817,"group":6,"version":0.6},{"shortcodes":["thread"],"annotation":"thread","tags":["needle","sewing","spool","string"],"emoji":"🧵","order":3818,"group":6,"version":11},{"shortcodes":["sewing_needle"],"annotation":"sewing needle","tags":["embroidery","needle","sewing","stitches","sutures","tailoring"],"emoji":"🪡","order":3819,"group":6,"version":13},{"shortcodes":["yarn"],"annotation":"yarn","tags":["ball","crochet","knit"],"emoji":"🧶","order":3820,"group":6,"version":11},{"shortcodes":["knot"],"annotation":"knot","tags":["rope","tangled","tie","twine","twist"],"emoji":"🪢","order":3821,"group":6,"version":13},{"shortcodes":["eyeglasses","glasses"],"annotation":"glasses","tags":["clothing","eye","eyeglasses","eyewear"],"emoji":"👓️","order":3822,"group":7,"version":0.6},{"shortcodes":["sunglasses"],"annotation":"sunglasses","tags":["dark","eye","eyewear","glasses"],"emoji":"🕶️","order":3824,"group":7,"version":0.7},{"shortcodes":["goggles"],"annotation":"goggles","tags":["eye protection","swimming","welding"],"emoji":"🥽","order":3825,"group":7,"version":11},{"shortcodes":["lab_coat"],"annotation":"lab coat","tags":["doctor","experiment","scientist"],"emoji":"🥼","order":3826,"group":7,"version":11},{"shortcodes":["safety_vest"],"annotation":"safety vest","tags":["emergency","safety","vest"],"emoji":"🦺","order":3827,"group":7,"version":12},{"shortcodes":["necktie"],"annotation":"necktie","tags":["clothing","tie"],"emoji":"👔","order":3828,"group":7,"version":0.6},{"shortcodes":["shirt"],"annotation":"t-shirt","tags":["clothing","shirt","tshirt"],"emoji":"👕","order":3829,"group":7,"version":0.6},{"shortcodes":["jeans"],"annotation":"jeans","tags":["clothing","pants","trousers"],"emoji":"👖","order":3830,"group":7,"version":0.6},{"shortcodes":["scarf"],"annotation":"scarf","tags":["neck"],"emoji":"🧣","order":3831,"group":7,"version":5},{"shortcodes":["gloves"],"annotation":"gloves","tags":["hand"],"emoji":"🧤","order":3832,"group":7,"version":5},{"shortcodes":["coat"],"annotation":"coat","tags":["jacket"],"emoji":"🧥","order":3833,"group":7,"version":5},{"shortcodes":["socks"],"annotation":"socks","tags":["stocking"],"emoji":"🧦","order":3834,"group":7,"version":5},{"shortcodes":["dress"],"annotation":"dress","tags":["clothing"],"emoji":"👗","order":3835,"group":7,"version":0.6},{"shortcodes":["kimono"],"annotation":"kimono","tags":["clothing"],"emoji":"👘","order":3836,"group":7,"version":0.6},{"shortcodes":["sari"],"annotation":"sari","tags":["clothing","dress"],"emoji":"🥻","order":3837,"group":7,"version":12},{"shortcodes":["one_piece_swimsuit"],"annotation":"one-piece swimsuit","tags":["bathing suit"],"emoji":"🩱","order":3838,"group":7,"version":12},{"shortcodes":["briefs"],"annotation":"briefs","tags":["bathing suit","one-piece","swimsuit","underwear"],"emoji":"🩲","order":3839,"group":7,"version":12},{"shortcodes":["shorts"],"annotation":"shorts","tags":["bathing suit","pants","underwear"],"emoji":"🩳","order":3840,"group":7,"version":12},{"shortcodes":["bikini"],"annotation":"bikini","tags":["clothing","swim"],"emoji":"👙","order":3841,"group":7,"version":0.6},{"shortcodes":["womans_clothes"],"annotation":"woman’s clothes","tags":["clothing","woman"],"emoji":"👚","order":3842,"group":7,"version":0.6},{"shortcodes":["purse"],"annotation":"purse","tags":["clothing","coin"],"emoji":"👛","order":3843,"group":7,"version":0.6},{"shortcodes":["handbag"],"annotation":"handbag","tags":["bag","clothing","purse"],"emoji":"👜","order":3844,"group":7,"version":0.6},{"shortcodes":["clutch_bag","pouch"],"annotation":"clutch bag","tags":["bag","clothing","pouch"],"emoji":"👝","order":3845,"group":7,"version":0.6},{"shortcodes":["shopping_bags"],"annotation":"shopping bags","tags":["bag","hotel","shopping"],"emoji":"🛍️","order":3847,"group":7,"version":0.7},{"shortcodes":["backpack","school_satchel"],"annotation":"backpack","tags":["bag","rucksack","satchel","school"],"emoji":"🎒","order":3848,"group":7,"version":0.6},{"shortcodes":["thong_sandal"],"annotation":"thong sandal","tags":["beach sandals","sandals","thong sandals","thongs","zōri"],"emoji":"🩴","order":3849,"group":7,"version":13},{"shortcodes":["mans_shoe"],"annotation":"man’s shoe","tags":["clothing","man","shoe"],"emoji":"👞","order":3850,"group":7,"version":0.6},{"shortcodes":["athletic_shoe","sneaker"],"annotation":"running shoe","tags":["athletic","clothing","shoe","sneaker"],"emoji":"👟","order":3851,"group":7,"version":0.6},{"shortcodes":["hiking_boot"],"annotation":"hiking boot","tags":["backpacking","boot","camping","hiking"],"emoji":"🥾","order":3852,"group":7,"version":11},{"shortcodes":["flat_shoe","womans_flat_shoe"],"annotation":"flat shoe","tags":["ballet flat","slip-on","slipper"],"emoji":"🥿","order":3853,"group":7,"version":11},{"shortcodes":["high_heel"],"annotation":"high-heeled shoe","tags":["clothing","heel","shoe","woman"],"emoji":"👠","order":3854,"group":7,"version":0.6},{"shortcodes":["sandal"],"annotation":"woman’s sandal","tags":["clothing","sandal","shoe","woman"],"emoji":"👡","order":3855,"group":7,"version":0.6},{"shortcodes":["ballet_shoes"],"annotation":"ballet shoes","tags":["ballet","dance"],"emoji":"🩰","order":3856,"group":7,"version":12},{"shortcodes":["boot"],"annotation":"woman’s boot","tags":["boot","clothing","shoe","woman"],"emoji":"👢","order":3857,"group":7,"version":0.6},{"shortcodes":["crown"],"annotation":"crown","tags":["clothing","king","queen"],"emoji":"👑","order":3858,"group":7,"version":0.6},{"shortcodes":["womans_hat"],"annotation":"woman’s hat","tags":["clothing","hat","woman"],"emoji":"👒","order":3859,"group":7,"version":0.6},{"shortcodes":["top_hat","tophat"],"annotation":"top hat","tags":["clothing","hat","top","tophat"],"emoji":"🎩","order":3860,"group":7,"version":0.6},{"shortcodes":["graduation_cap","mortar_board"],"annotation":"graduation cap","tags":["cap","celebration","clothing","graduation","hat"],"emoji":"🎓️","order":3861,"group":7,"version":0.6},{"shortcodes":["billed_cap"],"annotation":"billed cap","tags":["baseball cap"],"emoji":"🧢","order":3862,"group":7,"version":5},{"shortcodes":["military_helmet"],"annotation":"military helmet","tags":["army","helmet","military","soldier","warrior"],"emoji":"🪖","order":3863,"group":7,"version":13},{"shortcodes":["helmet_with_cross","rescue_worker_helmet"],"annotation":"rescue worker’s helmet","tags":["aid","cross","face","hat","helmet"],"emoji":"⛑️","order":3865,"group":7,"version":0.7},{"shortcodes":["prayer_beads"],"annotation":"prayer beads","tags":["beads","clothing","necklace","prayer","religion"],"emoji":"📿","order":3866,"group":7,"version":1},{"shortcodes":["lipstick"],"annotation":"lipstick","tags":["cosmetics","makeup"],"emoji":"💄","order":3867,"group":7,"version":0.6},{"shortcodes":["ring"],"annotation":"ring","tags":["diamond"],"emoji":"💍","order":3868,"group":7,"version":0.6},{"shortcodes":["gem"],"annotation":"gem stone","tags":["diamond","gem","jewel"],"emoji":"💎","order":3869,"group":7,"version":0.6},{"shortcodes":["mute","no_sound"],"annotation":"muted speaker","tags":["mute","quiet","silent","speaker"],"emoji":"🔇","order":3870,"group":7,"version":1},{"shortcodes":["low_volume","quiet_sound","speaker"],"annotation":"speaker low volume","tags":["soft"],"emoji":"🔈️","order":3871,"group":7,"version":0.7},{"shortcodes":["medium_volumne","sound"],"annotation":"speaker medium volume","tags":["medium"],"emoji":"🔉","order":3872,"group":7,"version":1},{"shortcodes":["high_volume","loud_sound"],"annotation":"speaker high volume","tags":["loud"],"emoji":"🔊","order":3873,"group":7,"version":0.6},{"shortcodes":["loudspeaker"],"annotation":"loudspeaker","tags":["loud","public address"],"emoji":"📢","order":3874,"group":7,"version":0.6},{"shortcodes":["mega","megaphone"],"annotation":"megaphone","tags":["cheering"],"emoji":"📣","order":3875,"group":7,"version":0.6},{"shortcodes":["postal_horn"],"annotation":"postal horn","tags":["horn","post","postal"],"emoji":"📯","order":3876,"group":7,"version":1},{"shortcodes":["bell"],"annotation":"bell","tags":["bell"],"emoji":"🔔","order":3877,"group":7,"version":0.6},{"shortcodes":["no_bell"],"annotation":"bell with slash","tags":["bell","forbidden","mute","quiet","silent"],"emoji":"🔕","order":3878,"group":7,"version":1},{"shortcodes":["musical_score"],"annotation":"musical score","tags":["music","score"],"emoji":"🎼","order":3879,"group":7,"version":0.6},{"shortcodes":["musical_note"],"annotation":"musical note","tags":["music","note"],"emoji":"🎵","order":3880,"group":7,"version":0.6},{"shortcodes":["musical_notes","notes"],"annotation":"musical notes","tags":["music","note","notes"],"emoji":"🎶","order":3881,"group":7,"version":0.6},{"shortcodes":["studio_microphone"],"annotation":"studio microphone","tags":["mic","microphone","music","studio"],"emoji":"🎙️","order":3883,"group":7,"version":0.7},{"shortcodes":["level_slider"],"annotation":"level slider","tags":["level","music","slider"],"emoji":"🎚️","order":3885,"group":7,"version":0.7},{"shortcodes":["control_knobs"],"annotation":"control knobs","tags":["control","knobs","music"],"emoji":"🎛️","order":3887,"group":7,"version":0.7},{"shortcodes":["microphone"],"annotation":"microphone","tags":["karaoke","mic"],"emoji":"🎤","order":3888,"group":7,"version":0.6},{"shortcodes":["headphones"],"annotation":"headphone","tags":["earbud"],"emoji":"🎧️","order":3889,"group":7,"version":0.6},{"shortcodes":["radio"],"annotation":"radio","tags":["video"],"emoji":"📻️","order":3890,"group":7,"version":0.6},{"shortcodes":["saxophone"],"annotation":"saxophone","tags":["instrument","music","sax"],"emoji":"🎷","order":3891,"group":7,"version":0.6},{"shortcodes":["accordion"],"annotation":"accordion","tags":["concertina","squeeze box"],"emoji":"🪗","order":3892,"group":7,"version":13},{"shortcodes":["guitar"],"annotation":"guitar","tags":["instrument","music"],"emoji":"🎸","order":3893,"group":7,"version":0.6},{"shortcodes":["musical_keyboard"],"annotation":"musical keyboard","tags":["instrument","keyboard","music","piano"],"emoji":"🎹","order":3894,"group":7,"version":0.6},{"shortcodes":["trumpet"],"annotation":"trumpet","tags":["instrument","music"],"emoji":"🎺","order":3895,"group":7,"version":0.6},{"shortcodes":["violin"],"annotation":"violin","tags":["instrument","music"],"emoji":"🎻","order":3896,"group":7,"version":0.6},{"shortcodes":["banjo"],"annotation":"banjo","tags":["music","stringed"],"emoji":"🪕","order":3897,"group":7,"version":12},{"shortcodes":["drum"],"annotation":"drum","tags":["drumsticks","music"],"emoji":"🥁","order":3898,"group":7,"version":3},{"shortcodes":["long_drum"],"annotation":"long drum","tags":["beat","conga","drum","rhythm"],"emoji":"🪘","order":3899,"group":7,"version":13},{"shortcodes":["android","iphone","mobile_phone"],"annotation":"mobile phone","tags":["cell","mobile","phone","telephone"],"emoji":"📱","order":3900,"group":7,"version":0.6},{"shortcodes":["calling","mobile_phone_arrow"],"annotation":"mobile phone with arrow","tags":["arrow","cell","mobile","phone","receive"],"emoji":"📲","order":3901,"group":7,"version":0.6},{"shortcodes":["telephone"],"annotation":"telephone","tags":["phone"],"emoji":"☎️","order":3903,"group":7,"version":0.6},{"shortcodes":["telephone_receiver"],"annotation":"telephone receiver","tags":["phone","receiver","telephone"],"emoji":"📞","order":3904,"group":7,"version":0.6},{"shortcodes":["pager"],"annotation":"pager","tags":["pager"],"emoji":"📟️","order":3905,"group":7,"version":0.6},{"shortcodes":["fax","fax_machine"],"annotation":"fax machine","tags":["fax"],"emoji":"📠","order":3906,"group":7,"version":0.6},{"shortcodes":["battery"],"annotation":"battery","tags":["battery"],"emoji":"🔋","order":3907,"group":7,"version":0.6},{"shortcodes":["low_battery"],"annotation":"low battery","tags":["electronic","low energy"],"emoji":"🪫","order":3908,"group":7,"version":14},{"shortcodes":["electric_plug"],"annotation":"electric plug","tags":["electric","electricity","plug"],"emoji":"🔌","order":3909,"group":7,"version":0.6},{"shortcodes":["laptop"],"annotation":"laptop","tags":["computer","pc","personal"],"emoji":"💻️","order":3910,"group":7,"version":0.6},{"shortcodes":["computer","desktop_computer"],"annotation":"desktop computer","tags":["computer","desktop"],"emoji":"🖥️","order":3912,"group":7,"version":0.7},{"shortcodes":["printer"],"annotation":"printer","tags":["computer"],"emoji":"🖨️","order":3914,"group":7,"version":0.7},{"shortcodes":["keyboard"],"annotation":"keyboard","tags":["computer"],"emoji":"⌨️","order":3916,"group":7,"version":1},{"shortcodes":["computer_mouse"],"annotation":"computer mouse","tags":["computer"],"emoji":"🖱️","order":3918,"group":7,"version":0.7},{"shortcodes":["trackball"],"annotation":"trackball","tags":["computer"],"emoji":"🖲️","order":3920,"group":7,"version":0.7},{"shortcodes":["computer_disk","minidisc"],"annotation":"computer disk","tags":["computer","disk","minidisk","optical"],"emoji":"💽","order":3921,"group":7,"version":0.6},{"shortcodes":["floppy_disk"],"annotation":"floppy disk","tags":["computer","disk","floppy"],"emoji":"💾","order":3922,"group":7,"version":0.6},{"shortcodes":["cd","optical_disk"],"annotation":"optical disk","tags":["cd","computer","disk","optical"],"emoji":"💿️","order":3923,"group":7,"version":0.6},{"shortcodes":["dvd"],"annotation":"dvd","tags":["blu-ray","computer","disk","optical"],"emoji":"📀","order":3924,"group":7,"version":0.6},{"shortcodes":["abacus"],"annotation":"abacus","tags":["calculation"],"emoji":"🧮","order":3925,"group":7,"version":11},{"shortcodes":["movie_camera"],"annotation":"movie camera","tags":["camera","cinema","movie"],"emoji":"🎥","order":3926,"group":7,"version":0.6},{"shortcodes":["film_frames"],"annotation":"film frames","tags":["cinema","film","frames","movie"],"emoji":"🎞️","order":3928,"group":7,"version":0.7},{"shortcodes":["film_projector"],"annotation":"film projector","tags":["cinema","film","movie","projector","video"],"emoji":"📽️","order":3930,"group":7,"version":0.7},{"shortcodes":["clapper"],"annotation":"clapper board","tags":["clapper","movie"],"emoji":"🎬️","order":3931,"group":7,"version":0.6},{"shortcodes":["tv"],"annotation":"television","tags":["tv","video"],"emoji":"📺️","order":3932,"group":7,"version":0.6},{"shortcodes":["camera"],"annotation":"camera","tags":["video"],"emoji":"📷️","order":3933,"group":7,"version":0.6},{"shortcodes":["camera_with_flash"],"annotation":"camera with flash","tags":["camera","flash","video"],"emoji":"📸","order":3934,"group":7,"version":1},{"shortcodes":["video_camera"],"annotation":"video camera","tags":["camera","video"],"emoji":"📹️","order":3935,"group":7,"version":0.6},{"shortcodes":["vhs","videocassette"],"annotation":"videocassette","tags":["tape","vhs","video"],"emoji":"📼","order":3936,"group":7,"version":0.6},{"shortcodes":["mag"],"annotation":"magnifying glass tilted left","tags":["glass","magnifying","search","tool"],"emoji":"🔍️","order":3937,"group":7,"version":0.6},{"shortcodes":["mag_right"],"annotation":"magnifying glass tilted right","tags":["glass","magnifying","search","tool"],"emoji":"🔎","order":3938,"group":7,"version":0.6},{"shortcodes":["candle"],"annotation":"candle","tags":["light"],"emoji":"🕯️","order":3940,"group":7,"version":0.7},{"shortcodes":["bulb","light_bulb"],"annotation":"light bulb","tags":["bulb","comic","electric","idea","light"],"emoji":"💡","order":3941,"group":7,"version":0.6},{"shortcodes":["flashlight"],"annotation":"flashlight","tags":["electric","light","tool","torch"],"emoji":"🔦","order":3942,"group":7,"version":0.6},{"shortcodes":["izakaya_lantern","red_paper_lantern"],"annotation":"red paper lantern","tags":["bar","lantern","light","red"],"emoji":"🏮","order":3943,"group":7,"version":0.6},{"shortcodes":["diya_lamp"],"annotation":"diya lamp","tags":["diya","lamp","oil"],"emoji":"🪔","order":3944,"group":7,"version":12},{"shortcodes":["notebook_with_decorative_cover"],"annotation":"notebook with decorative cover","tags":["book","cover","decorated","notebook"],"emoji":"📔","order":3945,"group":7,"version":0.6},{"shortcodes":["closed_book"],"annotation":"closed book","tags":["book","closed"],"emoji":"📕","order":3946,"group":7,"version":0.6},{"shortcodes":["book","open_book"],"annotation":"open book","tags":["book","open"],"emoji":"📖","order":3947,"group":7,"version":0.6},{"shortcodes":["green_book"],"annotation":"green book","tags":["book","green"],"emoji":"📗","order":3948,"group":7,"version":0.6},{"shortcodes":["blue_book"],"annotation":"blue book","tags":["blue","book"],"emoji":"📘","order":3949,"group":7,"version":0.6},{"shortcodes":["orange_book"],"annotation":"orange book","tags":["book","orange"],"emoji":"📙","order":3950,"group":7,"version":0.6},{"shortcodes":["books"],"annotation":"books","tags":["book"],"emoji":"📚️","order":3951,"group":7,"version":0.6},{"shortcodes":["notebook"],"annotation":"notebook","tags":["notebook"],"emoji":"📓","order":3952,"group":7,"version":0.6},{"shortcodes":["ledger"],"annotation":"ledger","tags":["notebook"],"emoji":"📒","order":3953,"group":7,"version":0.6},{"shortcodes":["page_with_curl"],"annotation":"page with curl","tags":["curl","document","page"],"emoji":"📃","order":3954,"group":7,"version":0.6},{"shortcodes":["scroll"],"annotation":"scroll","tags":["paper"],"emoji":"📜","order":3955,"group":7,"version":0.6},{"shortcodes":["page_facing_up"],"annotation":"page facing up","tags":["document","page"],"emoji":"📄","order":3956,"group":7,"version":0.6},{"shortcodes":["newspaper"],"annotation":"newspaper","tags":["news","paper"],"emoji":"📰","order":3957,"group":7,"version":0.6},{"shortcodes":["rolled_up_newspaper"],"annotation":"rolled-up newspaper","tags":["news","newspaper","paper","rolled"],"emoji":"🗞️","order":3959,"group":7,"version":0.7},{"shortcodes":["bookmark_tabs"],"annotation":"bookmark tabs","tags":["bookmark","mark","marker","tabs"],"emoji":"📑","order":3960,"group":7,"version":0.6},{"shortcodes":["bookmark"],"annotation":"bookmark","tags":["mark"],"emoji":"🔖","order":3961,"group":7,"version":0.6},{"shortcodes":["label"],"annotation":"label","tags":["label"],"emoji":"🏷️","order":3963,"group":7,"version":0.7},{"shortcodes":["moneybag"],"annotation":"money bag","tags":["bag","dollar","money","moneybag"],"emoji":"💰️","order":3964,"group":7,"version":0.6},{"shortcodes":["coin"],"annotation":"coin","tags":["gold","metal","money","silver","treasure"],"emoji":"🪙","order":3965,"group":7,"version":13},{"shortcodes":["yen"],"annotation":"yen banknote","tags":["banknote","bill","currency","money","note","yen"],"emoji":"💴","order":3966,"group":7,"version":0.6},{"shortcodes":["dollar"],"annotation":"dollar banknote","tags":["banknote","bill","currency","dollar","money","note"],"emoji":"💵","order":3967,"group":7,"version":0.6},{"shortcodes":["euro"],"annotation":"euro banknote","tags":["banknote","bill","currency","euro","money","note"],"emoji":"💶","order":3968,"group":7,"version":1},{"shortcodes":["pound"],"annotation":"pound banknote","tags":["banknote","bill","currency","money","note","pound"],"emoji":"💷","order":3969,"group":7,"version":1},{"shortcodes":["money_with_wings"],"annotation":"money with wings","tags":["banknote","bill","fly","money","wings"],"emoji":"💸","order":3970,"group":7,"version":0.6},{"shortcodes":["credit_card"],"annotation":"credit card","tags":["card","credit","money"],"emoji":"💳️","order":3971,"group":7,"version":0.6},{"shortcodes":["receipt"],"annotation":"receipt","tags":["accounting","bookkeeping","evidence","proof"],"emoji":"🧾","order":3972,"group":7,"version":11},{"shortcodes":["chart"],"annotation":"chart increasing with yen","tags":["chart","graph","growth","money","yen"],"emoji":"💹","order":3973,"group":7,"version":0.6},{"shortcodes":["envelope"],"annotation":"envelope","tags":["email","letter"],"emoji":"✉️","order":3975,"group":7,"version":0.6},{"shortcodes":["e-mail","email"],"annotation":"e-mail","tags":["email","letter","mail"],"emoji":"📧","order":3976,"group":7,"version":0.6},{"shortcodes":["incoming_envelope"],"annotation":"incoming envelope","tags":["e-mail","email","envelope","incoming","letter","receive"],"emoji":"📨","order":3977,"group":7,"version":0.6},{"shortcodes":["envelope_with_arrow"],"annotation":"envelope with arrow","tags":["arrow","e-mail","email","envelope","outgoing"],"emoji":"📩","order":3978,"group":7,"version":0.6},{"shortcodes":["outbox_tray"],"annotation":"outbox tray","tags":["box","letter","mail","outbox","sent","tray"],"emoji":"📤️","order":3979,"group":7,"version":0.6},{"shortcodes":["inbox_tray"],"annotation":"inbox tray","tags":["box","inbox","letter","mail","receive","tray"],"emoji":"📥️","order":3980,"group":7,"version":0.6},{"shortcodes":["package"],"annotation":"package","tags":["box","parcel"],"emoji":"📦️","order":3981,"group":7,"version":0.6},{"shortcodes":["mailbox"],"annotation":"closed mailbox with raised flag","tags":["closed","mail","mailbox","postbox"],"emoji":"📫️","order":3982,"group":7,"version":0.6},{"shortcodes":["mailbox_closed"],"annotation":"closed mailbox with lowered flag","tags":["closed","lowered","mail","mailbox","postbox"],"emoji":"📪️","order":3983,"group":7,"version":0.6},{"shortcodes":["mailbox_with_mail"],"annotation":"open mailbox with raised flag","tags":["mail","mailbox","open","postbox"],"emoji":"📬️","order":3984,"group":7,"version":0.7},{"shortcodes":["mailbox_with_no_mail"],"annotation":"open mailbox with lowered flag","tags":["lowered","mail","mailbox","open","postbox"],"emoji":"📭️","order":3985,"group":7,"version":0.7},{"shortcodes":["postbox"],"annotation":"postbox","tags":["mail","mailbox"],"emoji":"📮","order":3986,"group":7,"version":0.6},{"shortcodes":["ballot_box"],"annotation":"ballot box with ballot","tags":["ballot","box"],"emoji":"🗳️","order":3988,"group":7,"version":0.7},{"shortcodes":["pencil"],"annotation":"pencil","tags":["pencil"],"emoji":"✏️","order":3990,"group":7,"version":0.6},{"shortcodes":["black_nib"],"annotation":"black nib","tags":["nib","pen"],"emoji":"✒️","order":3992,"group":7,"version":0.6},{"shortcodes":["fountain_pen"],"annotation":"fountain pen","tags":["fountain","pen"],"emoji":"🖋️","order":3994,"group":7,"version":0.7},{"shortcodes":["pen"],"annotation":"pen","tags":["ballpoint"],"emoji":"🖊️","order":3996,"group":7,"version":0.7},{"shortcodes":["paintbrush"],"annotation":"paintbrush","tags":["painting"],"emoji":"🖌️","order":3998,"group":7,"version":0.7},{"shortcodes":["crayon"],"annotation":"crayon","tags":["crayon"],"emoji":"🖍️","order":4000,"group":7,"version":0.7},{"shortcodes":["memo"],"annotation":"memo","tags":["pencil"],"emoji":"📝","order":4001,"group":7,"version":0.6},{"shortcodes":["briefcase"],"annotation":"briefcase","tags":["briefcase"],"emoji":"💼","order":4002,"group":7,"version":0.6},{"shortcodes":["file_folder"],"annotation":"file folder","tags":["file","folder"],"emoji":"📁","order":4003,"group":7,"version":0.6},{"shortcodes":["open_file_folder"],"annotation":"open file folder","tags":["file","folder","open"],"emoji":"📂","order":4004,"group":7,"version":0.6},{"shortcodes":["card_index_dividers"],"annotation":"card index dividers","tags":["card","dividers","index"],"emoji":"🗂️","order":4006,"group":7,"version":0.7},{"shortcodes":["date"],"annotation":"calendar","tags":["date"],"emoji":"📅","order":4007,"group":7,"version":0.6},{"shortcodes":["calendar"],"annotation":"tear-off calendar","tags":["calendar"],"emoji":"📆","order":4008,"group":7,"version":0.6},{"shortcodes":["notepad_spiral"],"annotation":"spiral notepad","tags":["note","pad","spiral"],"emoji":"🗒️","order":4010,"group":7,"version":0.7},{"shortcodes":["calendar_spiral"],"annotation":"spiral calendar","tags":["calendar","pad","spiral"],"emoji":"🗓️","order":4012,"group":7,"version":0.7},{"shortcodes":["card_index"],"annotation":"card index","tags":["card","index","rolodex"],"emoji":"📇","order":4013,"group":7,"version":0.6},{"shortcodes":["chart_increasing","chart_with_upwards_trend"],"annotation":"chart increasing","tags":["chart","graph","growth","trend","upward"],"emoji":"📈","order":4014,"group":7,"version":0.6},{"shortcodes":["chart_decreasing","chart_with_downwards_trend"],"annotation":"chart decreasing","tags":["chart","down","graph","trend"],"emoji":"📉","order":4015,"group":7,"version":0.6},{"shortcodes":["bar_chart"],"annotation":"bar chart","tags":["bar","chart","graph"],"emoji":"📊","order":4016,"group":7,"version":0.6},{"shortcodes":["clipboard"],"annotation":"clipboard","tags":["clipboard"],"emoji":"📋️","order":4017,"group":7,"version":0.6},{"shortcodes":["pushpin"],"annotation":"pushpin","tags":["pin"],"emoji":"📌","order":4018,"group":7,"version":0.6},{"shortcodes":["round_pushpin"],"annotation":"round pushpin","tags":["pin","pushpin"],"emoji":"📍","order":4019,"group":7,"version":0.6},{"shortcodes":["paperclip"],"annotation":"paperclip","tags":["paperclip"],"emoji":"📎","order":4020,"group":7,"version":0.6},{"shortcodes":["paperclips"],"annotation":"linked paperclips","tags":["link","paperclip"],"emoji":"🖇️","order":4022,"group":7,"version":0.7},{"shortcodes":["straight_ruler"],"annotation":"straight ruler","tags":["ruler","straight edge"],"emoji":"📏","order":4023,"group":7,"version":0.6},{"shortcodes":["triangular_ruler"],"annotation":"triangular ruler","tags":["ruler","set","triangle"],"emoji":"📐","order":4024,"group":7,"version":0.6},{"shortcodes":["scissors"],"annotation":"scissors","tags":["cutting","tool"],"emoji":"✂️","order":4026,"group":7,"version":0.6},{"shortcodes":["card_file_box"],"annotation":"card file box","tags":["box","card","file"],"emoji":"🗃️","order":4028,"group":7,"version":0.7},{"shortcodes":["file_cabinet"],"annotation":"file cabinet","tags":["cabinet","file","filing"],"emoji":"🗄️","order":4030,"group":7,"version":0.7},{"shortcodes":["trashcan","wastebasket"],"annotation":"wastebasket","tags":["wastebasket"],"emoji":"🗑️","order":4032,"group":7,"version":0.7},{"shortcodes":["lock","locked"],"annotation":"locked","tags":["closed"],"emoji":"🔒️","order":4033,"group":7,"version":0.6},{"shortcodes":["unlock","unlocked"],"annotation":"unlocked","tags":["lock","open","unlock"],"emoji":"🔓️","order":4034,"group":7,"version":0.6},{"shortcodes":["lock_with_ink_pen","locked_with_pen"],"annotation":"locked with pen","tags":["ink","lock","nib","pen","privacy"],"emoji":"🔏","order":4035,"group":7,"version":0.6},{"shortcodes":["closed_lock_with_key","locked_with_key"],"annotation":"locked with key","tags":["closed","key","lock","secure"],"emoji":"🔐","order":4036,"group":7,"version":0.6},{"shortcodes":["key"],"annotation":"key","tags":["lock","password"],"emoji":"🔑","order":4037,"group":7,"version":0.6},{"shortcodes":["old_key"],"annotation":"old key","tags":["clue","key","lock","old"],"emoji":"🗝️","order":4039,"group":7,"version":0.7},{"shortcodes":["hammer"],"annotation":"hammer","tags":["tool"],"emoji":"🔨","order":4040,"group":7,"version":0.6},{"shortcodes":["axe"],"annotation":"axe","tags":["chop","hatchet","split","wood"],"emoji":"🪓","order":4041,"group":7,"version":12},{"shortcodes":["pick"],"annotation":"pick","tags":["mining","tool"],"emoji":"⛏️","order":4043,"group":7,"version":0.7},{"shortcodes":["hammer_and_pick"],"annotation":"hammer and pick","tags":["hammer","pick","tool"],"emoji":"⚒️","order":4045,"group":7,"version":1},{"shortcodes":["hammer_and_wrench"],"annotation":"hammer and wrench","tags":["hammer","spanner","tool","wrench"],"emoji":"🛠️","order":4047,"group":7,"version":0.7},{"shortcodes":["dagger"],"annotation":"dagger","tags":["knife","weapon"],"emoji":"🗡️","order":4049,"group":7,"version":0.7},{"shortcodes":["crossed_swords"],"annotation":"crossed swords","tags":["crossed","swords","weapon"],"emoji":"⚔️","order":4051,"group":7,"version":1},{"shortcodes":["gun","pistol"],"annotation":"water pistol","tags":["gun","handgun","pistol","revolver","tool","water","weapon"],"emoji":"🔫","order":4052,"group":7,"version":0.6},{"shortcodes":["boomerang"],"annotation":"boomerang","tags":["australia","rebound","repercussion"],"emoji":"🪃","order":4053,"group":7,"version":13},{"shortcodes":["bow_and_arrow"],"annotation":"bow and arrow","tags":["archer","arrow","bow","sagittarius","zodiac"],"emoji":"🏹","order":4054,"group":7,"version":1},{"shortcodes":["shield"],"annotation":"shield","tags":["weapon"],"emoji":"🛡️","order":4056,"group":7,"version":0.7},{"shortcodes":["carpentry_saw"],"annotation":"carpentry saw","tags":["carpenter","lumber","saw","tool"],"emoji":"🪚","order":4057,"group":7,"version":13},{"shortcodes":["wrench"],"annotation":"wrench","tags":["spanner","tool"],"emoji":"🔧","order":4058,"group":7,"version":0.6},{"shortcodes":["screwdriver"],"annotation":"screwdriver","tags":["screw","tool"],"emoji":"🪛","order":4059,"group":7,"version":13},{"shortcodes":["nut_and_bolt"],"annotation":"nut and bolt","tags":["bolt","nut","tool"],"emoji":"🔩","order":4060,"group":7,"version":0.6},{"shortcodes":["gear"],"annotation":"gear","tags":["cog","cogwheel","tool"],"emoji":"⚙️","order":4062,"group":7,"version":1},{"shortcodes":["clamp","compression"],"annotation":"clamp","tags":["compress","tool","vice"],"emoji":"🗜️","order":4064,"group":7,"version":0.7},{"shortcodes":["scales"],"annotation":"balance scale","tags":["balance","justice","libra","scale","zodiac"],"emoji":"⚖️","order":4066,"group":7,"version":1},{"shortcodes":["probing_cane","white_cane"],"annotation":"white cane","tags":["accessibility","blind"],"emoji":"🦯","order":4067,"group":7,"version":12},{"shortcodes":["link"],"annotation":"link","tags":["link"],"emoji":"🔗","order":4068,"group":7,"version":0.6},{"shortcodes":["chains"],"annotation":"chains","tags":["chain"],"emoji":"⛓️","order":4070,"group":7,"version":0.7},{"shortcodes":["hook"],"annotation":"hook","tags":["catch","crook","curve","ensnare","selling point"],"emoji":"🪝","order":4071,"group":7,"version":13},{"shortcodes":["toolbox"],"annotation":"toolbox","tags":["chest","mechanic","tool"],"emoji":"🧰","order":4072,"group":7,"version":11},{"shortcodes":["magnet"],"annotation":"magnet","tags":["attraction","horseshoe","magnetic"],"emoji":"🧲","order":4073,"group":7,"version":11},{"shortcodes":["ladder"],"annotation":"ladder","tags":["climb","rung","step"],"emoji":"🪜","order":4074,"group":7,"version":13},{"shortcodes":["alembic"],"annotation":"alembic","tags":["chemistry","tool"],"emoji":"⚗️","order":4076,"group":7,"version":1},{"shortcodes":["test_tube"],"annotation":"test tube","tags":["chemist","chemistry","experiment","lab","science"],"emoji":"🧪","order":4077,"group":7,"version":11},{"shortcodes":["petri_dish"],"annotation":"petri dish","tags":["bacteria","biologist","biology","culture","lab"],"emoji":"🧫","order":4078,"group":7,"version":11},{"shortcodes":["dna","double_helix"],"annotation":"dna","tags":["biologist","evolution","gene","genetics","life"],"emoji":"🧬","order":4079,"group":7,"version":11},{"shortcodes":["microscope"],"annotation":"microscope","tags":["science","tool"],"emoji":"🔬","order":4080,"group":7,"version":1},{"shortcodes":["telescope"],"annotation":"telescope","tags":["science","tool"],"emoji":"🔭","order":4081,"group":7,"version":1},{"shortcodes":["satellite_antenna"],"annotation":"satellite antenna","tags":["antenna","dish","satellite"],"emoji":"📡","order":4082,"group":7,"version":0.6},{"shortcodes":["syringe"],"annotation":"syringe","tags":["medicine","needle","shot","sick"],"emoji":"💉","order":4083,"group":7,"version":0.6},{"shortcodes":["drop_of_blood"],"annotation":"drop of blood","tags":["bleed","blood donation","injury","medicine","menstruation"],"emoji":"🩸","order":4084,"group":7,"version":12},{"shortcodes":["pill"],"annotation":"pill","tags":["doctor","medicine","sick"],"emoji":"💊","order":4085,"group":7,"version":0.6},{"shortcodes":["adhesive_bandage","bandaid"],"annotation":"adhesive bandage","tags":["bandage"],"emoji":"🩹","order":4086,"group":7,"version":12},{"shortcodes":["crutch"],"annotation":"crutch","tags":["cane","disability","hurt","mobility aid","stick"],"emoji":"🩼","order":4087,"group":7,"version":14},{"shortcodes":["stethoscope"],"annotation":"stethoscope","tags":["doctor","heart","medicine"],"emoji":"🩺","order":4088,"group":7,"version":12},{"shortcodes":["x-ray","xray"],"annotation":"x-ray","tags":["bones","doctor","medical","skeleton"],"emoji":"🩻","order":4089,"group":7,"version":14},{"shortcodes":["door"],"annotation":"door","tags":["door"],"emoji":"🚪","order":4090,"group":7,"version":0.6},{"shortcodes":["elevator"],"annotation":"elevator","tags":["accessibility","hoist","lift"],"emoji":"🛗","order":4091,"group":7,"version":13},{"shortcodes":["mirror"],"annotation":"mirror","tags":["reflection","reflector","speculum"],"emoji":"🪞","order":4092,"group":7,"version":13},{"shortcodes":["window"],"annotation":"window","tags":["frame","fresh air","opening","transparent","view"],"emoji":"🪟","order":4093,"group":7,"version":13},{"shortcodes":["bed"],"annotation":"bed","tags":["hotel","sleep"],"emoji":"🛏️","order":4095,"group":7,"version":0.7},{"shortcodes":["couch_and_lamp"],"annotation":"couch and lamp","tags":["couch","hotel","lamp"],"emoji":"🛋️","order":4097,"group":7,"version":0.7},{"shortcodes":["chair"],"annotation":"chair","tags":["seat","sit"],"emoji":"🪑","order":4098,"group":7,"version":12},{"shortcodes":["toilet"],"annotation":"toilet","tags":["toilet"],"emoji":"🚽","order":4099,"group":7,"version":0.6},{"shortcodes":["plunger"],"annotation":"plunger","tags":["force cup","plumber","suction","toilet"],"emoji":"🪠","order":4100,"group":7,"version":13},{"shortcodes":["shower"],"annotation":"shower","tags":["water"],"emoji":"🚿","order":4101,"group":7,"version":1},{"shortcodes":["bathtub"],"annotation":"bathtub","tags":["bath"],"emoji":"🛁","order":4102,"group":7,"version":1},{"shortcodes":["mouse_trap"],"annotation":"mouse trap","tags":["bait","mousetrap","snare","trap"],"emoji":"🪤","order":4103,"group":7,"version":13},{"shortcodes":["razor"],"annotation":"razor","tags":["sharp","shave"],"emoji":"🪒","order":4104,"group":7,"version":12},{"shortcodes":["lotion_bottle"],"annotation":"lotion bottle","tags":["lotion","moisturizer","shampoo","sunscreen"],"emoji":"🧴","order":4105,"group":7,"version":11},{"shortcodes":["safety_pin"],"annotation":"safety pin","tags":["diaper","punk rock"],"emoji":"🧷","order":4106,"group":7,"version":11},{"shortcodes":["broom"],"annotation":"broom","tags":["cleaning","sweeping","witch"],"emoji":"🧹","order":4107,"group":7,"version":11},{"shortcodes":["basket"],"annotation":"basket","tags":["farming","laundry","picnic"],"emoji":"🧺","order":4108,"group":7,"version":11},{"shortcodes":["roll_of_paper","toilet_paper"],"annotation":"roll of paper","tags":["paper towels","toilet paper"],"emoji":"🧻","order":4109,"group":7,"version":11},{"shortcodes":["bucket"],"annotation":"bucket","tags":["cask","pail","vat"],"emoji":"🪣","order":4110,"group":7,"version":13},{"shortcodes":["soap"],"annotation":"soap","tags":["bar","bathing","cleaning","lather","soapdish"],"emoji":"🧼","order":4111,"group":7,"version":11},{"shortcodes":["bubbles"],"annotation":"bubbles","tags":["burp","clean","soap","underwater"],"emoji":"🫧","order":4112,"group":7,"version":14},{"shortcodes":["toothbrush"],"annotation":"toothbrush","tags":["bathroom","brush","clean","dental","hygiene","teeth"],"emoji":"🪥","order":4113,"group":7,"version":13},{"shortcodes":["sponge"],"annotation":"sponge","tags":["absorbing","cleaning","porous"],"emoji":"🧽","order":4114,"group":7,"version":11},{"shortcodes":["fire_extinguisher"],"annotation":"fire extinguisher","tags":["extinguish","fire","quench"],"emoji":"🧯","order":4115,"group":7,"version":11},{"shortcodes":["shopping_cart"],"annotation":"shopping cart","tags":["cart","shopping","trolley"],"emoji":"🛒","order":4116,"group":7,"version":3},{"shortcodes":["cigarette","smoking"],"annotation":"cigarette","tags":["smoking"],"emoji":"🚬","order":4117,"group":7,"version":0.6},{"shortcodes":["coffin"],"annotation":"coffin","tags":["death"],"emoji":"⚰️","order":4119,"group":7,"version":1},{"shortcodes":["headstone"],"annotation":"headstone","tags":["cemetery","grave","graveyard","tombstone"],"emoji":"🪦","order":4120,"group":7,"version":13},{"shortcodes":["funeral_urn"],"annotation":"funeral urn","tags":["ashes","death","funeral","urn"],"emoji":"⚱️","order":4122,"group":7,"version":1},{"shortcodes":["moai","moyai"],"annotation":"moai","tags":["face","moyai","statue"],"emoji":"🗿","order":4123,"group":7,"version":0.6},{"shortcodes":["placard"],"annotation":"placard","tags":["demonstration","picket","protest","sign"],"emoji":"🪧","order":4124,"group":7,"version":13},{"shortcodes":["id_card"],"annotation":"identification card","tags":["credentials","id","license","security"],"emoji":"🪪","order":4125,"group":7,"version":14},{"shortcodes":["atm"],"annotation":"ATM sign","tags":["atm","atm sign","automated","bank","teller"],"emoji":"🏧","order":4126,"group":8,"version":0.6},{"shortcodes":["litter_bin","put_litter_in_its_place"],"annotation":"litter in bin sign","tags":["litter","litter bin"],"emoji":"🚮","order":4127,"group":8,"version":1},{"shortcodes":["potable_water"],"annotation":"potable water","tags":["drinking","potable","water"],"emoji":"🚰","order":4128,"group":8,"version":1},{"shortcodes":["handicapped","wheelchair"],"annotation":"wheelchair symbol","tags":["access"],"emoji":"♿️","order":4129,"group":8,"version":0.6},{"shortcodes":["mens"],"annotation":"men’s room","tags":["bathroom","lavatory","man","restroom","toilet","wc"],"emoji":"🚹️","order":4130,"group":8,"version":0.6},{"shortcodes":["womens"],"annotation":"women’s room","tags":["bathroom","lavatory","restroom","toilet","wc","woman"],"emoji":"🚺️","order":4131,"group":8,"version":0.6},{"shortcodes":["bathroom","restroom"],"annotation":"restroom","tags":["bathroom","lavatory","toilet","wc"],"emoji":"🚻","order":4132,"group":8,"version":0.6},{"shortcodes":["baby_symbol"],"annotation":"baby symbol","tags":["baby","changing"],"emoji":"🚼️","order":4133,"group":8,"version":0.6},{"shortcodes":["water_closet","wc"],"annotation":"water closet","tags":["bathroom","closet","lavatory","restroom","toilet","water","wc"],"emoji":"🚾","order":4134,"group":8,"version":0.6},{"shortcodes":["passport_control"],"annotation":"passport control","tags":["control","passport"],"emoji":"🛂","order":4135,"group":8,"version":1},{"shortcodes":["customs"],"annotation":"customs","tags":["customs"],"emoji":"🛃","order":4136,"group":8,"version":1},{"shortcodes":["baggage_claim"],"annotation":"baggage claim","tags":["baggage","claim"],"emoji":"🛄","order":4137,"group":8,"version":1},{"shortcodes":["left_luggage"],"annotation":"left luggage","tags":["baggage","locker","luggage"],"emoji":"🛅","order":4138,"group":8,"version":1},{"shortcodes":["warning"],"annotation":"warning","tags":["warning"],"emoji":"⚠️","order":4140,"group":8,"version":0.6},{"shortcodes":["children_crossing"],"annotation":"children crossing","tags":["child","crossing","pedestrian","traffic"],"emoji":"🚸","order":4141,"group":8,"version":1},{"shortcodes":["no_entry"],"annotation":"no entry","tags":["entry","forbidden","no","not","prohibited","traffic"],"emoji":"⛔️","order":4142,"group":8,"version":0.6},{"shortcodes":["no_entry_sign"],"annotation":"prohibited","tags":["entry","forbidden","no","not"],"emoji":"🚫","order":4143,"group":8,"version":0.6},{"shortcodes":["no_bicycles"],"annotation":"no bicycles","tags":["bicycle","bike","forbidden","no","prohibited"],"emoji":"🚳","order":4144,"group":8,"version":1},{"shortcodes":["no_smoking"],"annotation":"no smoking","tags":["forbidden","no","not","prohibited","smoking"],"emoji":"🚭️","order":4145,"group":8,"version":0.6},{"shortcodes":["do_not_litter","no_littering"],"annotation":"no littering","tags":["forbidden","litter","no","not","prohibited"],"emoji":"🚯","order":4146,"group":8,"version":1},{"shortcodes":["non-potable_water"],"annotation":"non-potable water","tags":["non-drinking","non-potable","water"],"emoji":"🚱","order":4147,"group":8,"version":1},{"shortcodes":["no_pedestrians"],"annotation":"no pedestrians","tags":["forbidden","no","not","pedestrian","prohibited"],"emoji":"🚷","order":4148,"group":8,"version":1},{"shortcodes":["no_mobile_phones"],"annotation":"no mobile phones","tags":["cell","forbidden","mobile","no","phone"],"emoji":"📵","order":4149,"group":8,"version":1},{"shortcodes":["no_one_under_18","underage"],"annotation":"no one under eighteen","tags":["18","age restriction","eighteen","prohibited","underage"],"emoji":"🔞","order":4150,"group":8,"version":0.6},{"shortcodes":["radioactive"],"annotation":"radioactive","tags":["sign"],"emoji":"☢️","order":4152,"group":8,"version":1},{"shortcodes":["biohazard"],"annotation":"biohazard","tags":["sign"],"emoji":"☣️","order":4154,"group":8,"version":1},{"shortcodes":["arrow_up"],"annotation":"up arrow","tags":["arrow","cardinal","direction","north"],"emoji":"⬆️","order":4156,"group":8,"version":0.6},{"shortcodes":["arrow_upper_right"],"annotation":"up-right arrow","tags":["arrow","direction","intercardinal","northeast"],"emoji":"↗️","order":4158,"group":8,"version":0.6},{"shortcodes":["arrow_right"],"annotation":"right arrow","tags":["arrow","cardinal","direction","east"],"emoji":"➡️","order":4160,"group":8,"version":0.6},{"shortcodes":["arrow_lower_right"],"annotation":"down-right arrow","tags":["arrow","direction","intercardinal","southeast"],"emoji":"↘️","order":4162,"group":8,"version":0.6},{"shortcodes":["arrow_down"],"annotation":"down arrow","tags":["arrow","cardinal","direction","down","south"],"emoji":"⬇️","order":4164,"group":8,"version":0.6},{"shortcodes":["arrow_lower_left"],"annotation":"down-left arrow","tags":["arrow","direction","intercardinal","southwest"],"emoji":"↙️","order":4166,"group":8,"version":0.6},{"shortcodes":["arrow_left"],"annotation":"left arrow","tags":["arrow","cardinal","direction","west"],"emoji":"⬅️","order":4168,"group":8,"version":0.6},{"shortcodes":["arrow_upper_left"],"annotation":"up-left arrow","tags":["arrow","direction","intercardinal","northwest"],"emoji":"↖️","order":4170,"group":8,"version":0.6},{"shortcodes":["arrow_up_down"],"annotation":"up-down arrow","tags":["arrow"],"emoji":"↕️","order":4172,"group":8,"version":0.6},{"shortcodes":["left_right_arrow"],"annotation":"left-right arrow","tags":["arrow"],"emoji":"↔️","order":4174,"group":8,"version":0.6},{"shortcodes":["arrow_left_hook","leftwards_arrow_with_hook"],"annotation":"right arrow curving left","tags":["arrow"],"emoji":"↩️","order":4176,"group":8,"version":0.6},{"shortcodes":["arrow_right_hook","rightwards_arrow_with_hook"],"annotation":"left arrow curving right","tags":["arrow"],"emoji":"↪️","order":4178,"group":8,"version":0.6},{"shortcodes":["arrow_heading_up"],"annotation":"right arrow curving up","tags":["arrow"],"emoji":"⤴️","order":4180,"group":8,"version":0.6},{"shortcodes":["arrow_heading_down"],"annotation":"right arrow curving down","tags":["arrow","down"],"emoji":"⤵️","order":4182,"group":8,"version":0.6},{"shortcodes":["arrows_clockwise","clockwise"],"annotation":"clockwise vertical arrows","tags":["arrow","clockwise","reload"],"emoji":"🔃","order":4183,"group":8,"version":0.6},{"shortcodes":["arrows_counterclockwise","counterclockwise"],"annotation":"counterclockwise arrows button","tags":["anticlockwise","arrow","counterclockwise","withershins"],"emoji":"🔄","order":4184,"group":8,"version":1},{"shortcodes":["back"],"annotation":"BACK arrow","tags":["arrow","back"],"emoji":"🔙","order":4185,"group":8,"version":0.6},{"shortcodes":["end"],"annotation":"END arrow","tags":["arrow","end"],"emoji":"🔚","order":4186,"group":8,"version":0.6},{"shortcodes":["on"],"annotation":"ON! arrow","tags":["arrow","mark","on","on!"],"emoji":"🔛","order":4187,"group":8,"version":0.6},{"shortcodes":["soon"],"annotation":"SOON arrow","tags":["arrow","soon"],"emoji":"🔜","order":4188,"group":8,"version":0.6},{"shortcodes":["top"],"annotation":"TOP arrow","tags":["arrow","top","up"],"emoji":"🔝","order":4189,"group":8,"version":0.6},{"shortcodes":["place_of_worship"],"annotation":"place of worship","tags":["religion","worship"],"emoji":"🛐","order":4190,"group":8,"version":1},{"shortcodes":["atom","atom_symbol"],"annotation":"atom symbol","tags":["atheist","atom"],"emoji":"⚛️","order":4192,"group":8,"version":1},{"shortcodes":["om"],"annotation":"om","tags":["hindu","religion"],"emoji":"🕉️","order":4194,"group":8,"version":0.7},{"shortcodes":["star_of_david"],"annotation":"star of David","tags":["david","jew","jewish","religion","star","star of david"],"emoji":"✡️","order":4196,"group":8,"version":0.7},{"shortcodes":["wheel_of_dharma"],"annotation":"wheel of dharma","tags":["buddhist","dharma","religion","wheel"],"emoji":"☸️","order":4198,"group":8,"version":0.7},{"shortcodes":["yin_yang"],"annotation":"yin yang","tags":["religion","tao","taoist","yang","yin"],"emoji":"☯️","order":4200,"group":8,"version":0.7},{"shortcodes":["latin_cross"],"annotation":"latin cross","tags":["christian","cross","religion"],"emoji":"✝️","order":4202,"group":8,"version":0.7},{"shortcodes":["orthodox_cross"],"annotation":"orthodox cross","tags":["christian","cross","religion"],"emoji":"☦️","order":4204,"group":8,"version":1},{"shortcodes":["star_and_crescent"],"annotation":"star and crescent","tags":["islam","muslim","religion"],"emoji":"☪️","order":4206,"group":8,"version":0.7},{"shortcodes":["peace","peace_symbol"],"annotation":"peace symbol","tags":["peace"],"emoji":"☮️","order":4208,"group":8,"version":1},{"shortcodes":["menorah"],"annotation":"menorah","tags":["candelabrum","candlestick","religion"],"emoji":"🕎","order":4209,"group":8,"version":1},{"shortcodes":["six_pointed_star"],"annotation":"dotted six-pointed star","tags":["fortune","star"],"emoji":"🔯","order":4210,"group":8,"version":0.6},{"shortcodes":["aries"],"annotation":"Aries","tags":["aries","ram","zodiac"],"emoji":"♈️","order":4211,"group":8,"version":0.6},{"shortcodes":["taurus"],"annotation":"Taurus","tags":["bull","ox","taurus","zodiac"],"emoji":"♉️","order":4212,"group":8,"version":0.6},{"shortcodes":["gemini"],"annotation":"Gemini","tags":["gemini","twins","zodiac"],"emoji":"♊️","order":4213,"group":8,"version":0.6},{"shortcodes":["cancer"],"annotation":"Cancer","tags":["cancer","crab","zodiac"],"emoji":"♋️","order":4214,"group":8,"version":0.6},{"shortcodes":["leo"],"annotation":"Leo","tags":["leo","lion","zodiac"],"emoji":"♌️","order":4215,"group":8,"version":0.6},{"shortcodes":["virgo"],"annotation":"Virgo","tags":["virgo","zodiac"],"emoji":"♍️","order":4216,"group":8,"version":0.6},{"shortcodes":["libra"],"annotation":"Libra","tags":["balance","justice","libra","scales","zodiac"],"emoji":"♎️","order":4217,"group":8,"version":0.6},{"shortcodes":["scorpius"],"annotation":"Scorpio","tags":["scorpio","scorpion","scorpius","zodiac"],"emoji":"♏️","order":4218,"group":8,"version":0.6},{"shortcodes":["sagittarius"],"annotation":"Sagittarius","tags":["archer","sagittarius","zodiac"],"emoji":"♐️","order":4219,"group":8,"version":0.6},{"shortcodes":["capricorn"],"annotation":"Capricorn","tags":["capricorn","goat","zodiac"],"emoji":"♑️","order":4220,"group":8,"version":0.6},{"shortcodes":["aquarius"],"annotation":"Aquarius","tags":["aquarius","bearer","water","zodiac"],"emoji":"♒️","order":4221,"group":8,"version":0.6},{"shortcodes":["pisces"],"annotation":"Pisces","tags":["fish","pisces","zodiac"],"emoji":"♓️","order":4222,"group":8,"version":0.6},{"shortcodes":["ophiuchus"],"annotation":"Ophiuchus","tags":["bearer","ophiuchus","serpent","snake","zodiac"],"emoji":"⛎","order":4223,"group":8,"version":0.6},{"shortcodes":["shuffle","twisted_rightwards_arrows"],"annotation":"shuffle tracks button","tags":["arrow","crossed"],"emoji":"🔀","order":4224,"group":8,"version":1},{"shortcodes":["repeat"],"annotation":"repeat button","tags":["arrow","clockwise","repeat"],"emoji":"🔁","order":4225,"group":8,"version":1},{"shortcodes":["repeat_one"],"annotation":"repeat single button","tags":["arrow","clockwise","once"],"emoji":"🔂","order":4226,"group":8,"version":1},{"shortcodes":["arrow_forward","play"],"annotation":"play button","tags":["arrow","play","right","triangle"],"emoji":"▶️","order":4228,"group":8,"version":0.6},{"shortcodes":["fast_forward"],"annotation":"fast-forward button","tags":["arrow","double","fast","forward"],"emoji":"⏩️","order":4229,"group":8,"version":0.6},{"shortcodes":["next_track"],"annotation":"next track button","tags":["arrow","next scene","next track","triangle"],"emoji":"⏭️","order":4231,"group":8,"version":0.7},{"shortcodes":["play_pause"],"annotation":"play or pause button","tags":["arrow","pause","play","right","triangle"],"emoji":"⏯️","order":4233,"group":8,"version":1},{"shortcodes":["arrow_backward","reverse"],"annotation":"reverse button","tags":["arrow","left","reverse","triangle"],"emoji":"◀️","order":4235,"group":8,"version":0.6},{"shortcodes":["fast_reverse","rewind"],"annotation":"fast reverse button","tags":["arrow","double","rewind"],"emoji":"⏪️","order":4236,"group":8,"version":0.6},{"shortcodes":["previous_track"],"annotation":"last track button","tags":["arrow","previous scene","previous track","triangle"],"emoji":"⏮️","order":4238,"group":8,"version":0.7},{"shortcodes":["arrow_up_small","up"],"annotation":"upwards button","tags":["arrow","button","red"],"emoji":"🔼","order":4239,"group":8,"version":0.6},{"shortcodes":["arrow_double_up","fast_up"],"annotation":"fast up button","tags":["arrow","double"],"emoji":"⏫","order":4240,"group":8,"version":0.6},{"shortcodes":["arrow_down_small","down"],"annotation":"downwards button","tags":["arrow","button","down","red"],"emoji":"🔽","order":4241,"group":8,"version":0.6},{"shortcodes":["arrow_double_down","fast_down"],"annotation":"fast down button","tags":["arrow","double","down"],"emoji":"⏬","order":4242,"group":8,"version":0.6},{"shortcodes":["pause"],"annotation":"pause button","tags":["bar","double","pause","vertical"],"emoji":"⏸️","order":4244,"group":8,"version":0.7},{"shortcodes":["stop"],"annotation":"stop button","tags":["square","stop"],"emoji":"⏹️","order":4246,"group":8,"version":0.7},{"shortcodes":["record"],"annotation":"record button","tags":["circle","record"],"emoji":"⏺️","order":4248,"group":8,"version":0.7},{"shortcodes":["eject"],"annotation":"eject button","tags":["eject"],"emoji":"⏏️","order":4250,"group":8,"version":1},{"shortcodes":["cinema"],"annotation":"cinema","tags":["camera","film","movie"],"emoji":"🎦","order":4251,"group":8,"version":0.6},{"shortcodes":["dim_button","low_brightness"],"annotation":"dim button","tags":["brightness","dim","low"],"emoji":"🔅","order":4252,"group":8,"version":1},{"shortcodes":["bright_button","high_brightness"],"annotation":"bright button","tags":["bright","brightness"],"emoji":"🔆","order":4253,"group":8,"version":1},{"shortcodes":["antenna_bars","signal_strength"],"annotation":"antenna bars","tags":["antenna","bar","cell","mobile","phone"],"emoji":"📶","order":4254,"group":8,"version":0.6},{"shortcodes":["vibration_mode"],"annotation":"vibration mode","tags":["cell","mobile","mode","phone","telephone","vibration"],"emoji":"📳","order":4255,"group":8,"version":0.6},{"shortcodes":["mobile_phone_off"],"annotation":"mobile phone off","tags":["cell","mobile","off","phone","telephone"],"emoji":"📴","order":4256,"group":8,"version":0.6},{"shortcodes":["female","female_sign"],"annotation":"female sign","tags":["woman"],"emoji":"♀️","order":4258,"group":8,"version":4},{"shortcodes":["male","male_sign"],"annotation":"male sign","tags":["man"],"emoji":"♂️","order":4260,"group":8,"version":4},{"shortcodes":["transgender_symbol"],"annotation":"transgender symbol","tags":["transgender"],"emoji":"⚧️","order":4262,"group":8,"version":13},{"shortcodes":["multiplication","multiply"],"annotation":"multiply","tags":["cancel","multiplication","sign","x","×"],"emoji":"✖️","order":4264,"group":8,"version":0.6},{"shortcodes":["plus"],"annotation":"plus","tags":["+","math","sign"],"emoji":"➕","order":4265,"group":8,"version":0.6},{"shortcodes":["minus"],"annotation":"minus","tags":["-","math","sign","−"],"emoji":"➖","order":4266,"group":8,"version":0.6},{"shortcodes":["divide","division"],"annotation":"divide","tags":["division","math","sign","÷"],"emoji":"➗","order":4267,"group":8,"version":0.6},{"shortcodes":["heavy_equals_sign"],"annotation":"heavy equals sign","tags":["equality","math"],"emoji":"🟰","order":4268,"group":8,"version":14},{"shortcodes":["infinity"],"annotation":"infinity","tags":["forever","unbounded","universal"],"emoji":"♾️","order":4270,"group":8,"version":11},{"shortcodes":["bangbang","double_exclamation"],"annotation":"double exclamation mark","tags":["!","!!","bangbang","exclamation","mark"],"emoji":"‼️","order":4272,"group":8,"version":0.6},{"shortcodes":["exclamation_question","interrobang"],"annotation":"exclamation question mark","tags":["!","!?","?","exclamation","interrobang","mark","punctuation","question"],"emoji":"⁉️","order":4274,"group":8,"version":0.6},{"shortcodes":["question"],"annotation":"red question mark","tags":["?","mark","punctuation","question"],"emoji":"❓️","order":4275,"group":8,"version":0.6},{"shortcodes":["white_question"],"annotation":"white question mark","tags":["?","mark","outlined","punctuation","question"],"emoji":"❔","order":4276,"group":8,"version":0.6},{"shortcodes":["white_exclamation"],"annotation":"white exclamation mark","tags":["!","exclamation","mark","outlined","punctuation"],"emoji":"❕","order":4277,"group":8,"version":0.6},{"shortcodes":["exclamation"],"annotation":"red exclamation mark","tags":["!","exclamation","mark","punctuation"],"emoji":"❗️","order":4278,"group":8,"version":0.6},{"shortcodes":["wavy_dash"],"annotation":"wavy dash","tags":["dash","punctuation","wavy"],"emoji":"〰️","order":4280,"group":8,"version":0.6},{"shortcodes":["currency_exchange"],"annotation":"currency exchange","tags":["bank","currency","exchange","money"],"emoji":"💱","order":4281,"group":8,"version":0.6},{"shortcodes":["heavy_dollar_sign"],"annotation":"heavy dollar sign","tags":["currency","dollar","money"],"emoji":"💲","order":4282,"group":8,"version":0.6},{"shortcodes":["medical","medical_symbol"],"annotation":"medical symbol","tags":["aesculapius","medicine","staff"],"emoji":"⚕️","order":4284,"group":8,"version":4},{"shortcodes":["recycle","recycling_symbol"],"annotation":"recycling symbol","tags":["recycle"],"emoji":"♻️","order":4286,"group":8,"version":0.6},{"shortcodes":["fleur-de-lis"],"annotation":"fleur-de-lis","tags":["fleur-de-lis"],"emoji":"⚜️","order":4288,"group":8,"version":1},{"shortcodes":["trident"],"annotation":"trident emblem","tags":["anchor","emblem","ship","tool","trident"],"emoji":"🔱","order":4289,"group":8,"version":0.6},{"shortcodes":["name_badge"],"annotation":"name badge","tags":["badge","name"],"emoji":"📛","order":4290,"group":8,"version":0.6},{"shortcodes":["beginner"],"annotation":"Japanese symbol for beginner","tags":["beginner","chevron","japanese","japanese symbol for beginner","leaf"],"emoji":"🔰","order":4291,"group":8,"version":0.6},{"shortcodes":["hollow_red_circle","red_o"],"annotation":"hollow red circle","tags":["circle","large","o","red"],"emoji":"⭕️","order":4292,"group":8,"version":0.6},{"shortcodes":["check_mark_button","white_check_mark"],"annotation":"check mark button","tags":["button","check","mark","✓"],"emoji":"✅","order":4293,"group":8,"version":0.6},{"shortcodes":["ballot_box_with_check"],"annotation":"check box with check","tags":["box","check","✓"],"emoji":"☑️","order":4295,"group":8,"version":0.6},{"shortcodes":["check_mark","heavy_check_mark"],"annotation":"check mark","tags":["check","mark","✓"],"emoji":"✔️","order":4297,"group":8,"version":0.6},{"shortcodes":["cross_mark","x"],"annotation":"cross mark","tags":["cancel","cross","mark","multiplication","multiply","x","×"],"emoji":"❌","order":4298,"group":8,"version":0.6},{"shortcodes":["cross_mark_button","negative_squared_cross_mark"],"annotation":"cross mark button","tags":["mark","square","x","×"],"emoji":"❎","order":4299,"group":8,"version":0.6},{"shortcodes":["curly_loop"],"annotation":"curly loop","tags":["curl","loop"],"emoji":"➰","order":4300,"group":8,"version":0.6},{"shortcodes":["double_curly_loop","loop"],"annotation":"double curly loop","tags":["curl","double","loop"],"emoji":"➿","order":4301,"group":8,"version":1},{"shortcodes":["part_alternation_mark"],"annotation":"part alternation mark","tags":["mark","part"],"emoji":"〽️","order":4303,"group":8,"version":0.6},{"shortcodes":["eight_spoked_asterisk"],"annotation":"eight-spoked asterisk","tags":["*","asterisk"],"emoji":"✳️","order":4305,"group":8,"version":0.6},{"shortcodes":["eight_pointed_black_star"],"annotation":"eight-pointed star","tags":["*","star"],"emoji":"✴️","order":4307,"group":8,"version":0.6},{"shortcodes":["sparkle"],"annotation":"sparkle","tags":["*"],"emoji":"❇️","order":4309,"group":8,"version":0.6},{"shortcodes":["copyright"],"annotation":"copyright","tags":["c"],"emoji":"©️","order":4311,"group":8,"version":0.6},{"shortcodes":["registered"],"annotation":"registered","tags":["r"],"emoji":"®️","order":4313,"group":8,"version":0.6},{"shortcodes":["tm","trade_mark"],"annotation":"trade mark","tags":["mark","tm","trademark"],"emoji":"™️","order":4315,"group":8,"version":0.6},{"shortcodes":["hash","number_sign"],"annotation":"keycap: #","tags":["keycap"],"emoji":"#️⃣","order":4316,"group":8,"version":0.6},{"shortcodes":["asterisk"],"annotation":"keycap: *","tags":["keycap"],"emoji":"*️⃣","order":4318,"group":8,"version":2},{"shortcodes":["zero"],"annotation":"keycap: 0","tags":["keycap"],"emoji":"0️⃣","order":4320,"group":8,"version":0.6},{"shortcodes":["one"],"annotation":"keycap: 1","tags":["keycap"],"emoji":"1️⃣","order":4322,"group":8,"version":0.6},{"shortcodes":["two"],"annotation":"keycap: 2","tags":["keycap"],"emoji":"2️⃣","order":4324,"group":8,"version":0.6},{"shortcodes":["three"],"annotation":"keycap: 3","tags":["keycap"],"emoji":"3️⃣","order":4326,"group":8,"version":0.6},{"shortcodes":["four"],"annotation":"keycap: 4","tags":["keycap"],"emoji":"4️⃣","order":4328,"group":8,"version":0.6},{"shortcodes":["five"],"annotation":"keycap: 5","tags":["keycap"],"emoji":"5️⃣","order":4330,"group":8,"version":0.6},{"shortcodes":["six"],"annotation":"keycap: 6","tags":["keycap"],"emoji":"6️⃣","order":4332,"group":8,"version":0.6},{"shortcodes":["seven"],"annotation":"keycap: 7","tags":["keycap"],"emoji":"7️⃣","order":4334,"group":8,"version":0.6},{"shortcodes":["eight"],"annotation":"keycap: 8","tags":["keycap"],"emoji":"8️⃣","order":4336,"group":8,"version":0.6},{"shortcodes":["nine"],"annotation":"keycap: 9","tags":["keycap"],"emoji":"9️⃣","order":4338,"group":8,"version":0.6},{"shortcodes":["ten"],"annotation":"keycap: 10","tags":["keycap"],"emoji":"🔟","order":4340,"group":8,"version":0.6},{"shortcodes":["capital_abcd"],"annotation":"input latin uppercase","tags":["abcd","input","latin","letters","uppercase"],"emoji":"🔠","order":4341,"group":8,"version":0.6},{"shortcodes":["abcd"],"annotation":"input latin lowercase","tags":["abcd","input","latin","letters","lowercase"],"emoji":"🔡","order":4342,"group":8,"version":0.6},{"shortcodes":["1234"],"annotation":"input numbers","tags":["1234","input","numbers"],"emoji":"🔢","order":4343,"group":8,"version":0.6},{"shortcodes":["symbols"],"annotation":"input symbols","tags":["input","〒♪&%"],"emoji":"🔣","order":4344,"group":8,"version":0.6},{"shortcodes":["abc"],"annotation":"input latin letters","tags":["abc","alphabet","input","latin","letters"],"emoji":"🔤","order":4345,"group":8,"version":0.6},{"shortcodes":["a","a_blood"],"annotation":"A button (blood type)","tags":["a","a button (blood type)","blood type"],"emoji":"🅰️","order":4347,"group":8,"version":0.6},{"shortcodes":["ab","ab_blood"],"annotation":"AB button (blood type)","tags":["ab","ab button (blood type)","blood type"],"emoji":"🆎","order":4348,"group":8,"version":0.6},{"shortcodes":["b","b_blood"],"annotation":"B button (blood type)","tags":["b","b button (blood type)","blood type"],"emoji":"🅱️","order":4350,"group":8,"version":0.6},{"shortcodes":["cl"],"annotation":"CL button","tags":["cl","cl button"],"emoji":"🆑","order":4351,"group":8,"version":0.6},{"shortcodes":["cool"],"annotation":"COOL button","tags":["cool","cool button"],"emoji":"🆒","order":4352,"group":8,"version":0.6},{"shortcodes":["free"],"annotation":"FREE button","tags":["free","free button"],"emoji":"🆓","order":4353,"group":8,"version":0.6},{"shortcodes":["info","information_source"],"annotation":"information","tags":["i"],"emoji":"ℹ️","order":4355,"group":8,"version":0.6},{"shortcodes":["id"],"annotation":"ID button","tags":["id","id button","identity"],"emoji":"🆔","order":4356,"group":8,"version":0.6},{"shortcodes":["m"],"annotation":"circled M","tags":["circle","circled m","m"],"emoji":"Ⓜ️","order":4358,"group":8,"version":0.6},{"shortcodes":["new"],"annotation":"NEW button","tags":["new","new button"],"emoji":"🆕","order":4359,"group":8,"version":0.6},{"shortcodes":["ng"],"annotation":"NG button","tags":["ng","ng button"],"emoji":"🆖","order":4360,"group":8,"version":0.6},{"shortcodes":["o","o_blood"],"annotation":"O button (blood type)","tags":["blood type","o","o button (blood type)"],"emoji":"🅾️","order":4362,"group":8,"version":0.6},{"shortcodes":["ok"],"annotation":"OK button","tags":["ok","ok button"],"emoji":"🆗","order":4363,"group":8,"version":0.6},{"shortcodes":["parking"],"annotation":"P button","tags":["p","p button","parking"],"emoji":"🅿️","order":4365,"group":8,"version":0.6},{"shortcodes":["sos"],"annotation":"SOS button","tags":["help","sos","sos button"],"emoji":"🆘","order":4366,"group":8,"version":0.6},{"shortcodes":["up2"],"annotation":"UP! button","tags":["mark","up","up!","up! button"],"emoji":"🆙","order":4367,"group":8,"version":0.6},{"shortcodes":["vs"],"annotation":"VS button","tags":["versus","vs","vs button"],"emoji":"🆚","order":4368,"group":8,"version":0.6},{"shortcodes":["ja_here","koko"],"annotation":"Japanese “here” button","tags":["japanese","japanese “here” button","katakana","“here”","ココ"],"emoji":"🈁","order":4369,"group":8,"version":0.6},{"shortcodes":["ja_service_charge"],"annotation":"Japanese “service charge” button","tags":["japanese","japanese “service charge” button","katakana","“service charge”","サ"],"emoji":"🈂️","order":4371,"group":8,"version":0.6},{"shortcodes":["ja_monthly_amount"],"annotation":"Japanese “monthly amount” button","tags":["ideograph","japanese","japanese “monthly amount” button","“monthly amount”","月"],"emoji":"🈷️","order":4373,"group":8,"version":0.6},{"shortcodes":["ja_not_free_of_carge"],"annotation":"Japanese “not free of charge” button","tags":["ideograph","japanese","japanese “not free of charge” button","“not free of charge”","有"],"emoji":"🈶","order":4374,"group":8,"version":0.6},{"shortcodes":["ja_reserved"],"annotation":"Japanese “reserved” button","tags":["ideograph","japanese","japanese “reserved” button","“reserved”","指"],"emoji":"🈯️","order":4375,"group":8,"version":0.6},{"shortcodes":["ideograph_advantage","ja_bargain"],"annotation":"Japanese “bargain” button","tags":["ideograph","japanese","japanese “bargain” button","“bargain”","得"],"emoji":"🉐","order":4376,"group":8,"version":0.6},{"shortcodes":["ja_discount"],"annotation":"Japanese “discount” button","tags":["ideograph","japanese","japanese “discount” button","“discount”","割"],"emoji":"🈹","order":4377,"group":8,"version":0.6},{"shortcodes":["ja_free_of_charge"],"annotation":"Japanese “free of charge” button","tags":["ideograph","japanese","japanese “free of charge” button","“free of charge”","無"],"emoji":"🈚️","order":4378,"group":8,"version":0.6},{"shortcodes":["ja_prohibited"],"annotation":"Japanese “prohibited” button","tags":["ideograph","japanese","japanese “prohibited” button","“prohibited”","禁"],"emoji":"🈲","order":4379,"group":8,"version":0.6},{"shortcodes":["accept","ja_acceptable"],"annotation":"Japanese “acceptable” button","tags":["ideograph","japanese","japanese “acceptable” button","“acceptable”","可"],"emoji":"🉑","order":4380,"group":8,"version":0.6},{"shortcodes":["ja_application"],"annotation":"Japanese “application” button","tags":["ideograph","japanese","japanese “application” button","“application”","申"],"emoji":"🈸","order":4381,"group":8,"version":0.6},{"shortcodes":["ja_passing_grade"],"annotation":"Japanese “passing grade” button","tags":["ideograph","japanese","japanese “passing grade” button","“passing grade”","合"],"emoji":"🈴","order":4382,"group":8,"version":0.6},{"shortcodes":["ja_vacancy"],"annotation":"Japanese “vacancy” button","tags":["ideograph","japanese","japanese “vacancy” button","“vacancy”","空"],"emoji":"🈳","order":4383,"group":8,"version":0.6},{"shortcodes":["congratulations","ja_congratulations"],"annotation":"Japanese “congratulations” button","tags":["ideograph","japanese","japanese “congratulations” button","“congratulations”","祝"],"emoji":"㊗️","order":4385,"group":8,"version":0.6},{"shortcodes":["ja_secret","secret"],"annotation":"Japanese “secret” button","tags":["ideograph","japanese","japanese “secret” button","“secret”","秘"],"emoji":"㊙️","order":4387,"group":8,"version":0.6},{"shortcodes":["ja_open_for_business"],"annotation":"Japanese “open for business” button","tags":["ideograph","japanese","japanese “open for business” button","“open for business”","営"],"emoji":"🈺","order":4388,"group":8,"version":0.6},{"shortcodes":["ja_no_vacancy"],"annotation":"Japanese “no vacancy” button","tags":["ideograph","japanese","japanese “no vacancy” button","“no vacancy”","満"],"emoji":"🈵","order":4389,"group":8,"version":0.6},{"shortcodes":["red_circle"],"annotation":"red circle","tags":["circle","geometric","red"],"emoji":"🔴","order":4390,"group":8,"version":0.6},{"shortcodes":["orange_circle"],"annotation":"orange circle","tags":["circle","orange"],"emoji":"🟠","order":4391,"group":8,"version":12},{"shortcodes":["yellow_circle"],"annotation":"yellow circle","tags":["circle","yellow"],"emoji":"🟡","order":4392,"group":8,"version":12},{"shortcodes":["green_circle"],"annotation":"green circle","tags":["circle","green"],"emoji":"🟢","order":4393,"group":8,"version":12},{"shortcodes":["blue_circle"],"annotation":"blue circle","tags":["blue","circle","geometric"],"emoji":"🔵","order":4394,"group":8,"version":0.6},{"shortcodes":["purple_circle"],"annotation":"purple circle","tags":["circle","purple"],"emoji":"🟣","order":4395,"group":8,"version":12},{"shortcodes":["brown_circle"],"annotation":"brown circle","tags":["brown","circle"],"emoji":"🟤","order":4396,"group":8,"version":12},{"shortcodes":["black_circle"],"annotation":"black circle","tags":["circle","geometric"],"emoji":"⚫️","order":4397,"group":8,"version":0.6},{"shortcodes":["white_circle"],"annotation":"white circle","tags":["circle","geometric"],"emoji":"⚪️","order":4398,"group":8,"version":0.6},{"shortcodes":["red_square"],"annotation":"red square","tags":["red","square"],"emoji":"🟥","order":4399,"group":8,"version":12},{"shortcodes":["orange_square"],"annotation":"orange square","tags":["orange","square"],"emoji":"🟧","order":4400,"group":8,"version":12},{"shortcodes":["yellow_square"],"annotation":"yellow square","tags":["square","yellow"],"emoji":"🟨","order":4401,"group":8,"version":12},{"shortcodes":["green_square"],"annotation":"green square","tags":["green","square"],"emoji":"🟩","order":4402,"group":8,"version":12},{"shortcodes":["blue_square"],"annotation":"blue square","tags":["blue","square"],"emoji":"🟦","order":4403,"group":8,"version":12},{"shortcodes":["purple_square"],"annotation":"purple square","tags":["purple","square"],"emoji":"🟪","order":4404,"group":8,"version":12},{"shortcodes":["brown_square"],"annotation":"brown square","tags":["brown","square"],"emoji":"🟫","order":4405,"group":8,"version":12},{"shortcodes":["black_large_square"],"annotation":"black large square","tags":["geometric","square"],"emoji":"⬛️","order":4406,"group":8,"version":0.6},{"shortcodes":["white_large_square"],"annotation":"white large square","tags":["geometric","square"],"emoji":"⬜️","order":4407,"group":8,"version":0.6},{"shortcodes":["black_medium_square"],"annotation":"black medium square","tags":["geometric","square"],"emoji":"◼️","order":4409,"group":8,"version":0.6},{"shortcodes":["white_medium_square"],"annotation":"white medium square","tags":["geometric","square"],"emoji":"◻️","order":4411,"group":8,"version":0.6},{"shortcodes":["black_medium_small_square"],"annotation":"black medium-small square","tags":["geometric","square"],"emoji":"◾️","order":4412,"group":8,"version":0.6},{"shortcodes":["white_medium_small_square"],"annotation":"white medium-small square","tags":["geometric","square"],"emoji":"◽️","order":4413,"group":8,"version":0.6},{"shortcodes":["black_small_square"],"annotation":"black small square","tags":["geometric","square"],"emoji":"▪️","order":4415,"group":8,"version":0.6},{"shortcodes":["white_small_square"],"annotation":"white small square","tags":["geometric","square"],"emoji":"▫️","order":4417,"group":8,"version":0.6},{"shortcodes":["large_orange_diamond"],"annotation":"large orange diamond","tags":["diamond","geometric","orange"],"emoji":"🔶","order":4418,"group":8,"version":0.6},{"shortcodes":["large_blue_diamond"],"annotation":"large blue diamond","tags":["blue","diamond","geometric"],"emoji":"🔷","order":4419,"group":8,"version":0.6},{"shortcodes":["small_orange_diamond"],"annotation":"small orange diamond","tags":["diamond","geometric","orange"],"emoji":"🔸","order":4420,"group":8,"version":0.6},{"shortcodes":["small_blue_diamond"],"annotation":"small blue diamond","tags":["blue","diamond","geometric"],"emoji":"🔹","order":4421,"group":8,"version":0.6},{"shortcodes":["small_red_triangle"],"annotation":"red triangle pointed up","tags":["geometric","red"],"emoji":"🔺","order":4422,"group":8,"version":0.6},{"shortcodes":["small_red_triangle_down"],"annotation":"red triangle pointed down","tags":["down","geometric","red"],"emoji":"🔻","order":4423,"group":8,"version":0.6},{"shortcodes":["diamond_shape_with_a_dot_inside","diamond_with_a_dot"],"annotation":"diamond with a dot","tags":["comic","diamond","geometric","inside"],"emoji":"💠","order":4424,"group":8,"version":0.6},{"shortcodes":["radio_button"],"annotation":"radio button","tags":["button","geometric","radio"],"emoji":"🔘","order":4425,"group":8,"version":0.6},{"shortcodes":["white_square_button"],"annotation":"white square button","tags":["button","geometric","outlined","square"],"emoji":"🔳","order":4426,"group":8,"version":0.6},{"shortcodes":["black_square_button"],"annotation":"black square button","tags":["button","geometric","square"],"emoji":"🔲","order":4427,"group":8,"version":0.6},{"shortcodes":["checkered_flag"],"annotation":"chequered flag","tags":["checkered","chequered","racing"],"emoji":"🏁","order":4428,"group":9,"version":0.6},{"shortcodes":["triangular_flag","triangular_flag_on_post"],"annotation":"triangular flag","tags":["post"],"emoji":"🚩","order":4429,"group":9,"version":0.6},{"shortcodes":["crossed_flags"],"annotation":"crossed flags","tags":["celebration","cross","crossed","japanese"],"emoji":"🎌","order":4430,"group":9,"version":0.6},{"shortcodes":["black_flag"],"annotation":"black flag","tags":["waving"],"emoji":"🏴","order":4431,"group":9,"version":1},{"shortcodes":["white_flag"],"annotation":"white flag","tags":["waving"],"emoji":"🏳️","order":4433,"group":9,"version":0.7},{"shortcodes":["rainbow_flag"],"annotation":"rainbow flag","tags":["pride","rainbow"],"emoji":"🏳️‍🌈","order":4434,"group":9,"version":4},{"shortcodes":["transgender_flag"],"annotation":"transgender flag","tags":["flag","light blue","pink","transgender","white"],"emoji":"🏳️‍⚧️","order":4436,"group":9,"version":13},{"shortcodes":["jolly_roger","pirate_flag"],"annotation":"pirate flag","tags":["jolly roger","pirate","plunder","treasure"],"emoji":"🏴‍☠️","order":4440,"group":9,"version":11},{"shortcodes":["ascension_island","flag_ac"],"annotation":"flag: Ascension Island","tags":["AC","flag"],"emoji":"🇦🇨","order":4442,"group":9,"version":2},{"shortcodes":["andorra","flag_ad"],"annotation":"flag: Andorra","tags":["AD","flag"],"emoji":"🇦🇩","order":4443,"group":9,"version":2},{"shortcodes":["flag_ae","united_arab_emirates"],"annotation":"flag: United Arab Emirates","tags":["AE","flag"],"emoji":"🇦🇪","order":4444,"group":9,"version":2},{"shortcodes":["afghanistan","flag_af"],"annotation":"flag: Afghanistan","tags":["AF","flag"],"emoji":"🇦🇫","order":4445,"group":9,"version":2},{"shortcodes":["antigua_barbuda","flag_ag"],"annotation":"flag: Antigua & Barbuda","tags":["AG","flag"],"emoji":"🇦🇬","order":4446,"group":9,"version":2},{"shortcodes":["anguilla","flag_ai"],"annotation":"flag: Anguilla","tags":["AI","flag"],"emoji":"🇦🇮","order":4447,"group":9,"version":2},{"shortcodes":["albania","flag_al"],"annotation":"flag: Albania","tags":["AL","flag"],"emoji":"🇦🇱","order":4448,"group":9,"version":2},{"shortcodes":["armenia","flag_am"],"annotation":"flag: Armenia","tags":["AM","flag"],"emoji":"🇦🇲","order":4449,"group":9,"version":2},{"shortcodes":["angola","flag_ao"],"annotation":"flag: Angola","tags":["AO","flag"],"emoji":"🇦🇴","order":4450,"group":9,"version":2},{"shortcodes":["antarctica","flag_aq"],"annotation":"flag: Antarctica","tags":["AQ","flag"],"emoji":"🇦🇶","order":4451,"group":9,"version":2},{"shortcodes":["argentina","flag_ar"],"annotation":"flag: Argentina","tags":["AR","flag"],"emoji":"🇦🇷","order":4452,"group":9,"version":2},{"shortcodes":["american_samoa","flag_as"],"annotation":"flag: American Samoa","tags":["AS","flag"],"emoji":"🇦🇸","order":4453,"group":9,"version":2},{"shortcodes":["austria","flag_at"],"annotation":"flag: Austria","tags":["AT","flag"],"emoji":"🇦🇹","order":4454,"group":9,"version":2},{"shortcodes":["australia","flag_au"],"annotation":"flag: Australia","tags":["AU","flag"],"emoji":"🇦🇺","order":4455,"group":9,"version":2},{"shortcodes":["aruba","flag_aw"],"annotation":"flag: Aruba","tags":["AW","flag"],"emoji":"🇦🇼","order":4456,"group":9,"version":2},{"shortcodes":["aland_islands","flag_ax"],"annotation":"flag: Åland Islands","tags":["AX","flag"],"emoji":"🇦🇽","order":4457,"group":9,"version":2},{"shortcodes":["azerbaijan","flag_az"],"annotation":"flag: Azerbaijan","tags":["AZ","flag"],"emoji":"🇦🇿","order":4458,"group":9,"version":2},{"shortcodes":["bosnia_herzegovina","flag_ba"],"annotation":"flag: Bosnia & Herzegovina","tags":["BA","flag"],"emoji":"🇧🇦","order":4459,"group":9,"version":2},{"shortcodes":["barbados","flag_bb"],"annotation":"flag: Barbados","tags":["BB","flag"],"emoji":"🇧🇧","order":4460,"group":9,"version":2},{"shortcodes":["bangladesh","flag_bd"],"annotation":"flag: Bangladesh","tags":["BD","flag"],"emoji":"🇧🇩","order":4461,"group":9,"version":2},{"shortcodes":["belgium","flag_be"],"annotation":"flag: Belgium","tags":["BE","flag"],"emoji":"🇧🇪","order":4462,"group":9,"version":2},{"shortcodes":["burkina_faso","flag_bf"],"annotation":"flag: Burkina Faso","tags":["BF","flag"],"emoji":"🇧🇫","order":4463,"group":9,"version":2},{"shortcodes":["bulgaria","flag_bg"],"annotation":"flag: Bulgaria","tags":["BG","flag"],"emoji":"🇧🇬","order":4464,"group":9,"version":2},{"shortcodes":["bahrain","flag_bh"],"annotation":"flag: Bahrain","tags":["BH","flag"],"emoji":"🇧🇭","order":4465,"group":9,"version":2},{"shortcodes":["burundi","flag_bi"],"annotation":"flag: Burundi","tags":["BI","flag"],"emoji":"🇧🇮","order":4466,"group":9,"version":2},{"shortcodes":["benin","flag_bj"],"annotation":"flag: Benin","tags":["BJ","flag"],"emoji":"🇧🇯","order":4467,"group":9,"version":2},{"shortcodes":["flag_bl","st_barthelemy"],"annotation":"flag: St. Barthélemy","tags":["BL","flag"],"emoji":"🇧🇱","order":4468,"group":9,"version":2},{"shortcodes":["bermuda","flag_bm"],"annotation":"flag: Bermuda","tags":["BM","flag"],"emoji":"🇧🇲","order":4469,"group":9,"version":2},{"shortcodes":["brunei","flag_bn"],"annotation":"flag: Brunei","tags":["BN","flag"],"emoji":"🇧🇳","order":4470,"group":9,"version":2},{"shortcodes":["bolivia","flag_bo"],"annotation":"flag: Bolivia","tags":["BO","flag"],"emoji":"🇧🇴","order":4471,"group":9,"version":2},{"shortcodes":["caribbean_netherlands","flag_bq"],"annotation":"flag: Caribbean Netherlands","tags":["BQ","flag"],"emoji":"🇧🇶","order":4472,"group":9,"version":2},{"shortcodes":["brazil","flag_br"],"annotation":"flag: Brazil","tags":["BR","flag"],"emoji":"🇧🇷","order":4473,"group":9,"version":2},{"shortcodes":["bahamas","flag_bs"],"annotation":"flag: Bahamas","tags":["BS","flag"],"emoji":"🇧🇸","order":4474,"group":9,"version":2},{"shortcodes":["bhutan","flag_bt"],"annotation":"flag: Bhutan","tags":["BT","flag"],"emoji":"🇧🇹","order":4475,"group":9,"version":2},{"shortcodes":["bouvet_island","flag_bv"],"annotation":"flag: Bouvet Island","tags":["BV","flag"],"emoji":"🇧🇻","order":4476,"group":9,"version":2},{"shortcodes":["botswana","flag_bw"],"annotation":"flag: Botswana","tags":["BW","flag"],"emoji":"🇧🇼","order":4477,"group":9,"version":2},{"shortcodes":["belarus","flag_by"],"annotation":"flag: Belarus","tags":["BY","flag"],"emoji":"🇧🇾","order":4478,"group":9,"version":2},{"shortcodes":["belize","flag_bz"],"annotation":"flag: Belize","tags":["BZ","flag"],"emoji":"🇧🇿","order":4479,"group":9,"version":2},{"shortcodes":["canada","flag_ca"],"annotation":"flag: Canada","tags":["CA","flag"],"emoji":"🇨🇦","order":4480,"group":9,"version":2},{"shortcodes":["cocos_islands","flag_cc"],"annotation":"flag: Cocos (Keeling) Islands","tags":["CC","flag"],"emoji":"🇨🇨","order":4481,"group":9,"version":2},{"shortcodes":["congo_kinshasa","flag_cd"],"annotation":"flag: Congo - Kinshasa","tags":["CD","flag"],"emoji":"🇨🇩","order":4482,"group":9,"version":2},{"shortcodes":["central_african_republic","flag_cf"],"annotation":"flag: Central African Republic","tags":["CF","flag"],"emoji":"🇨🇫","order":4483,"group":9,"version":2},{"shortcodes":["congo_brazzaville","flag_cg"],"annotation":"flag: Congo - Brazzaville","tags":["CG","flag"],"emoji":"🇨🇬","order":4484,"group":9,"version":2},{"shortcodes":["flag_ch","switzerland"],"annotation":"flag: Switzerland","tags":["CH","flag"],"emoji":"🇨🇭","order":4485,"group":9,"version":2},{"shortcodes":["cote_divoire","flag_ci"],"annotation":"flag: Côte d’Ivoire","tags":["CI","flag"],"emoji":"🇨🇮","order":4486,"group":9,"version":2},{"shortcodes":["cook_islands","flag_ck"],"annotation":"flag: Cook Islands","tags":["CK","flag"],"emoji":"🇨🇰","order":4487,"group":9,"version":2},{"shortcodes":["chile","flag_cl"],"annotation":"flag: Chile","tags":["CL","flag"],"emoji":"🇨🇱","order":4488,"group":9,"version":2},{"shortcodes":["cameroon","flag_cm"],"annotation":"flag: Cameroon","tags":["CM","flag"],"emoji":"🇨🇲","order":4489,"group":9,"version":2},{"shortcodes":["china","flag_cn"],"annotation":"flag: China","tags":["CN","flag"],"emoji":"🇨🇳","order":4490,"group":9,"version":0.6},{"shortcodes":["colombia","flag_co"],"annotation":"flag: Colombia","tags":["CO","flag"],"emoji":"🇨🇴","order":4491,"group":9,"version":2},{"shortcodes":["clipperton_island","flag_cp"],"annotation":"flag: Clipperton Island","tags":["CP","flag"],"emoji":"🇨🇵","order":4492,"group":9,"version":2},{"shortcodes":["costa_rica","flag_cr"],"annotation":"flag: Costa Rica","tags":["CR","flag"],"emoji":"🇨🇷","order":4493,"group":9,"version":2},{"shortcodes":["cuba","flag_cu"],"annotation":"flag: Cuba","tags":["CU","flag"],"emoji":"🇨🇺","order":4494,"group":9,"version":2},{"shortcodes":["cape_verde","flag_cv"],"annotation":"flag: Cape Verde","tags":["CV","flag"],"emoji":"🇨🇻","order":4495,"group":9,"version":2},{"shortcodes":["curacao","flag_cw"],"annotation":"flag: Curaçao","tags":["CW","flag"],"emoji":"🇨🇼","order":4496,"group":9,"version":2},{"shortcodes":["christmas_island","flag_cx"],"annotation":"flag: Christmas Island","tags":["CX","flag"],"emoji":"🇨🇽","order":4497,"group":9,"version":2},{"shortcodes":["cyprus","flag_cy"],"annotation":"flag: Cyprus","tags":["CY","flag"],"emoji":"🇨🇾","order":4498,"group":9,"version":2},{"shortcodes":["czech_republic","czechia","flag_cz"],"annotation":"flag: Czechia","tags":["CZ","flag"],"emoji":"🇨🇿","order":4499,"group":9,"version":2},{"shortcodes":["flag_de","germany"],"annotation":"flag: Germany","tags":["DE","flag"],"emoji":"🇩🇪","order":4500,"group":9,"version":0.6},{"shortcodes":["diego_garcia","flag_dg"],"annotation":"flag: Diego Garcia","tags":["DG","flag"],"emoji":"🇩🇬","order":4501,"group":9,"version":2},{"shortcodes":["djibouti","flag_dj"],"annotation":"flag: Djibouti","tags":["DJ","flag"],"emoji":"🇩🇯","order":4502,"group":9,"version":2},{"shortcodes":["denmark","flag_dk"],"annotation":"flag: Denmark","tags":["DK","flag"],"emoji":"🇩🇰","order":4503,"group":9,"version":2},{"shortcodes":["dominica","flag_dm"],"annotation":"flag: Dominica","tags":["DM","flag"],"emoji":"🇩🇲","order":4504,"group":9,"version":2},{"shortcodes":["dominican_republic","flag_do"],"annotation":"flag: Dominican Republic","tags":["DO","flag"],"emoji":"🇩🇴","order":4505,"group":9,"version":2},{"shortcodes":["algeria","flag_dz"],"annotation":"flag: Algeria","tags":["DZ","flag"],"emoji":"🇩🇿","order":4506,"group":9,"version":2},{"shortcodes":["ceuta_melilla","flag_ea"],"annotation":"flag: Ceuta & Melilla","tags":["EA","flag"],"emoji":"🇪🇦","order":4507,"group":9,"version":2},{"shortcodes":["ecuador","flag_ec"],"annotation":"flag: Ecuador","tags":["EC","flag"],"emoji":"🇪🇨","order":4508,"group":9,"version":2},{"shortcodes":["estonia","flag_ee"],"annotation":"flag: Estonia","tags":["EE","flag"],"emoji":"🇪🇪","order":4509,"group":9,"version":2},{"shortcodes":["egypt","flag_eg"],"annotation":"flag: Egypt","tags":["EG","flag"],"emoji":"🇪🇬","order":4510,"group":9,"version":2},{"shortcodes":["flag_eh","western_sahara"],"annotation":"flag: Western Sahara","tags":["EH","flag"],"emoji":"🇪🇭","order":4511,"group":9,"version":2},{"shortcodes":["eritrea","flag_er"],"annotation":"flag: Eritrea","tags":["ER","flag"],"emoji":"🇪🇷","order":4512,"group":9,"version":2},{"shortcodes":["flag_es","spain"],"annotation":"flag: Spain","tags":["ES","flag"],"emoji":"🇪🇸","order":4513,"group":9,"version":0.6},{"shortcodes":["ethiopia","flag_et"],"annotation":"flag: Ethiopia","tags":["ET","flag"],"emoji":"🇪🇹","order":4514,"group":9,"version":2},{"shortcodes":["european_union","flag_eu"],"annotation":"flag: European Union","tags":["EU","flag"],"emoji":"🇪🇺","order":4515,"group":9,"version":2},{"shortcodes":["finland","flag_fi"],"annotation":"flag: Finland","tags":["FI","flag"],"emoji":"🇫🇮","order":4516,"group":9,"version":2},{"shortcodes":["fiji","flag_fj"],"annotation":"flag: Fiji","tags":["FJ","flag"],"emoji":"🇫🇯","order":4517,"group":9,"version":2},{"shortcodes":["falkland_islands","flag_fk"],"annotation":"flag: Falkland Islands","tags":["FK","flag"],"emoji":"🇫🇰","order":4518,"group":9,"version":2},{"shortcodes":["flag_fm","micronesia"],"annotation":"flag: Micronesia","tags":["FM","flag"],"emoji":"🇫🇲","order":4519,"group":9,"version":2},{"shortcodes":["faroe_islands","flag_fo"],"annotation":"flag: Faroe Islands","tags":["FO","flag"],"emoji":"🇫🇴","order":4520,"group":9,"version":2},{"shortcodes":["flag_fr","france"],"annotation":"flag: France","tags":["FR","flag"],"emoji":"🇫🇷","order":4521,"group":9,"version":0.6},{"shortcodes":["flag_ga","gabon"],"annotation":"flag: Gabon","tags":["GA","flag"],"emoji":"🇬🇦","order":4522,"group":9,"version":2},{"shortcodes":["flag_gb","uk","united_kingdom"],"annotation":"flag: United Kingdom","tags":["GB","flag"],"emoji":"🇬🇧","order":4523,"group":9,"version":0.6},{"shortcodes":["flag_gd","grenada"],"annotation":"flag: Grenada","tags":["GD","flag"],"emoji":"🇬🇩","order":4524,"group":9,"version":2},{"shortcodes":["flag_ge","georgia"],"annotation":"flag: Georgia","tags":["GE","flag"],"emoji":"🇬🇪","order":4525,"group":9,"version":2},{"shortcodes":["flag_gf","french_guiana"],"annotation":"flag: French Guiana","tags":["GF","flag"],"emoji":"🇬🇫","order":4526,"group":9,"version":2},{"shortcodes":["flag_gg","guernsey"],"annotation":"flag: Guernsey","tags":["GG","flag"],"emoji":"🇬🇬","order":4527,"group":9,"version":2},{"shortcodes":["flag_gh","ghana"],"annotation":"flag: Ghana","tags":["GH","flag"],"emoji":"🇬🇭","order":4528,"group":9,"version":2},{"shortcodes":["flag_gi","gibraltar"],"annotation":"flag: Gibraltar","tags":["GI","flag"],"emoji":"🇬🇮","order":4529,"group":9,"version":2},{"shortcodes":["flag_gl","greenland"],"annotation":"flag: Greenland","tags":["GL","flag"],"emoji":"🇬🇱","order":4530,"group":9,"version":2},{"shortcodes":["flag_gm","gambia"],"annotation":"flag: Gambia","tags":["GM","flag"],"emoji":"🇬🇲","order":4531,"group":9,"version":2},{"shortcodes":["flag_gn","guinea"],"annotation":"flag: Guinea","tags":["GN","flag"],"emoji":"🇬🇳","order":4532,"group":9,"version":2},{"shortcodes":["flag_gp","guadeloupe"],"annotation":"flag: Guadeloupe","tags":["GP","flag"],"emoji":"🇬🇵","order":4533,"group":9,"version":2},{"shortcodes":["equatorial_guinea","flag_gq"],"annotation":"flag: Equatorial Guinea","tags":["GQ","flag"],"emoji":"🇬🇶","order":4534,"group":9,"version":2},{"shortcodes":["flag_gr","greece"],"annotation":"flag: Greece","tags":["GR","flag"],"emoji":"🇬🇷","order":4535,"group":9,"version":2},{"shortcodes":["flag_gs","south_georgia_south_sandwich_islands"],"annotation":"flag: South Georgia & South Sandwich Islands","tags":["GS","flag"],"emoji":"🇬🇸","order":4536,"group":9,"version":2},{"shortcodes":["flag_gt","guatemala"],"annotation":"flag: Guatemala","tags":["GT","flag"],"emoji":"🇬🇹","order":4537,"group":9,"version":2},{"shortcodes":["flag_gu","guam"],"annotation":"flag: Guam","tags":["GU","flag"],"emoji":"🇬🇺","order":4538,"group":9,"version":2},{"shortcodes":["flag_gw","guinea_bissau"],"annotation":"flag: Guinea-Bissau","tags":["GW","flag"],"emoji":"🇬🇼","order":4539,"group":9,"version":2},{"shortcodes":["flag_gy","guyana"],"annotation":"flag: Guyana","tags":["GY","flag"],"emoji":"🇬🇾","order":4540,"group":9,"version":2},{"shortcodes":["flag_hk","hong_kong"],"annotation":"flag: Hong Kong SAR China","tags":["HK","flag"],"emoji":"🇭🇰","order":4541,"group":9,"version":2},{"shortcodes":["flag_hm","heard_mcdonald_islands"],"annotation":"flag: Heard & McDonald Islands","tags":["HM","flag"],"emoji":"🇭🇲","order":4542,"group":9,"version":2},{"shortcodes":["flag_hn","honduras"],"annotation":"flag: Honduras","tags":["HN","flag"],"emoji":"🇭🇳","order":4543,"group":9,"version":2},{"shortcodes":["croatia","flag_hr"],"annotation":"flag: Croatia","tags":["HR","flag"],"emoji":"🇭🇷","order":4544,"group":9,"version":2},{"shortcodes":["flag_ht","haiti"],"annotation":"flag: Haiti","tags":["HT","flag"],"emoji":"🇭🇹","order":4545,"group":9,"version":2},{"shortcodes":["flag_hu","hungary"],"annotation":"flag: Hungary","tags":["HU","flag"],"emoji":"🇭🇺","order":4546,"group":9,"version":2},{"shortcodes":["canary_islands","flag_ic"],"annotation":"flag: Canary Islands","tags":["IC","flag"],"emoji":"🇮🇨","order":4547,"group":9,"version":2},{"shortcodes":["flag_id","indonesia"],"annotation":"flag: Indonesia","tags":["ID","flag"],"emoji":"🇮🇩","order":4548,"group":9,"version":2},{"shortcodes":["flag_ie","ireland"],"annotation":"flag: Ireland","tags":["IE","flag"],"emoji":"🇮🇪","order":4549,"group":9,"version":2},{"shortcodes":["flag_il","israel"],"annotation":"flag: Israel","tags":["IL","flag"],"emoji":"🇮🇱","order":4550,"group":9,"version":2},{"shortcodes":["flag_im","isle_of_man"],"annotation":"flag: Isle of Man","tags":["IM","flag"],"emoji":"🇮🇲","order":4551,"group":9,"version":2},{"shortcodes":["flag_in","india"],"annotation":"flag: India","tags":["IN","flag"],"emoji":"🇮🇳","order":4552,"group":9,"version":2},{"shortcodes":["british_indian_ocean_territory","flag_io"],"annotation":"flag: British Indian Ocean Territory","tags":["IO","flag"],"emoji":"🇮🇴","order":4553,"group":9,"version":2},{"shortcodes":["flag_iq","iraq"],"annotation":"flag: Iraq","tags":["IQ","flag"],"emoji":"🇮🇶","order":4554,"group":9,"version":2},{"shortcodes":["flag_ir","iran"],"annotation":"flag: Iran","tags":["IR","flag"],"emoji":"🇮🇷","order":4555,"group":9,"version":2},{"shortcodes":["flag_is","iceland"],"annotation":"flag: Iceland","tags":["IS","flag"],"emoji":"🇮🇸","order":4556,"group":9,"version":2},{"shortcodes":["flag_it","italy"],"annotation":"flag: Italy","tags":["IT","flag"],"emoji":"🇮🇹","order":4557,"group":9,"version":0.6},{"shortcodes":["flag_je","jersey"],"annotation":"flag: Jersey","tags":["JE","flag"],"emoji":"🇯🇪","order":4558,"group":9,"version":2},{"shortcodes":["flag_jm","jamaica"],"annotation":"flag: Jamaica","tags":["JM","flag"],"emoji":"🇯🇲","order":4559,"group":9,"version":2},{"shortcodes":["flag_jo","jordan"],"annotation":"flag: Jordan","tags":["JO","flag"],"emoji":"🇯🇴","order":4560,"group":9,"version":2},{"shortcodes":["flag_jp","japan"],"annotation":"flag: Japan","tags":["JP","flag"],"emoji":"🇯🇵","order":4561,"group":9,"version":0.6},{"shortcodes":["flag_ke","kenya"],"annotation":"flag: Kenya","tags":["KE","flag"],"emoji":"🇰🇪","order":4562,"group":9,"version":2},{"shortcodes":["flag_kg","kyrgyzstan"],"annotation":"flag: Kyrgyzstan","tags":["KG","flag"],"emoji":"🇰🇬","order":4563,"group":9,"version":2},{"shortcodes":["cambodia","flag_kh"],"annotation":"flag: Cambodia","tags":["KH","flag"],"emoji":"🇰🇭","order":4564,"group":9,"version":2},{"shortcodes":["flag_ki","kiribati"],"annotation":"flag: Kiribati","tags":["KI","flag"],"emoji":"🇰🇮","order":4565,"group":9,"version":2},{"shortcodes":["comoros","flag_km"],"annotation":"flag: Comoros","tags":["KM","flag"],"emoji":"🇰🇲","order":4566,"group":9,"version":2},{"shortcodes":["flag_kn","st_kitts_nevis"],"annotation":"flag: St. Kitts & Nevis","tags":["KN","flag"],"emoji":"🇰🇳","order":4567,"group":9,"version":2},{"shortcodes":["flag_kp","north_korea"],"annotation":"flag: North Korea","tags":["KP","flag"],"emoji":"🇰🇵","order":4568,"group":9,"version":2},{"shortcodes":["flag_kr","south_korea"],"annotation":"flag: South Korea","tags":["KR","flag"],"emoji":"🇰🇷","order":4569,"group":9,"version":0.6},{"shortcodes":["flag_kw","kuwait"],"annotation":"flag: Kuwait","tags":["KW","flag"],"emoji":"🇰🇼","order":4570,"group":9,"version":2},{"shortcodes":["cayman_islands","flag_ky"],"annotation":"flag: Cayman Islands","tags":["KY","flag"],"emoji":"🇰🇾","order":4571,"group":9,"version":2},{"shortcodes":["flag_kz","kazakhstan"],"annotation":"flag: Kazakhstan","tags":["KZ","flag"],"emoji":"🇰🇿","order":4572,"group":9,"version":2},{"shortcodes":["flag_la","laos"],"annotation":"flag: Laos","tags":["LA","flag"],"emoji":"🇱🇦","order":4573,"group":9,"version":2},{"shortcodes":["flag_lb","lebanon"],"annotation":"flag: Lebanon","tags":["LB","flag"],"emoji":"🇱🇧","order":4574,"group":9,"version":2},{"shortcodes":["flag_lc","st_lucia"],"annotation":"flag: St. Lucia","tags":["LC","flag"],"emoji":"🇱🇨","order":4575,"group":9,"version":2},{"shortcodes":["flag_li","liechtenstein"],"annotation":"flag: Liechtenstein","tags":["LI","flag"],"emoji":"🇱🇮","order":4576,"group":9,"version":2},{"shortcodes":["flag_lk","sri_lanka"],"annotation":"flag: Sri Lanka","tags":["LK","flag"],"emoji":"🇱🇰","order":4577,"group":9,"version":2},{"shortcodes":["flag_lr","liberia"],"annotation":"flag: Liberia","tags":["LR","flag"],"emoji":"🇱🇷","order":4578,"group":9,"version":2},{"shortcodes":["flag_ls","lesotho"],"annotation":"flag: Lesotho","tags":["LS","flag"],"emoji":"🇱🇸","order":4579,"group":9,"version":2},{"shortcodes":["flag_lt","lithuania"],"annotation":"flag: Lithuania","tags":["LT","flag"],"emoji":"🇱🇹","order":4580,"group":9,"version":2},{"shortcodes":["flag_lu","luxembourg"],"annotation":"flag: Luxembourg","tags":["LU","flag"],"emoji":"🇱🇺","order":4581,"group":9,"version":2},{"shortcodes":["flag_lv","latvia"],"annotation":"flag: Latvia","tags":["LV","flag"],"emoji":"🇱🇻","order":4582,"group":9,"version":2},{"shortcodes":["flag_ly","libya"],"annotation":"flag: Libya","tags":["LY","flag"],"emoji":"🇱🇾","order":4583,"group":9,"version":2},{"shortcodes":["flag_ma","morocco"],"annotation":"flag: Morocco","tags":["MA","flag"],"emoji":"🇲🇦","order":4584,"group":9,"version":2},{"shortcodes":["flag_mc","monaco"],"annotation":"flag: Monaco","tags":["MC","flag"],"emoji":"🇲🇨","order":4585,"group":9,"version":2},{"shortcodes":["flag_md","moldova"],"annotation":"flag: Moldova","tags":["MD","flag"],"emoji":"🇲🇩","order":4586,"group":9,"version":2},{"shortcodes":["flag_me","montenegro"],"annotation":"flag: Montenegro","tags":["ME","flag"],"emoji":"🇲🇪","order":4587,"group":9,"version":2},{"shortcodes":["flag_mf","st_martin"],"annotation":"flag: St. Martin","tags":["MF","flag"],"emoji":"🇲🇫","order":4588,"group":9,"version":2},{"shortcodes":["flag_mg","madagascar"],"annotation":"flag: Madagascar","tags":["MG","flag"],"emoji":"🇲🇬","order":4589,"group":9,"version":2},{"shortcodes":["flag_mh","marshall_islands"],"annotation":"flag: Marshall Islands","tags":["MH","flag"],"emoji":"🇲🇭","order":4590,"group":9,"version":2},{"shortcodes":["flag_mk","macedonia"],"annotation":"flag: North Macedonia","tags":["MK","flag"],"emoji":"🇲🇰","order":4591,"group":9,"version":2},{"shortcodes":["flag_ml","mali"],"annotation":"flag: Mali","tags":["ML","flag"],"emoji":"🇲🇱","order":4592,"group":9,"version":2},{"shortcodes":["burma","flag_mm","myanmar"],"annotation":"flag: Myanmar (Burma)","tags":["MM","flag"],"emoji":"🇲🇲","order":4593,"group":9,"version":2},{"shortcodes":["flag_mn","mongolia"],"annotation":"flag: Mongolia","tags":["MN","flag"],"emoji":"🇲🇳","order":4594,"group":9,"version":2},{"shortcodes":["flag_mo","macao","macau"],"annotation":"flag: Macao SAR China","tags":["MO","flag"],"emoji":"🇲🇴","order":4595,"group":9,"version":2},{"shortcodes":["flag_mp","northern_mariana_islands"],"annotation":"flag: Northern Mariana Islands","tags":["MP","flag"],"emoji":"🇲🇵","order":4596,"group":9,"version":2},{"shortcodes":["flag_mq","martinique"],"annotation":"flag: Martinique","tags":["MQ","flag"],"emoji":"🇲🇶","order":4597,"group":9,"version":2},{"shortcodes":["flag_mr","mauritania"],"annotation":"flag: Mauritania","tags":["MR","flag"],"emoji":"🇲🇷","order":4598,"group":9,"version":2},{"shortcodes":["flag_ms","montserrat"],"annotation":"flag: Montserrat","tags":["MS","flag"],"emoji":"🇲🇸","order":4599,"group":9,"version":2},{"shortcodes":["flag_mt","malta"],"annotation":"flag: Malta","tags":["MT","flag"],"emoji":"🇲🇹","order":4600,"group":9,"version":2},{"shortcodes":["flag_mu","mauritius"],"annotation":"flag: Mauritius","tags":["MU","flag"],"emoji":"🇲🇺","order":4601,"group":9,"version":2},{"shortcodes":["flag_mv","maldives"],"annotation":"flag: Maldives","tags":["MV","flag"],"emoji":"🇲🇻","order":4602,"group":9,"version":2},{"shortcodes":["flag_mw","malawi"],"annotation":"flag: Malawi","tags":["MW","flag"],"emoji":"🇲🇼","order":4603,"group":9,"version":2},{"shortcodes":["flag_mx","mexico"],"annotation":"flag: Mexico","tags":["MX","flag"],"emoji":"🇲🇽","order":4604,"group":9,"version":2},{"shortcodes":["flag_my","malaysia"],"annotation":"flag: Malaysia","tags":["MY","flag"],"emoji":"🇲🇾","order":4605,"group":9,"version":2},{"shortcodes":["flag_mz","mozambique"],"annotation":"flag: Mozambique","tags":["MZ","flag"],"emoji":"🇲🇿","order":4606,"group":9,"version":2},{"shortcodes":["flag_na","namibia"],"annotation":"flag: Namibia","tags":["NA","flag"],"emoji":"🇳🇦","order":4607,"group":9,"version":2},{"shortcodes":["flag_nc","new_caledonia"],"annotation":"flag: New Caledonia","tags":["NC","flag"],"emoji":"🇳🇨","order":4608,"group":9,"version":2},{"shortcodes":["flag_ne","niger"],"annotation":"flag: Niger","tags":["NE","flag"],"emoji":"🇳🇪","order":4609,"group":9,"version":2},{"shortcodes":["flag_nf","norfolk_island"],"annotation":"flag: Norfolk Island","tags":["NF","flag"],"emoji":"🇳🇫","order":4610,"group":9,"version":2},{"shortcodes":["flag_ng","nigeria"],"annotation":"flag: Nigeria","tags":["NG","flag"],"emoji":"🇳🇬","order":4611,"group":9,"version":2},{"shortcodes":["flag_ni","nicaragua"],"annotation":"flag: Nicaragua","tags":["NI","flag"],"emoji":"🇳🇮","order":4612,"group":9,"version":2},{"shortcodes":["flag_nl","netherlands"],"annotation":"flag: Netherlands","tags":["NL","flag"],"emoji":"🇳🇱","order":4613,"group":9,"version":2},{"shortcodes":["flag_no","norway"],"annotation":"flag: Norway","tags":["NO","flag"],"emoji":"🇳🇴","order":4614,"group":9,"version":2},{"shortcodes":["flag_np","nepal"],"annotation":"flag: Nepal","tags":["NP","flag"],"emoji":"🇳🇵","order":4615,"group":9,"version":2},{"shortcodes":["flag_nr","nauru"],"annotation":"flag: Nauru","tags":["NR","flag"],"emoji":"🇳🇷","order":4616,"group":9,"version":2},{"shortcodes":["flag_nu","niue"],"annotation":"flag: Niue","tags":["NU","flag"],"emoji":"🇳🇺","order":4617,"group":9,"version":2},{"shortcodes":["flag_nz","new_zealand"],"annotation":"flag: New Zealand","tags":["NZ","flag"],"emoji":"🇳🇿","order":4618,"group":9,"version":2},{"shortcodes":["flag_om","oman"],"annotation":"flag: Oman","tags":["OM","flag"],"emoji":"🇴🇲","order":4619,"group":9,"version":2},{"shortcodes":["flag_pa","panama"],"annotation":"flag: Panama","tags":["PA","flag"],"emoji":"🇵🇦","order":4620,"group":9,"version":2},{"shortcodes":["flag_pe","peru"],"annotation":"flag: Peru","tags":["PE","flag"],"emoji":"🇵🇪","order":4621,"group":9,"version":2},{"shortcodes":["flag_pf","french_polynesia"],"annotation":"flag: French Polynesia","tags":["PF","flag"],"emoji":"🇵🇫","order":4622,"group":9,"version":2},{"shortcodes":["flag_pg","papua_new_guinea"],"annotation":"flag: Papua New Guinea","tags":["PG","flag"],"emoji":"🇵🇬","order":4623,"group":9,"version":2},{"shortcodes":["flag_ph","philippines"],"annotation":"flag: Philippines","tags":["PH","flag"],"emoji":"🇵🇭","order":4624,"group":9,"version":2},{"shortcodes":["flag_pk","pakistan"],"annotation":"flag: Pakistan","tags":["PK","flag"],"emoji":"🇵🇰","order":4625,"group":9,"version":2},{"shortcodes":["flag_pl","poland"],"annotation":"flag: Poland","tags":["PL","flag"],"emoji":"🇵🇱","order":4626,"group":9,"version":2},{"shortcodes":["flag_pm","st_pierre_miquelon"],"annotation":"flag: St. Pierre & Miquelon","tags":["PM","flag"],"emoji":"🇵🇲","order":4627,"group":9,"version":2},{"shortcodes":["flag_pn","pitcairn_islands"],"annotation":"flag: Pitcairn Islands","tags":["PN","flag"],"emoji":"🇵🇳","order":4628,"group":9,"version":2},{"shortcodes":["flag_pr","puerto_rico"],"annotation":"flag: Puerto Rico","tags":["PR","flag"],"emoji":"🇵🇷","order":4629,"group":9,"version":2},{"shortcodes":["flag_ps","palestinian_territories"],"annotation":"flag: Palestinian Territories","tags":["PS","flag"],"emoji":"🇵🇸","order":4630,"group":9,"version":2},{"shortcodes":["flag_pt","portugal"],"annotation":"flag: Portugal","tags":["PT","flag"],"emoji":"🇵🇹","order":4631,"group":9,"version":2},{"shortcodes":["flag_pw","palau"],"annotation":"flag: Palau","tags":["PW","flag"],"emoji":"🇵🇼","order":4632,"group":9,"version":2},{"shortcodes":["flag_py","paraguay"],"annotation":"flag: Paraguay","tags":["PY","flag"],"emoji":"🇵🇾","order":4633,"group":9,"version":2},{"shortcodes":["flag_qa","qatar"],"annotation":"flag: Qatar","tags":["QA","flag"],"emoji":"🇶🇦","order":4634,"group":9,"version":2},{"shortcodes":["flag_re","reunion"],"annotation":"flag: Réunion","tags":["RE","flag"],"emoji":"🇷🇪","order":4635,"group":9,"version":2},{"shortcodes":["flag_ro","romania"],"annotation":"flag: Romania","tags":["RO","flag"],"emoji":"🇷🇴","order":4636,"group":9,"version":2},{"shortcodes":["flag_rs","serbia"],"annotation":"flag: Serbia","tags":["RS","flag"],"emoji":"🇷🇸","order":4637,"group":9,"version":2},{"shortcodes":["flag_ru","russia"],"annotation":"flag: Russia","tags":["RU","flag"],"emoji":"🇷🇺","order":4638,"group":9,"version":0.6},{"shortcodes":["flag_rw","rwanda"],"annotation":"flag: Rwanda","tags":["RW","flag"],"emoji":"🇷🇼","order":4639,"group":9,"version":2},{"shortcodes":["flag_sa","saudi_arabia"],"annotation":"flag: Saudi Arabia","tags":["SA","flag"],"emoji":"🇸🇦","order":4640,"group":9,"version":2},{"shortcodes":["flag_sb","solomon_islands"],"annotation":"flag: Solomon Islands","tags":["SB","flag"],"emoji":"🇸🇧","order":4641,"group":9,"version":2},{"shortcodes":["flag_sc","seychelles"],"annotation":"flag: Seychelles","tags":["SC","flag"],"emoji":"🇸🇨","order":4642,"group":9,"version":2},{"shortcodes":["flag_sd","sudan"],"annotation":"flag: Sudan","tags":["SD","flag"],"emoji":"🇸🇩","order":4643,"group":9,"version":2},{"shortcodes":["flag_se","sweden"],"annotation":"flag: Sweden","tags":["SE","flag"],"emoji":"🇸🇪","order":4644,"group":9,"version":2},{"shortcodes":["flag_sg","singapore"],"annotation":"flag: Singapore","tags":["SG","flag"],"emoji":"🇸🇬","order":4645,"group":9,"version":2},{"shortcodes":["flag_sh","st_helena"],"annotation":"flag: St. Helena","tags":["SH","flag"],"emoji":"🇸🇭","order":4646,"group":9,"version":2},{"shortcodes":["flag_si","slovenia"],"annotation":"flag: Slovenia","tags":["SI","flag"],"emoji":"🇸🇮","order":4647,"group":9,"version":2},{"shortcodes":["flag_sj","svalbard_jan_mayen"],"annotation":"flag: Svalbard & Jan Mayen","tags":["SJ","flag"],"emoji":"🇸🇯","order":4648,"group":9,"version":2},{"shortcodes":["flag_sk","slovakia"],"annotation":"flag: Slovakia","tags":["SK","flag"],"emoji":"🇸🇰","order":4649,"group":9,"version":2},{"shortcodes":["flag_sl","sierra_leone"],"annotation":"flag: Sierra Leone","tags":["SL","flag"],"emoji":"🇸🇱","order":4650,"group":9,"version":2},{"shortcodes":["flag_sm","san_marino"],"annotation":"flag: San Marino","tags":["SM","flag"],"emoji":"🇸🇲","order":4651,"group":9,"version":2},{"shortcodes":["flag_sn","senegal"],"annotation":"flag: Senegal","tags":["SN","flag"],"emoji":"🇸🇳","order":4652,"group":9,"version":2},{"shortcodes":["flag_so","somalia"],"annotation":"flag: Somalia","tags":["SO","flag"],"emoji":"🇸🇴","order":4653,"group":9,"version":2},{"shortcodes":["flag_sr","suriname"],"annotation":"flag: Suriname","tags":["SR","flag"],"emoji":"🇸🇷","order":4654,"group":9,"version":2},{"shortcodes":["flag_ss","south_sudan"],"annotation":"flag: South Sudan","tags":["SS","flag"],"emoji":"🇸🇸","order":4655,"group":9,"version":2},{"shortcodes":["flag_st","sao_tome_principe"],"annotation":"flag: São Tomé & Príncipe","tags":["ST","flag"],"emoji":"🇸🇹","order":4656,"group":9,"version":2},{"shortcodes":["el_salvador","flag_sv"],"annotation":"flag: El Salvador","tags":["SV","flag"],"emoji":"🇸🇻","order":4657,"group":9,"version":2},{"shortcodes":["flag_sx","sint_maarten"],"annotation":"flag: Sint Maarten","tags":["SX","flag"],"emoji":"🇸🇽","order":4658,"group":9,"version":2},{"shortcodes":["flag_sy","syria"],"annotation":"flag: Syria","tags":["SY","flag"],"emoji":"🇸🇾","order":4659,"group":9,"version":2},{"shortcodes":["eswatini","flag_sz","swaziland"],"annotation":"flag: Eswatini","tags":["SZ","flag"],"emoji":"🇸🇿","order":4660,"group":9,"version":2},{"shortcodes":["flag_ta","tristan_da_cunha"],"annotation":"flag: Tristan da Cunha","tags":["TA","flag"],"emoji":"🇹🇦","order":4661,"group":9,"version":2},{"shortcodes":["flag_tc","turks_caicos_islands"],"annotation":"flag: Turks & Caicos Islands","tags":["TC","flag"],"emoji":"🇹🇨","order":4662,"group":9,"version":2},{"shortcodes":["chad","flag_td"],"annotation":"flag: Chad","tags":["TD","flag"],"emoji":"🇹🇩","order":4663,"group":9,"version":2},{"shortcodes":["flag_tf","french_southern_territories"],"annotation":"flag: French Southern Territories","tags":["TF","flag"],"emoji":"🇹🇫","order":4664,"group":9,"version":2},{"shortcodes":["flag_tg","togo"],"annotation":"flag: Togo","tags":["TG","flag"],"emoji":"🇹🇬","order":4665,"group":9,"version":2},{"shortcodes":["flag_th","thailand"],"annotation":"flag: Thailand","tags":["TH","flag"],"emoji":"🇹🇭","order":4666,"group":9,"version":2},{"shortcodes":["flag_tj","tajikistan"],"annotation":"flag: Tajikistan","tags":["TJ","flag"],"emoji":"🇹🇯","order":4667,"group":9,"version":2},{"shortcodes":["flag_tk","tokelau"],"annotation":"flag: Tokelau","tags":["TK","flag"],"emoji":"🇹🇰","order":4668,"group":9,"version":2},{"shortcodes":["flag_tl","timor_leste"],"annotation":"flag: Timor-Leste","tags":["TL","flag"],"emoji":"🇹🇱","order":4669,"group":9,"version":2},{"shortcodes":["flag_tm","turkmenistan"],"annotation":"flag: Turkmenistan","tags":["TM","flag"],"emoji":"🇹🇲","order":4670,"group":9,"version":2},{"shortcodes":["flag_tn","tunisia"],"annotation":"flag: Tunisia","tags":["TN","flag"],"emoji":"🇹🇳","order":4671,"group":9,"version":2},{"shortcodes":["flag_to","tonga"],"annotation":"flag: Tonga","tags":["TO","flag"],"emoji":"🇹🇴","order":4672,"group":9,"version":2},{"shortcodes":["flag_tr","turkey_tr"],"annotation":"flag: Turkey","tags":["TR","flag"],"emoji":"🇹🇷","order":4673,"group":9,"version":2},{"shortcodes":["flag_tt","trinidad_tobago"],"annotation":"flag: Trinidad & Tobago","tags":["TT","flag"],"emoji":"🇹🇹","order":4674,"group":9,"version":2},{"shortcodes":["flag_tv","tuvalu"],"annotation":"flag: Tuvalu","tags":["TV","flag"],"emoji":"🇹🇻","order":4675,"group":9,"version":2},{"shortcodes":["flag_tw","taiwan"],"annotation":"flag: Taiwan","tags":["TW","flag"],"emoji":"🇹🇼","order":4676,"group":9,"version":2},{"shortcodes":["flag_tz","tanzania"],"annotation":"flag: Tanzania","tags":["TZ","flag"],"emoji":"🇹🇿","order":4677,"group":9,"version":2},{"shortcodes":["flag_ua","ukraine"],"annotation":"flag: Ukraine","tags":["UA","flag"],"emoji":"🇺🇦","order":4678,"group":9,"version":2},{"shortcodes":["flag_ug","uganda"],"annotation":"flag: Uganda","tags":["UG","flag"],"emoji":"🇺🇬","order":4679,"group":9,"version":2},{"shortcodes":["flag_um","us_outlying_islands"],"annotation":"flag: U.S. Outlying Islands","tags":["UM","flag"],"emoji":"🇺🇲","order":4680,"group":9,"version":2},{"shortcodes":["flag_un","un","united_nations"],"annotation":"flag: United Nations","tags":["UN","flag"],"emoji":"🇺🇳","order":4681,"group":9,"version":4},{"shortcodes":["flag_us","united_states","usa"],"annotation":"flag: United States","tags":["US","flag"],"emoji":"🇺🇸","order":4682,"group":9,"version":0.6},{"shortcodes":["flag_uy","uruguay"],"annotation":"flag: Uruguay","tags":["UY","flag"],"emoji":"🇺🇾","order":4683,"group":9,"version":2},{"shortcodes":["flag_uz","uzbekistan"],"annotation":"flag: Uzbekistan","tags":["UZ","flag"],"emoji":"🇺🇿","order":4684,"group":9,"version":2},{"shortcodes":["flag_va","vatican_city"],"annotation":"flag: Vatican City","tags":["VA","flag"],"emoji":"🇻🇦","order":4685,"group":9,"version":2},{"shortcodes":["flag_vc","st_vincent_grenadines"],"annotation":"flag: St. Vincent & Grenadines","tags":["VC","flag"],"emoji":"🇻🇨","order":4686,"group":9,"version":2},{"shortcodes":["flag_ve","venezuela"],"annotation":"flag: Venezuela","tags":["VE","flag"],"emoji":"🇻🇪","order":4687,"group":9,"version":2},{"shortcodes":["british_virgin_islands","flag_vg"],"annotation":"flag: British Virgin Islands","tags":["VG","flag"],"emoji":"🇻🇬","order":4688,"group":9,"version":2},{"shortcodes":["flag_vi","us_virgin_islands"],"annotation":"flag: U.S. Virgin Islands","tags":["VI","flag"],"emoji":"🇻🇮","order":4689,"group":9,"version":2},{"shortcodes":["flag_vn","vietnam"],"annotation":"flag: Vietnam","tags":["VN","flag"],"emoji":"🇻🇳","order":4690,"group":9,"version":2},{"shortcodes":["flag_vu","vanuatu"],"annotation":"flag: Vanuatu","tags":["VU","flag"],"emoji":"🇻🇺","order":4691,"group":9,"version":2},{"shortcodes":["flag_wf","wallis_futuna"],"annotation":"flag: Wallis & Futuna","tags":["WF","flag"],"emoji":"🇼🇫","order":4692,"group":9,"version":2},{"shortcodes":["flag_ws","samoa"],"annotation":"flag: Samoa","tags":["WS","flag"],"emoji":"🇼🇸","order":4693,"group":9,"version":2},{"shortcodes":["flag_xk","kosovo"],"annotation":"flag: Kosovo","tags":["XK","flag"],"emoji":"🇽🇰","order":4694,"group":9,"version":2},{"shortcodes":["flag_ye","yemen"],"annotation":"flag: Yemen","tags":["YE","flag"],"emoji":"🇾🇪","order":4695,"group":9,"version":2},{"shortcodes":["flag_yt","mayotte"],"annotation":"flag: Mayotte","tags":["YT","flag"],"emoji":"🇾🇹","order":4696,"group":9,"version":2},{"shortcodes":["flag_za","south_africa"],"annotation":"flag: South Africa","tags":["ZA","flag"],"emoji":"🇿🇦","order":4697,"group":9,"version":2},{"shortcodes":["flag_zm","zambia"],"annotation":"flag: Zambia","tags":["ZM","flag"],"emoji":"🇿🇲","order":4698,"group":9,"version":2},{"shortcodes":["flag_zw","zimbabwe"],"annotation":"flag: Zimbabwe","tags":["ZW","flag"],"emoji":"🇿🇼","order":4699,"group":9,"version":2},{"shortcodes":["england","flag_gbeng"],"annotation":"flag: England","tags":["flag","gbeng"],"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","order":4700,"group":9,"version":5},{"shortcodes":["flag_gbsct","scotland"],"annotation":"flag: Scotland","tags":["flag","gbsct"],"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","order":4701,"group":9,"version":5},{"shortcodes":["flag_gbwls","wales"],"annotation":"flag: Wales","tags":["flag","gbwls"],"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","order":4702,"group":9,"version":5}] \ No newline at end of file diff --git a/app/static/scripts/emoji-picker-element/database.js b/app/static/scripts/emoji-picker-element/database.js new file mode 100644 index 0000000..ada5098 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/database.js @@ -0,0 +1,972 @@ +function assertNonEmptyString (str) { + if (typeof str !== 'string' || !str) { + throw new Error('expected a non-empty string, got: ' + str) + } +} + +function assertNumber (number) { + if (typeof number !== 'number') { + throw new Error('expected a number, got: ' + number) + } +} + +const DB_VERSION_CURRENT = 1; +const DB_VERSION_INITIAL = 1; +const STORE_EMOJI = 'emoji'; +const STORE_KEYVALUE = 'keyvalue'; +const STORE_FAVORITES = 'favorites'; +const FIELD_TOKENS = 'tokens'; +const INDEX_TOKENS = 'tokens'; +const FIELD_UNICODE = 'unicode'; +const INDEX_COUNT = 'count'; +const FIELD_GROUP = 'group'; +const FIELD_ORDER = 'order'; +const INDEX_GROUP_AND_ORDER = 'group-order'; +const KEY_ETAG = 'eTag'; +const KEY_URL = 'url'; +const KEY_PREFERRED_SKINTONE = 'skinTone'; +const MODE_READONLY = 'readonly'; +const MODE_READWRITE = 'readwrite'; +const INDEX_SKIN_UNICODE = 'skinUnicodes'; +const FIELD_SKIN_UNICODE = 'skinUnicodes'; + +const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json'; +const DEFAULT_LOCALE = 'en'; + +// like lodash's uniqBy but much smaller +function uniqBy (arr, func) { + const set = new Set(); + const res = []; + for (const item of arr) { + const key = func(item); + if (!set.has(key)) { + set.add(key); + res.push(item); + } + } + return res +} + +function uniqEmoji (emojis) { + return uniqBy(emojis, _ => _.unicode) +} + +function initialMigration (db) { + function createObjectStore (name, keyPath, indexes) { + const store = keyPath + ? db.createObjectStore(name, { keyPath }) + : db.createObjectStore(name); + if (indexes) { + for (const [indexName, [keyPath, multiEntry]] of Object.entries(indexes)) { + store.createIndex(indexName, keyPath, { multiEntry }); + } + } + return store + } + + createObjectStore(STORE_KEYVALUE); + createObjectStore(STORE_EMOJI, /* keyPath */ FIELD_UNICODE, { + [INDEX_TOKENS]: [FIELD_TOKENS, /* multiEntry */ true], + [INDEX_GROUP_AND_ORDER]: [[FIELD_GROUP, FIELD_ORDER]], + [INDEX_SKIN_UNICODE]: [FIELD_SKIN_UNICODE, /* multiEntry */ true] + }); + createObjectStore(STORE_FAVORITES, undefined, { + [INDEX_COUNT]: [''] + }); +} + +const openIndexedDBRequests = {}; +const databaseCache = {}; +const onCloseListeners = {}; + +function handleOpenOrDeleteReq (resolve, reject, req) { + // These things are almost impossible to test with fakeIndexedDB sadly + /* istanbul ignore next */ + req.onerror = () => reject(req.error); + /* istanbul ignore next */ + req.onblocked = () => reject(new Error('IDB blocked')); + req.onsuccess = () => resolve(req.result); +} + +async function createDatabase (dbName) { + const db = await new Promise((resolve, reject) => { + const req = indexedDB.open(dbName, DB_VERSION_CURRENT); + openIndexedDBRequests[dbName] = req; + req.onupgradeneeded = e => { + // Technically there is only one version, so we don't need this `if` check + // But if an old version of the JS is in another browser tab + // and it gets upgraded in the future and we have a new DB version, well... + // better safe than sorry. + /* istanbul ignore else */ + if (e.oldVersion < DB_VERSION_INITIAL) { + initialMigration(req.result); + } + }; + handleOpenOrDeleteReq(resolve, reject, req); + }); + // Handle abnormal closes, e.g. "delete database" in chrome dev tools. + // No need for removeEventListener, because once the DB can no longer + // fire "close" events, it will auto-GC. + // Unfortunately cannot test in fakeIndexedDB: https://github.com/dumbmatter/fakeIndexedDB/issues/50 + /* istanbul ignore next */ + db.onclose = () => closeDatabase(dbName); + return db +} + +function openDatabase (dbName) { + if (!databaseCache[dbName]) { + databaseCache[dbName] = createDatabase(dbName); + } + return databaseCache[dbName] +} + +function dbPromise (db, storeName, readOnlyOrReadWrite, cb) { + return new Promise((resolve, reject) => { + // Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone + // are really irreplaceable data. IndexedDB is just a cache in this case. + const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' }); + const store = typeof storeName === 'string' + ? txn.objectStore(storeName) + : storeName.map(name => txn.objectStore(name)); + let res; + cb(store, txn, (result) => { + res = result; + }); + + txn.oncomplete = () => resolve(res); + /* istanbul ignore next */ + txn.onerror = () => reject(txn.error); + }) +} + +function closeDatabase (dbName) { + // close any open requests + const req = openIndexedDBRequests[dbName]; + const db = req && req.result; + if (db) { + db.close(); + const listeners = onCloseListeners[dbName]; + /* istanbul ignore else */ + if (listeners) { + for (const listener of listeners) { + listener(); + } + } + } + delete openIndexedDBRequests[dbName]; + delete databaseCache[dbName]; + delete onCloseListeners[dbName]; +} + +function deleteDatabase (dbName) { + return new Promise((resolve, reject) => { + // close any open requests + closeDatabase(dbName); + const req = indexedDB.deleteDatabase(dbName); + handleOpenOrDeleteReq(resolve, reject, req); + }) +} + +// The "close" event occurs during an abnormal shutdown, e.g. a user clearing their browser data. +// However, it doesn't occur with the normal "close" event, so we handle that separately. +// https://www.w3.org/TR/IndexedDB/#close-a-database-connection +function addOnCloseListener (dbName, listener) { + let listeners = onCloseListeners[dbName]; + if (!listeners) { + listeners = onCloseListeners[dbName] = []; + } + listeners.push(listener); +} + +// list of emoticons that don't match a simple \W+ regex +// extracted using: +// require('emoji-picker-element-data/en/emojibase/data.json').map(_ => _.emoticon).filter(Boolean).filter(_ => !/^\W+$/.test(_)) +const irregularEmoticons = new Set([ + ':D', 'XD', ":'D", 'O:)', + ':X', ':P', ';P', 'XP', + ':L', ':Z', ':j', '8D', + 'XO', '8)', ':B', ':O', + ':S', ":'o", 'Dx', 'X(', + 'D:', ':C', '>0)', ':3', + ' { + if (!word.match(/\w/) || irregularEmoticons.has(word)) { + // for pure emoticons like :) or :-), just leave them as-is + return word.toLowerCase() + } + + return word + .replace(/[)(:,]/g, '') + .replace(/’/g, "'") + .toLowerCase() + }).filter(Boolean) +} + +const MIN_SEARCH_TEXT_LENGTH = 2; + +// This is an extra step in addition to extractTokens(). The difference here is that we expect +// the input to have already been run through extractTokens(). This is useful for cases like +// emoticons, where we don't want to do any tokenization (because it makes no sense to split up +// ">:)" by the colon) but we do want to lowercase it to have consistent search results, so that +// the user can type ':P' or ':p' and still get the same result. +function normalizeTokens (str) { + return str + .filter(Boolean) + .map(_ => _.toLowerCase()) + .filter(_ => _.length >= MIN_SEARCH_TEXT_LENGTH) +} + +// Transform emoji data for storage in IDB +function transformEmojiData (emojiData) { + const res = emojiData.map(({ annotation, emoticon, group, order, shortcodes, skins, tags, emoji, version }) => { + const tokens = [...new Set( + normalizeTokens([ + ...(shortcodes || []).map(extractTokens).flat(), + ...tags.map(extractTokens).flat(), + ...extractTokens(annotation), + emoticon + ]) + )].sort(); + const res = { + annotation, + group, + order, + tags, + tokens, + unicode: emoji, + version + }; + if (emoticon) { + res.emoticon = emoticon; + } + if (shortcodes) { + res.shortcodes = shortcodes; + } + if (skins) { + res.skinTones = []; + res.skinUnicodes = []; + res.skinVersions = []; + for (const { tone, emoji, version } of skins) { + res.skinTones.push(tone); + res.skinUnicodes.push(emoji); + res.skinVersions.push(version); + } + } + return res + }); + return res +} + +// helper functions that help compress the code better + +function callStore (store, method, key, cb) { + store[method](key).onsuccess = e => (cb && cb(e.target.result)); +} + +function getIDB (store, key, cb) { + callStore(store, 'get', key, cb); +} + +function getAllIDB (store, key, cb) { + callStore(store, 'getAll', key, cb); +} + +function commit (txn) { + /* istanbul ignore else */ + if (txn.commit) { + txn.commit(); + } +} + +// like lodash's minBy +function minBy (array, func) { + let minItem = array[0]; + for (let i = 1; i < array.length; i++) { + const item = array[i]; + if (func(minItem) > func(item)) { + minItem = item; + } + } + return minItem +} + +// return an array of results representing all items that are found in each one of the arrays + +function findCommonMembers (arrays, uniqByFunc) { + const shortestArray = minBy(arrays, _ => _.length); + const results = []; + for (const item of shortestArray) { + // if this item is included in every array in the intermediate results, add it to the final results + if (!arrays.some(array => array.findIndex(_ => uniqByFunc(_) === uniqByFunc(item)) === -1)) { + results.push(item); + } + } + return results +} + +async function isEmpty (db) { + return !(await get(db, STORE_KEYVALUE, KEY_URL)) +} + +async function hasData (db, url, eTag) { + const [oldETag, oldUrl] = await Promise.all([KEY_ETAG, KEY_URL] + .map(key => get(db, STORE_KEYVALUE, key))); + return (oldETag === eTag && oldUrl === url) +} + +async function doFullDatabaseScanForSingleResult (db, predicate) { + // This batching algorithm is just a perf improvement over a basic + // cursor. The BATCH_SIZE is an estimate of what would give the best + // perf for doing a full DB scan (worst case). + // + // Mini-benchmark for determining the best batch size: + // + // PERF=1 yarn build:rollup && yarn test:adhoc + // + // (async () => { + // performance.mark('start') + // await $('emoji-picker').database.getEmojiByShortcode('doesnotexist') + // performance.measure('total', 'start') + // console.log(performance.getEntriesByName('total').slice(-1)[0].duration) + // })() + const BATCH_SIZE = 50; // Typically around 150ms for 6x slowdown in Chrome for above benchmark + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { + let lastKey; + + const processNextBatch = () => { + emojiStore.getAll(lastKey && IDBKeyRange.lowerBound(lastKey, true), BATCH_SIZE).onsuccess = e => { + const results = e.target.result; + for (const result of results) { + lastKey = result.unicode; + if (predicate(result)) { + return cb(result) + } + } + if (results.length < BATCH_SIZE) { + return cb() + } + processNextBatch(); + }; + }; + processNextBatch(); + }) +} + +async function loadData (db, emojiData, url, eTag) { + try { + const transformedData = transformEmojiData(emojiData); + await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => { + let oldETag; + let oldUrl; + let todo = 0; + + function checkFetched () { + if (++todo === 2) { // 2 requests made + onFetched(); + } + } + + function onFetched () { + if (oldETag === eTag && oldUrl === url) { + // check again within the transaction to guard against concurrency, e.g. multiple browser tabs + return + } + // delete old data + emojiStore.clear(); + // insert new data + for (const data of transformedData) { + emojiStore.put(data); + } + metaStore.put(eTag, KEY_ETAG); + metaStore.put(url, KEY_URL); + commit(txn); + } + + getIDB(metaStore, KEY_ETAG, result => { + oldETag = result; + checkFetched(); + }); + + getIDB(metaStore, KEY_URL, result => { + oldUrl = result; + checkFetched(); + }); + }); + } finally { + } +} + +async function getEmojiByGroup (db, group) { + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { + const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true); + getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb); + }) +} + +async function getEmojiBySearchQuery (db, query) { + const tokens = normalizeTokens(extractTokens(query)); + + if (!tokens.length) { + return [] + } + + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { + // get all results that contain all tokens (i.e. an AND query) + const intermediateResults = []; + + const checkDone = () => { + if (intermediateResults.length === tokens.length) { + onDone(); + } + }; + + const onDone = () => { + const results = findCommonMembers(intermediateResults, _ => _.unicode); + cb(results.sort((a, b) => a.order < b.order ? -1 : 1)); + }; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + const range = i === tokens.length - 1 + ? IDBKeyRange.bound(token, token + '\uffff', false, true) // treat last token as a prefix search + : IDBKeyRange.only(token); // treat all other tokens as an exact match + getAllIDB(emojiStore.index(INDEX_TOKENS), range, result => { + intermediateResults.push(result); + checkDone(); + }); + } + }) +} + +// This could have been implemented as an IDB index on shortcodes, but it seemed wasteful to do that +// when we can already query by tokens and this will give us what we're looking for 99.9% of the time +async function getEmojiByShortcode (db, shortcode) { + const emojis = await getEmojiBySearchQuery(db, shortcode); + + // In very rare cases (e.g. the shortcode "v" as in "v for victory"), we cannot search because + // there are no usable tokens (too short in this case). In that case, we have to do an inefficient + // full-database scan, which I believe is an acceptable tradeoff for not having to have an extra + // index on shortcodes. + + if (!emojis.length) { + const predicate = _ => ((_.shortcodes || []).includes(shortcode.toLowerCase())); + return (await doFullDatabaseScanForSingleResult(db, predicate)) || null + } + + return emojis.filter(_ => { + const lowerShortcodes = (_.shortcodes || []).map(_ => _.toLowerCase()); + return lowerShortcodes.includes(shortcode.toLowerCase()) + })[0] || null +} + +async function getEmojiByUnicode (db, unicode) { + return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => ( + getIDB(emojiStore, unicode, result => { + if (result) { + return cb(result) + } + getIDB(emojiStore.index(INDEX_SKIN_UNICODE), unicode, result => cb(result || null)); + }) + )) +} + +function get (db, storeName, key) { + return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => ( + getIDB(store, key, cb) + )) +} + +function set (db, storeName, key, value) { + return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => { + store.put(value, key); + commit(txn); + }) +} + +function incrementFavoriteEmojiCount (db, unicode) { + return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => ( + getIDB(store, unicode, result => { + store.put((result || 0) + 1, unicode); + commit(txn); + }) + )) +} + +function getTopFavoriteEmoji (db, customEmojiIndex, limit) { + if (limit === 0) { + return [] + } + return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => { + const results = []; + favoritesStore.index(INDEX_COUNT).openCursor(undefined, 'prev').onsuccess = e => { + const cursor = e.target.result; + if (!cursor) { // no more results + return cb(results) + } + + function addResult (result) { + results.push(result); + if (results.length === limit) { + return cb(results) // done, reached the limit + } + cursor.continue(); + } + + const unicodeOrName = cursor.primaryKey; + const custom = customEmojiIndex.byName(unicodeOrName); + if (custom) { + return addResult(custom) + } + // This could be done in parallel (i.e. make the cursor and the get()s parallelized), + // but my testing suggests it's not actually faster. + getIDB(emojiStore, unicodeOrName, emoji => { + if (emoji) { + return addResult(emoji) + } + // emoji not found somehow, ignore (may happen if custom emoji change) + cursor.continue(); + }); + }; + }) +} + +// trie data structure for prefix searches +// loosely based on https://github.com/nolanlawson/substring-trie + +const CODA_MARKER = ''; // marks the end of the string + +function trie (arr, itemToTokens) { + const map = new Map(); + for (const item of arr) { + const tokens = itemToTokens(item); + for (const token of tokens) { + let currentMap = map; + for (let i = 0; i < token.length; i++) { + const char = token.charAt(i); + let nextMap = currentMap.get(char); + if (!nextMap) { + nextMap = new Map(); + currentMap.set(char, nextMap); + } + currentMap = nextMap; + } + let valuesAtCoda = currentMap.get(CODA_MARKER); + if (!valuesAtCoda) { + valuesAtCoda = []; + currentMap.set(CODA_MARKER, valuesAtCoda); + } + valuesAtCoda.push(item); + } + } + + const search = (query, exact) => { + let currentMap = map; + for (let i = 0; i < query.length; i++) { + const char = query.charAt(i); + const nextMap = currentMap.get(char); + if (nextMap) { + currentMap = nextMap; + } else { + return [] + } + } + + if (exact) { + const results = currentMap.get(CODA_MARKER); + return results || [] + } + + const results = []; + // traverse + const queue = [currentMap]; + while (queue.length) { + const currentMap = queue.shift(); + const entriesSortedByKey = [...currentMap.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1); + for (const [key, value] of entriesSortedByKey) { + if (key === CODA_MARKER) { // CODA_MARKER always comes first; it's the empty string + results.push(...value); + } else { + queue.push(value); + } + } + } + return results + }; + + return search +} + +const requiredKeys$1 = [ + 'name', + 'url' +]; + +function assertCustomEmojis (customEmojis) { + const isArray = customEmojis && Array.isArray(customEmojis); + const firstItemIsFaulty = isArray && + customEmojis.length && + (!customEmojis[0] || requiredKeys$1.some(key => !(key in customEmojis[0]))); + if (!isArray || firstItemIsFaulty) { + throw new Error('Custom emojis are in the wrong format') + } +} + +function customEmojiIndex (customEmojis) { + assertCustomEmojis(customEmojis); + + const sortByName = (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + + // + // all() + // + const all = customEmojis.sort(sortByName); + + // + // search() + // + const emojiToTokens = emoji => ( + [...new Set((emoji.shortcodes || []).map(shortcode => extractTokens(shortcode)).flat())] + ); + const searchTrie = trie(customEmojis, emojiToTokens); + const searchByExactMatch = _ => searchTrie(_, true); + const searchByPrefix = _ => searchTrie(_, false); + + // Search by query for custom emoji. Similar to how we do this in IDB, the last token + // is treated as a prefix search, but every other one is treated as an exact match. + // Then we AND the results together + const search = query => { + const tokens = extractTokens(query); + const intermediateResults = tokens.map((token, i) => ( + (i < tokens.length - 1 ? searchByExactMatch : searchByPrefix)(token) + )); + return findCommonMembers(intermediateResults, _ => _.name).sort(sortByName) + }; + + // + // byShortcode, byName + // + const shortcodeToEmoji = new Map(); + const nameToEmoji = new Map(); + for (const customEmoji of customEmojis) { + nameToEmoji.set(customEmoji.name.toLowerCase(), customEmoji); + for (const shortcode of (customEmoji.shortcodes || [])) { + shortcodeToEmoji.set(shortcode.toLowerCase(), customEmoji); + } + } + + const byShortcode = shortcode => shortcodeToEmoji.get(shortcode.toLowerCase()); + const byName = name => nameToEmoji.get(name.toLowerCase()); + + return { + all, + search, + byShortcode, + byName + } +} + +// remove some internal implementation details, i.e. the "tokens" array on the emoji object +// essentially, convert the emoji from the version stored in IDB to the version used in-memory +function cleanEmoji (emoji) { + if (!emoji) { + return emoji + } + delete emoji.tokens; + if (emoji.skinTones) { + const len = emoji.skinTones.length; + emoji.skins = Array(len); + for (let i = 0; i < len; i++) { + emoji.skins[i] = { + tone: emoji.skinTones[i], + unicode: emoji.skinUnicodes[i], + version: emoji.skinVersions[i] + }; + } + delete emoji.skinTones; + delete emoji.skinUnicodes; + delete emoji.skinVersions; + } + return emoji +} + +function warnETag (eTag) { + if (!eTag) { + console.warn('emoji-picker-element is more efficient if the dataSource server exposes an ETag header.'); + } +} + +const requiredKeys = [ + 'annotation', + 'emoji', + 'group', + 'order', + 'tags', + 'version' +]; + +function assertEmojiData (emojiData) { + if (!emojiData || + !Array.isArray(emojiData) || + !emojiData[0] || + (typeof emojiData[0] !== 'object') || + requiredKeys.some(key => (!(key in emojiData[0])))) { + throw new Error('Emoji data is in the wrong format') + } +} + +function assertStatus (response, dataSource) { + if (Math.floor(response.status / 100) !== 2) { + throw new Error('Failed to fetch: ' + dataSource + ': ' + response.status) + } +} + +async function getETag (dataSource) { + const response = await fetch(dataSource, { method: 'HEAD' }); + assertStatus(response, dataSource); + const eTag = response.headers.get('etag'); + warnETag(eTag); + return eTag +} + +async function getETagAndData (dataSource) { + const response = await fetch(dataSource); + assertStatus(response, dataSource); + const eTag = response.headers.get('etag'); + warnETag(eTag); + const emojiData = await response.json(); + assertEmojiData(emojiData); + return [eTag, emojiData] +} + +// TODO: including these in blob-util.ts causes typedoc to generate docs for them, +/** + * Convert an `ArrayBuffer` to a binary string. + * + * Example: + * + * ```js + * var myString = blobUtil.arrayBufferToBinaryString(arrayBuff) + * ``` + * + * @param buffer - array buffer + * @returns binary string + */ +function arrayBufferToBinaryString(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var length = bytes.byteLength; + var i = -1; + while (++i < length) { + binary += String.fromCharCode(bytes[i]); + } + return binary; +} +/** + * Convert a binary string to an `ArrayBuffer`. + * + * ```js + * var myBuffer = blobUtil.binaryStringToArrayBuffer(binaryString) + * ``` + * + * @param binary - binary string + * @returns array buffer + */ +function binaryStringToArrayBuffer(binary) { + var length = binary.length; + var buf = new ArrayBuffer(length); + var arr = new Uint8Array(buf); + var i = -1; + while (++i < length) { + arr[i] = binary.charCodeAt(i); + } + return buf; +} + +// generate a checksum based on the stringified JSON +async function jsonChecksum (object) { + const inString = JSON.stringify(object); + const inBuffer = binaryStringToArrayBuffer(inString); + // this does not need to be cryptographically secure, SHA-1 is fine + const outBuffer = await crypto.subtle.digest('SHA-1', inBuffer); + const outBinString = arrayBufferToBinaryString(outBuffer); + const res = btoa(outBinString); + return res +} + +async function checkForUpdates (db, dataSource) { + // just do a simple HEAD request first to see if the eTags match + let emojiData; + let eTag = await getETag(dataSource); + if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers + const eTagAndData = await getETagAndData(dataSource); + eTag = eTagAndData[0]; + emojiData = eTagAndData[1]; + if (!eTag) { + eTag = await jsonChecksum(emojiData); + } + } + if (await hasData(db, dataSource, eTag)) ; else { + if (!emojiData) { + const eTagAndData = await getETagAndData(dataSource); + emojiData = eTagAndData[1]; + } + await loadData(db, emojiData, dataSource, eTag); + } +} + +async function loadDataForFirstTime (db, dataSource) { + let [eTag, emojiData] = await getETagAndData(dataSource); + if (!eTag) { + // Handle lack of support for ETag or Access-Control-Expose-Headers + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility + eTag = await jsonChecksum(emojiData); + } + + await loadData(db, emojiData, dataSource, eTag); +} + +class Database { + constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) { + this.dataSource = dataSource; + this.locale = locale; + this._dbName = `emoji-picker-element-${this.locale}`; + this._db = undefined; + this._lazyUpdate = undefined; + this._custom = customEmojiIndex(customEmoji); + + this._clear = this._clear.bind(this); + this._ready = this._init(); + } + + async _init () { + const db = this._db = await openDatabase(this._dbName); + + addOnCloseListener(this._dbName, this._clear); + const dataSource = this.dataSource; + const empty = await isEmpty(db); + + if (empty) { + await loadDataForFirstTime(db, dataSource); + } else { // offline-first - do an update asynchronously + this._lazyUpdate = checkForUpdates(db, dataSource); + } + } + + async ready () { + const checkReady = async () => { + if (!this._ready) { + this._ready = this._init(); + } + return this._ready + }; + await checkReady(); + // There's a possibility of a race condition where the element gets added, removed, and then added again + // with a particular timing, which would set the _db to undefined. + // We *could* do a while loop here, but that seems excessive and could lead to an infinite loop. + if (!this._db) { + await checkReady(); + } + } + + async getEmojiByGroup (group) { + assertNumber(group); + await this.ready(); + return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji) + } + + async getEmojiBySearchQuery (query) { + assertNonEmptyString(query); + await this.ready(); + const customs = this._custom.search(query); + const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji); + return [ + ...customs, + ...natives + ] + } + + async getEmojiByShortcode (shortcode) { + assertNonEmptyString(shortcode); + await this.ready(); + const custom = this._custom.byShortcode(shortcode); + if (custom) { + return custom + } + return cleanEmoji(await getEmojiByShortcode(this._db, shortcode)) + } + + async getEmojiByUnicodeOrName (unicodeOrName) { + assertNonEmptyString(unicodeOrName); + await this.ready(); + const custom = this._custom.byName(unicodeOrName); + if (custom) { + return custom + } + return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName)) + } + + async getPreferredSkinTone () { + await this.ready(); + return (await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0 + } + + async setPreferredSkinTone (skinTone) { + assertNumber(skinTone); + await this.ready(); + return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone) + } + + async incrementFavoriteEmojiCount (unicodeOrName) { + assertNonEmptyString(unicodeOrName); + await this.ready(); + return incrementFavoriteEmojiCount(this._db, unicodeOrName) + } + + async getTopFavoriteEmoji (limit) { + assertNumber(limit); + await this.ready(); + return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji) + } + + set customEmoji (customEmojis) { + this._custom = customEmojiIndex(customEmojis); + } + + get customEmoji () { + return this._custom.all + } + + async _shutdown () { + await this.ready(); // reopen if we've already been closed/deleted + try { + await this._lazyUpdate; // allow any lazy updates to process before closing/deleting + } catch (err) { /* ignore network errors (offline-first) */ } + } + + // clear references to IDB, e.g. during a close event + _clear () { + // We don't need to call removeEventListener or remove the manual "close" listeners. + // The memory leak tests prove this is unnecessary. It's because: + // 1) IDBDatabases that can no longer fire "close" automatically have listeners GCed + // 2) we clear the manual close listeners in databaseLifecycle.js. + this._db = this._ready = this._lazyUpdate = undefined; + } + + async close () { + await this._shutdown(); + await closeDatabase(this._dbName); + } + + async delete () { + await this._shutdown(); + await deleteDatabase(this._dbName); + } +} + +export { Database as default }; diff --git a/app/static/scripts/emoji-picker-element/i18n/ar.js b/app/static/scripts/emoji-picker-element/i18n/ar.js new file mode 100644 index 0000000..672a0d9 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/ar.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'الفئات', + emojiUnsupportedMessage: 'متصفحك لا يدعم رموز المشاعر الملونة.', + favoritesLabel: 'المفضلة', + loadingMessage: 'جارٍ التحميل…', + networkErrorMessage: 'تعذر تحميل رمز مشاعر.', + regionLabel: 'منتقي رموز المشاعر', + searchDescription: 'عندما تكون نتائج البحث متاحة، اضغط السهم لأعلى أو لأسفل للتحديد واضغط enter للاختيار.', + searchLabel: 'بحث', + searchResultsLabel: 'نتائج البحث', + skinToneDescription: 'عند توسيع النتائج، اضغط السهم لأعلى أو لأسفل للتحديد واضغط enter للاختيار.', + skinToneLabel: 'اختر درجة لون البشرة (حاليًا {skinTone})', + skinTonesLabel: 'درجات لون البشرة', + skinTones: [ + 'افتراضي', + 'فاتح', + 'فاتح متوسط', + 'متوسط', + 'داكن متوسط', + 'داكن' + ], + categories: { + custom: 'مخصص', + 'smileys-emotion': 'الوجوه الضاحكة ورموز المشاعر', + 'people-body': 'الأشخاص والجسد', + 'animals-nature': 'الحيوانات والطبيعة', + 'food-drink': 'الطعام والشراب', + 'travel-places': 'السفر والأماكن', + activities: 'الأنشطة', + objects: 'الأشياء', + symbols: 'الرموز', + flags: 'الأعلام' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/de.js b/app/static/scripts/emoji-picker-element/i18n/de.js new file mode 100644 index 0000000..bd21ed3 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/de.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Kategorien', + emojiUnsupportedMessage: 'Dein Browser unterstützt keine farbigen Emojis.', + favoritesLabel: 'Favoriten', + loadingMessage: 'Wird geladen…', + networkErrorMessage: 'Konnte Emoji nicht laden.', + regionLabel: 'Emoji auswählen', + searchDescription: 'Wenn Suchergebnisse verfügbar sind, wähle sie mit Pfeil rauf und runter, dann Eingabetaste, aus.', + searchLabel: 'Suchen', + searchResultsLabel: 'Suchergebnisse', + skinToneDescription: 'Wenn angezeigt, nutze Pfeiltasten rauf und runter zum Auswählen, Eingabe zum Akzeptieren.', + skinToneLabel: 'Wähle einen Hautton (aktuell {skinTone})', + skinTonesLabel: 'Hauttöne', + skinTones: [ + 'Standard', + 'Hell', + 'Mittel-hell', + 'Mittel', + 'Mittel-dunkel', + 'Dunkel' + ], + categories: { + custom: 'Benutzerdefiniert', + 'smileys-emotion': 'Smileys und Emoticons', + 'people-body': 'Menschen und Körper', + 'animals-nature': 'Tiere und Natur', + 'food-drink': 'Essen und Trinken', + 'travel-places': 'Reisen und Orte', + activities: 'Aktivitäten', + objects: 'Objekte', + symbols: 'Symbole', + flags: 'Flaggen' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/en.js b/app/static/scripts/emoji-picker-element/i18n/en.js new file mode 100644 index 0000000..57e15ff --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/en.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Categories', + emojiUnsupportedMessage: 'Your browser does not support color emoji.', + favoritesLabel: 'Favorites', + loadingMessage: 'Loading…', + networkErrorMessage: 'Could not load emoji.', + regionLabel: 'Emoji picker', + searchDescription: 'When search results are available, press up or down to select and enter to choose.', + searchLabel: 'Search', + searchResultsLabel: 'Search results', + skinToneDescription: 'When expanded, press up or down to select and enter to choose.', + skinToneLabel: 'Choose a skin tone (currently {skinTone})', + skinTonesLabel: 'Skin tones', + skinTones: [ + 'Default', + 'Light', + 'Medium-Light', + 'Medium', + 'Medium-Dark', + 'Dark' + ], + categories: { + custom: 'Custom', + 'smileys-emotion': 'Smileys and emoticons', + 'people-body': 'People and body', + 'animals-nature': 'Animals and nature', + 'food-drink': 'Food and drink', + 'travel-places': 'Travel and places', + activities: 'Activities', + objects: 'Objects', + symbols: 'Symbols', + flags: 'Flags' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/es.js b/app/static/scripts/emoji-picker-element/i18n/es.js new file mode 100644 index 0000000..9b8ec49 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/es.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Categorías', + emojiUnsupportedMessage: 'El navegador no admite emojis de color.', + favoritesLabel: 'Favoritos', + loadingMessage: 'Cargando…', + networkErrorMessage: 'No se pudo cargar el emoji.', + regionLabel: 'Selector de emojis', + searchDescription: 'Cuando estén disponibles los resultados, pulsa la tecla hacia arriba o hacia abajo para seleccionar y la tecla intro para elegir.', + searchLabel: 'Buscar', + searchResultsLabel: 'Resultados de búsqueda', + skinToneDescription: 'Cuando se abran las opciones, pulsa la tecla hacia arriba o hacia abajo para seleccionar y la tecla intro para elegir.', + skinToneLabel: 'Elige un tono de piel ({skinTone} es el actual)', + skinTonesLabel: 'Tonos de piel', + skinTones: [ + 'Predeterminado', + 'Claro', + 'Claro medio', + 'Medio', + 'Oscuro medio', + 'Oscuro' + ], + categories: { + custom: 'Personalizado', + 'smileys-emotion': 'Emojis y emoticones', + 'people-body': 'Personas y partes del cuerpo', + 'animals-nature': 'Animales y naturaleza', + 'food-drink': 'Comida y bebida', + 'travel-places': 'Viajes y lugares', + activities: 'Actividades', + objects: 'Objetos', + symbols: 'Símbolos', + flags: 'Banderas' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/fr.js b/app/static/scripts/emoji-picker-element/i18n/fr.js new file mode 100644 index 0000000..d61e2cf --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/fr.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Catégories', + emojiUnsupportedMessage: 'Votre navigateur ne soutient pas les emojis en couleur.', + favoritesLabel: 'Favoris', + loadingMessage: 'Chargement en cours…', + networkErrorMessage: 'Impossible de charger les emojis.', + regionLabel: 'Choisir un emoji', + searchDescription: 'Quand les résultats sont disponisbles, appuyez la fleche vers le haut ou le bas et la touche entrée pour choisir.', + searchLabel: 'Rechercher', + searchResultsLabel: 'Résultats', + skinToneDescription: 'Quand disponible, appuyez la fleche vers le haut ou le bas et la touch entrée pour choisir.', + skinToneLabel: 'Choisir une couleur de peau (actuellement {skinTone})', + skinTonesLabel: 'Couleurs de peau', + skinTones: [ + 'Défaut', + 'Clair', + 'Moyennement clair', + 'Moyen', + 'Moyennement sombre', + 'Sombre' + ], + categories: { + custom: 'Customisé', + 'smileys-emotion': 'Les smileyes et les émoticônes', + 'people-body': 'Les gens et le corps', + 'animals-nature': 'Les animaux et la nature', + 'food-drink': 'La nourriture et les boissons', + 'travel-places': 'Les voyages et les endroits', + activities: 'Les activités', + objects: 'Les objets', + symbols: 'Les symbols', + flags: 'Les drapeaux' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/hi.js b/app/static/scripts/emoji-picker-element/i18n/hi.js new file mode 100644 index 0000000..1141cd1 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/hi.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'श्रेणियाँ', + emojiUnsupportedMessage: 'आपका ब्राउज़र कलर इमोजी का समर्थन नहीं करता।', + favoritesLabel: 'पसंदीदा', + loadingMessage: 'लोड हो रहा है...', + networkErrorMessage: 'इमोजी लोड नहीं हो सके।', + regionLabel: 'इमोजी चुननेवाला', + searchDescription: 'जब खोज परिणाम उपलब्ध हों तो चयन करने के लिए ऊपर या नीचे दबाएं और चुनने के लिए एंटर दबाएं।', + searchLabel: 'खोज', + searchResultsLabel: 'खोज के परिणाम', + skinToneDescription: 'जब विस्तृत किया जाता है तो चयन करने के लिए ऊपर या नीचे दबाएं और चुनने के लिए एंटर दबाएं।', + skinToneLabel: 'त्वचा का रंग चुनें (वर्तमान में {skinTone})', + skinTonesLabel: 'त्वचा के रंग', + skinTones: [ + 'डिफॉल्ट', + 'हल्का', + 'मध्यम हल्का', + 'मध्यम', + 'मध्यम गहरा', + 'गहरा' + ], + categories: { + custom: 'कस्टम', + 'smileys-emotion': 'स्माइली और इमोटिकॉन्स', + 'people-body': 'लोग और शरीर', + 'animals-nature': 'पशु और प्रकृति', + 'food-drink': 'खाद्य और पेय', + 'travel-places': 'यात्रा और स्थान', + activities: 'गतिविधियां', + objects: 'वस्तुएं', + symbols: 'प्रतीक', + flags: 'झंडे' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/id.js b/app/static/scripts/emoji-picker-element/i18n/id.js new file mode 100644 index 0000000..994a26d --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/id.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Kategori', + emojiUnsupportedMessage: 'Browser Anda tidak mendukung emoji warna.', + favoritesLabel: 'Favorit', + loadingMessage: 'Memuat...', + networkErrorMessage: 'Tidak dapat memuat emoji.', + regionLabel: 'Pemilih emoji', + searchDescription: 'Ketika hasil pencarian tersedia, tekan atas atau bawah untuk menyeleksi dan enter untuk memilih.', + searchLabel: 'Cari', + searchResultsLabel: 'Hasil Pencarian', + skinToneDescription: 'Saat diperluas tekan atas atau bawah untuk menyeleksi dan enter untuk memilih.', + skinToneLabel: 'Pilih warna skin (saat ini {skinTone})', + skinTonesLabel: 'Warna skin', + skinTones: [ + 'Default', + 'Light', + 'Medium light', + 'Medium', + 'Medium dark', + 'Dark' + ], + categories: { + custom: 'Kustom', + 'smileys-emotion': 'Smiley dan emoticon', + 'people-body': 'Orang dan bagian tubuh', + 'animals-nature': 'Hewan dan tumbuhan', + 'food-drink': 'Makanan dan minuman', + 'travel-places': 'Rekreasi dan tempat', + activities: 'Aktivitas', + objects: 'Objek', + symbols: 'Simbol', + flags: 'Bendera' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/it.js b/app/static/scripts/emoji-picker-element/i18n/it.js new file mode 100644 index 0000000..df55324 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/it.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Categorie', + emojiUnsupportedMessage: 'Il tuo browser non supporta le emoji colorate.', + favoritesLabel: 'Preferiti', + loadingMessage: 'Caricamento...', + networkErrorMessage: 'Impossibile caricare le emoji.', + regionLabel: 'Selezione emoji', + searchDescription: 'Quando i risultati della ricerca sono disponibili, premi su o giù per selezionare e invio per scegliere.', + searchLabel: 'Cerca', + searchResultsLabel: 'Risultati di ricerca', + skinToneDescription: 'Quando espanso, premi su o giù per selezionare e invio per scegliere.', + skinToneLabel: 'Scegli una tonalità della pelle (corrente {skinTone})', + skinTonesLabel: 'Tonalità della pelle', + skinTones: [ + 'Predefinita', + 'Chiara', + 'Medio-Chiara', + 'Media', + 'Medio-Scura', + 'Scura' + ], + categories: { + custom: 'Personalizzata', + 'smileys-emotion': 'Faccine ed emozioni', + 'people-body': 'Persone e corpi', + 'animals-nature': 'Animali e natura', + 'food-drink': 'Cibi e bevande', + 'travel-places': 'Viaggi e luoghi', + activities: 'Attività', + objects: 'Oggetti', + symbols: 'Simboli', + flags: 'Bandiere' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/ms_MY.js b/app/static/scripts/emoji-picker-element/i18n/ms_MY.js new file mode 100644 index 0000000..21ae388 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/ms_MY.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Kategori', + emojiUnsupportedMessage: 'Penyemak imbas anda tidak menyokong emoji warna.', + favoritesLabel: 'Kegemaran', + loadingMessage: 'Memuat…', + networkErrorMessage: 'Tidak dapat memuatkan emoji.', + regionLabel: 'Pemilih emoji', + searchDescription: 'Apabila hasil carian tersedia, tekan atas atau bawah untuk memilih dan tekan masukkan untuk memilih.', + searchLabel: 'Cari', + searchResultsLabel: 'Hasil carian', + skinToneDescription: 'Apabila dikembangkan, tekan atas atau bawah untuk memilih dan tekan masukkan untuk memilih.', + skinToneLabel: 'Pilih warna kulit (pada masa ini {skinTone})', + skinTonesLabel: 'Warna kulit', + skinTones: [ + 'Lalai', + 'Cerah', + 'Kuning langsat', + 'Sederhana cerah', + 'Sawo matang', + 'Gelap' + ], + categories: { + custom: 'Tersuai', + 'smileys-emotion': 'Smiley dan emotikon', + 'people-body': 'Orang dan badan', + 'animals-nature': 'Haiwan dan alam semula jadi', + 'food-drink': 'Makanan dan minuman', + 'travel-places': 'Perjalanan dan tempat', + activities: 'Aktiviti', + objects: 'Objek', + symbols: 'Simbol', + flags: 'Bendera' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/nl.js b/app/static/scripts/emoji-picker-element/i18n/nl.js new file mode 100644 index 0000000..a2dabc5 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/nl.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Categorieën', + emojiUnsupportedMessage: 'Uw browser ondersteunt geen kleurenemoji.', + favoritesLabel: 'Favorieten', + loadingMessage: 'Bezig met laden…', + networkErrorMessage: 'Kan emoji niet laden.', + regionLabel: 'Emoji-kiezer', + searchDescription: 'Als er zoekresultaten beschikbaar zijn, drukt u op omhoog of omlaag om te selecteren en op enter om te kiezen.', + searchLabel: 'Zoeken', + searchResultsLabel: 'Zoekresultaten', + skinToneDescription: 'Wanneer uitgevouwen, druk omhoog of omlaag om te selecteren en enter om te kiezen.', + skinToneLabel: 'Kies een huidskleur (momenteel {skinTone})', + skinTonesLabel: 'Huidskleuren', + skinTones: [ + 'Standaard', + 'Licht', + 'Medium-Licht', + 'Medium', + 'Middeldonker', + 'Donker' + ], + categories: { + custom: 'Aangepast', + 'smileys-emotion': 'Smileys en emoticons', + 'people-body': 'Mensen en lichaam', + 'animals-nature': 'Dieren en natuur', + 'food-drink': 'Eten en drinken', + 'travel-places': 'Reizen en plaatsen', + activities: 'Activiteiten', + objects: 'Voorwerpen', + symbols: 'Symbolen', + flags: 'Vlaggen' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/pl.js b/app/static/scripts/emoji-picker-element/i18n/pl.js new file mode 100644 index 0000000..454cb7d --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/pl.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Kategorie', + emojiUnsupportedMessage: 'Twoja przeglądarka nie wspiera kolorowych emotikon.', + favoritesLabel: 'Ulubione', + loadingMessage: 'Ładuję…', + networkErrorMessage: 'Nie można załadować emoji.', + regionLabel: 'Selektor emoji', + searchDescription: 'Kiedy wyniki wyszukiwania będą dostępne, wciśnij góra lub dół aby wybrać oraz enter aby zatwierdzić wybór.', + searchLabel: 'Wyszukaj', + searchResultsLabel: 'Wyniki wyszukiwania', + skinToneDescription: 'Po rozwinięciu wciśnij góra lub dół aby wybrać oraz enter aby zatwierdzić wybór.', + skinToneLabel: 'Wybierz odcień skóry (aktualnie {skinTone})', + skinTonesLabel: 'Odcienie skóry', + skinTones: [ + 'Domyślna', + 'Jasna', + 'Średnio-jasna', + 'Średnia', + 'Średnio-ciemna', + 'Ciemna' + ], + categories: { + custom: 'Własne', + 'smileys-emotion': 'Uśmiechy', + 'people-body': 'Ludzie', + 'animals-nature': 'Zwierzęta i natura', + 'food-drink': 'Żywność i napoje', + 'travel-places': 'Podróże i miejsca', + activities: 'Aktywności', + objects: 'Obiekty', + symbols: 'Symbole', + flags: 'Flagi' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/pt_BR.js b/app/static/scripts/emoji-picker-element/i18n/pt_BR.js new file mode 100644 index 0000000..25b5dc9 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/pt_BR.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Categorias', + emojiUnsupportedMessage: 'Seu navegador não suporta emojis coloridos.', + favoritesLabel: 'Favoritos', + loadingMessage: 'Carregando…', + networkErrorMessage: 'Não foi possível carregar o emoji.', + regionLabel: 'Seletor de emoji', + searchDescription: 'Quando os resultados da pesquisa estiverem disponíveis, pressione para cima ou para baixo para selecionar e “enter” para escolher.', + searchLabel: 'Procurar', + searchResultsLabel: 'Resultados da pesquisa', + skinToneDescription: 'Quando expandido, pressione para cima ou para baixo para selecionar e “enter” para escolher.', + skinToneLabel: 'Escolha um tom de pele (atualmente {skinTone})', + skinTonesLabel: 'Tons de pele', + skinTones: [ + 'Padrão', + 'Claro', + 'Claro médio', + 'Médio', + 'Escuro médio', + 'Escuro' + ], + categories: { + custom: 'Personalizar', + 'smileys-emotion': 'Carinhas e emoticons', + 'people-body': 'Pessoas e corpo', + 'animals-nature': 'Animais e natureza', + 'food-drink': 'Alimentos e bebidas', + 'travel-places': 'Viagem e lugares', + activities: 'Atividades', + objects: 'Objetos', + symbols: 'Símbolos', + flags: 'Bandeiras' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/pt_PT.js b/app/static/scripts/emoji-picker-element/i18n/pt_PT.js new file mode 100644 index 0000000..93d88db --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/pt_PT.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Categorias', + emojiUnsupportedMessage: 'O seu browser não suporta emojis.', + favoritesLabel: 'Favoritos', + loadingMessage: 'A Carregar…', + networkErrorMessage: 'Não foi possível carregar o emoji.', + regionLabel: 'Emoji picker', + searchDescription: 'Quando os resultados da pesquisa estiverem disponíveis, pressione para cima ou para baixo para selecionar e digite para escolher.', + searchLabel: 'Procurar', + searchResultsLabel: 'Resultados da procura', + skinToneDescription: 'Quando expandido, pressione para cima ou para baixo para selecionar e digite para escolher.', + skinToneLabel: 'Escolha um tom de pele (atual {skinTone})', + skinTonesLabel: 'Tons de pele', + skinTones: [ + 'Pré-definido', + 'Claro', + 'Médio-Claro', + 'Médio', + 'Médio-Escuro', + 'Escuro' + ], + categories: { + custom: 'Personalizados', + 'smileys-emotion': 'Smileys e emoticons', + 'people-body': 'Pessoas e corpo', + 'animals-nature': 'Animais e natureza', + 'food-drink': 'Comida e bebida', + 'travel-places': 'Viagens e locais', + activities: 'Atividades', + objects: 'Objetos', + symbols: 'Símbolos', + flags: 'Bandeiras' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/ru_RU.js b/app/static/scripts/emoji-picker-element/i18n/ru_RU.js new file mode 100644 index 0000000..ea90966 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/ru_RU.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Категории', + emojiUnsupportedMessage: 'Ваш браузер не поддерживает цветные эмодзи.', + favoritesLabel: 'Избранное', + loadingMessage: 'Загрузка…', + networkErrorMessage: 'Не удалось загрузить эмодзи. Попробуйте перезагрузить страницу.', + regionLabel: 'Выберите эмодзи', + searchDescription: 'Когда результаты поиска станут доступны, выберите их с помощью стрелок вверх и вниз, затем нажмите для подтверждения.', + searchLabel: 'Искать', + searchResultsLabel: 'Результаты поиска', + skinToneDescription: 'При отображении используйте клавиши со стрелками вверх и вниз для выбора, нажмите для подтверждения.', + skinToneLabel: 'Выберите оттенок кожи (текущий {skinTone})', + skinTonesLabel: 'Оттенки кожи', + skinTones: [ + 'Стандартный', + 'Светлый', + 'Средне-светлый', + 'Средний', + 'Средне-темный', + 'Темный' + ], + categories: { + custom: 'Пользовательский', + 'smileys-emotion': 'Смайлики и Эмотиконы', + 'people-body': 'Люди и Тела', + 'animals-nature': 'Животные и Природа', + 'food-drink': 'Еда и Напитки', + 'travel-places': 'Путешествия и Места', + activities: 'Виды деятельности', + objects: 'Объекты', + symbols: 'Символы', + flags: 'Флаги' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/tr.js b/app/static/scripts/emoji-picker-element/i18n/tr.js new file mode 100644 index 0000000..c41491b --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/tr.js @@ -0,0 +1,34 @@ +export default { + categoriesLabel: 'Kategoriler', + emojiUnsupportedMessage: 'Tarayıcınız renkli emojiyi desteklemiyor.', + favoritesLabel: 'Favoriler', + loadingMessage: 'Yükleniyor…', + networkErrorMessage: 'Emoji yüklenemedi.', + regionLabel: 'Emoji seçici', + searchDescription: 'Arama sonuçları mevcut olduğunda seçmek için yukarı veya aşağı basın ve seçmek için girin.', + searchLabel: 'Arama', + searchResultsLabel: 'Arama sonuçları', + skinToneDescription: 'Genişletildiğinde seçmek için yukarı veya aşağı basın ve seçmek için girin.', + skinToneLabel: 'Cilt tonu seçin (şu anda {skinTone})', + skinTonesLabel: 'Cilt tonları', + skinTones: [ + 'Varsayılan', + 'Işık', + 'Orta ışık', + 'Orta', + 'Orta koyu', + 'Karanlık' + ], + categories: { + custom: 'Gelenek', + 'smileys-emotion': 'Suratlar ve ifadeler', + 'people-body': 'İnsanlar ve vücut', + 'animals-nature': 'Hayvanlar ve doğa', + 'food-drink': 'Yiyecek ve içecek', + 'travel-places': 'Seyahat ve yerler', + activities: 'Aktiviteler', + objects: 'Nesneler', + symbols: 'Semboller', + flags: 'Bayraklar' + } +} diff --git a/app/static/scripts/emoji-picker-element/i18n/zh_CN.js b/app/static/scripts/emoji-picker-element/i18n/zh_CN.js new file mode 100644 index 0000000..6dd452a --- /dev/null +++ b/app/static/scripts/emoji-picker-element/i18n/zh_CN.js @@ -0,0 +1,27 @@ +export default { + categoriesLabel: '类别', + emojiUnsupportedMessage: '您的浏览器不支持彩色表情符号。', + favoritesLabel: '收藏夹', + loadingMessage: '正在加载…', + networkErrorMessage: '无法加载表情符号。', + regionLabel: '表情符号选择器', + searchDescription: '当搜索结果可用时,按向上或向下选择并输入选择。', + searchLabel: '搜索', + searchResultsLabel: '搜索结果', + skinToneDescription: '展开时,按向上或向下键进行选择,按回车键进行选择。', + skinToneLabel: '选择肤色(当前为 {skinTone})', + skinTonesLabel: '肤色', + skinTones: ['默认', '明亮', '微亮', '中等', '微暗', '暗'], + categories: { + custom: '自定义', + 'smileys-emotion': '笑脸和表情', + 'people-body': '人物和身体', + 'animals-nature': '动物与自然', + 'food-drink': '食品饮料', + 'travel-places': '旅行和地方', + activities: '活动', + objects: '物体', + symbols: '符号', + flags: '旗帜' + } +} diff --git a/app/static/scripts/emoji-picker-element/index.js b/app/static/scripts/emoji-picker-element/index.js new file mode 100644 index 0000000..aa23cc4 --- /dev/null +++ b/app/static/scripts/emoji-picker-element/index.js @@ -0,0 +1,3 @@ +import Picker from './picker.js' +import Database from './database.js' +export { Picker, Database } diff --git a/app/static/scripts/emoji-picker-element/picker.js b/app/static/scripts/emoji-picker-element/picker.js new file mode 100644 index 0000000..c4ed88b --- /dev/null +++ b/app/static/scripts/emoji-picker-element/picker.js @@ -0,0 +1,2704 @@ +import Database from './database.js'; + +function noop() { } +function run(fn) { + return fn(); +} +function blank_object() { + return Object.create(null); +} +function run_all(fns) { + fns.forEach(run); +} +function is_function(thing) { + return typeof thing === 'function'; +} +function safe_not_equal(a, b) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} +let src_url_equal_anchor; +function src_url_equal(element_src, url) { + if (!src_url_equal_anchor) { + src_url_equal_anchor = document.createElement('a'); + } + src_url_equal_anchor.href = url; + return element_src === src_url_equal_anchor.href; +} +function is_empty(obj) { + return Object.keys(obj).length === 0; +} +function action_destroyer(action_result) { + return action_result && is_function(action_result.destroy) ? action_result.destroy : noop; +} +function append(target, node) { + target.appendChild(node); +} +function insert(target, node, anchor) { + target.insertBefore(node, anchor || null); +} +function detach(node) { + if (node.parentNode) { + node.parentNode.removeChild(node); + } +} +function element(name) { + return document.createElement(name); +} +function text(data) { + return document.createTextNode(data); +} +function listen(node, event, handler, options) { + node.addEventListener(event, handler, options); + return () => node.removeEventListener(event, handler, options); +} +function attr(node, attribute, value) { + if (value == null) + node.removeAttribute(attribute); + else if (node.getAttribute(attribute) !== value) + node.setAttribute(attribute, value); +} +function set_data(text, data) { + data = '' + data; + if (text.wholeText !== data) + text.data = data; +} +function set_input_value(input, value) { + input.value = value == null ? '' : value; +} +function set_style(node, key, value, important) { + if (value === null) { + node.style.removeProperty(key); + } + else { + node.style.setProperty(key, value, important ? 'important' : ''); + } +} + +let current_component; +function set_current_component(component) { + current_component = component; +} +function get_current_component() { + if (!current_component) + throw new Error('Function called outside component initialization'); + return current_component; +} +/** + * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. + * It must be called during the component's initialisation (but doesn't need to live *inside* the component; + * it can be called from an external module). + * + * `onMount` does not run inside a [server-side component](/docs#run-time-server-side-component-api). + * + * https://svelte.dev/docs#run-time-svelte-onmount + */ +function onMount(fn) { + get_current_component().$$.on_mount.push(fn); +} + +const dirty_components = []; +const binding_callbacks = []; +const render_callbacks = []; +const flush_callbacks = []; +const resolved_promise = Promise.resolve(); +let update_scheduled = false; +function schedule_update() { + if (!update_scheduled) { + update_scheduled = true; + resolved_promise.then(flush); + } +} +function tick() { + schedule_update(); + return resolved_promise; +} +function add_render_callback(fn) { + render_callbacks.push(fn); +} +// flush() calls callbacks in this order: +// 1. All beforeUpdate callbacks, in order: parents before children +// 2. All bind:this callbacks, in reverse order: children before parents. +// 3. All afterUpdate callbacks, in order: parents before children. EXCEPT +// for afterUpdates called during the initial onMount, which are called in +// reverse order: children before parents. +// Since callbacks might update component values, which could trigger another +// call to flush(), the following steps guard against this: +// 1. During beforeUpdate, any updated components will be added to the +// dirty_components array and will cause a reentrant call to flush(). Because +// the flush index is kept outside the function, the reentrant call will pick +// up where the earlier call left off and go through all dirty components. The +// current_component value is saved and restored so that the reentrant call will +// not interfere with the "parent" flush() call. +// 2. bind:this callbacks cannot trigger new flush() calls. +// 3. During afterUpdate, any updated components will NOT have their afterUpdate +// callback called a second time; the seen_callbacks set, outside the flush() +// function, guarantees this behavior. +const seen_callbacks = new Set(); +let flushidx = 0; // Do *not* move this inside the flush() function +function flush() { + // Do not reenter flush while dirty components are updated, as this can + // result in an infinite loop. Instead, let the inner flush handle it. + // Reentrancy is ok afterwards for bindings etc. + if (flushidx !== 0) { + return; + } + const saved_component = current_component; + do { + // first, call beforeUpdate functions + // and update components + try { + while (flushidx < dirty_components.length) { + const component = dirty_components[flushidx]; + flushidx++; + set_current_component(component); + update(component.$$); + } + } + catch (e) { + // reset dirty state to not end up in a deadlocked state and then rethrow + dirty_components.length = 0; + flushidx = 0; + throw e; + } + set_current_component(null); + dirty_components.length = 0; + flushidx = 0; + while (binding_callbacks.length) + binding_callbacks.pop()(); + // then, once components are updated, call + // afterUpdate functions. This may cause + // subsequent updates... + for (let i = 0; i < render_callbacks.length; i += 1) { + const callback = render_callbacks[i]; + if (!seen_callbacks.has(callback)) { + // ...so guard against infinite loops + seen_callbacks.add(callback); + callback(); + } + } + render_callbacks.length = 0; + } while (dirty_components.length); + while (flush_callbacks.length) { + flush_callbacks.pop()(); + } + update_scheduled = false; + seen_callbacks.clear(); + set_current_component(saved_component); +} +function update($$) { + if ($$.fragment !== null) { + $$.update(); + run_all($$.before_update); + const dirty = $$.dirty; + $$.dirty = [-1]; + $$.fragment && $$.fragment.p($$.ctx, dirty); + $$.after_update.forEach(add_render_callback); + } +} +const outroing = new Set(); +function transition_in(block, local) { + if (block && block.i) { + outroing.delete(block); + block.i(local); + } +} + +const globals = (typeof window !== 'undefined' + ? window + : typeof globalThis !== 'undefined' + ? globalThis + : global); + +function destroy_block(block, lookup) { + block.d(1); + lookup.delete(block.key); +} +function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) { + let o = old_blocks.length; + let n = list.length; + let i = o; + const old_indexes = {}; + while (i--) + old_indexes[old_blocks[i].key] = i; + const new_blocks = []; + const new_lookup = new Map(); + const deltas = new Map(); + i = n; + while (i--) { + const child_ctx = get_context(ctx, list, i); + const key = get_key(child_ctx); + let block = lookup.get(key); + if (!block) { + block = create_each_block(key, child_ctx); + block.c(); + } + else if (dynamic) { + block.p(child_ctx, dirty); + } + new_lookup.set(key, new_blocks[i] = block); + if (key in old_indexes) + deltas.set(key, Math.abs(i - old_indexes[key])); + } + const will_move = new Set(); + const did_move = new Set(); + function insert(block) { + transition_in(block, 1); + block.m(node, next); + lookup.set(block.key, block); + next = block.first; + n--; + } + while (o && n) { + const new_block = new_blocks[n - 1]; + const old_block = old_blocks[o - 1]; + const new_key = new_block.key; + const old_key = old_block.key; + if (new_block === old_block) { + // do nothing + next = new_block.first; + o--; + n--; + } + else if (!new_lookup.has(old_key)) { + // remove old block + destroy(old_block, lookup); + o--; + } + else if (!lookup.has(new_key) || will_move.has(new_key)) { + insert(new_block); + } + else if (did_move.has(old_key)) { + o--; + } + else if (deltas.get(new_key) > deltas.get(old_key)) { + did_move.add(new_key); + insert(new_block); + } + else { + will_move.add(old_key); + o--; + } + } + while (o--) { + const old_block = old_blocks[o]; + if (!new_lookup.has(old_block.key)) + destroy(old_block, lookup); + } + while (n) + insert(new_blocks[n - 1]); + return new_blocks; +} +function mount_component(component, target, anchor, customElement) { + const { fragment, after_update } = component.$$; + fragment && fragment.m(target, anchor); + if (!customElement) { + // onMount happens before the initial afterUpdate + add_render_callback(() => { + const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); + // if the component was destroyed immediately + // it will update the `$$.on_destroy` reference to `null`. + // the destructured on_destroy may still reference to the old array + if (component.$$.on_destroy) { + component.$$.on_destroy.push(...new_on_destroy); + } + else { + // Edge case - component was destroyed immediately, + // most likely as a result of a binding initialising + run_all(new_on_destroy); + } + component.$$.on_mount = []; + }); + } + after_update.forEach(add_render_callback); +} +function destroy_component(component, detaching) { + const $$ = component.$$; + if ($$.fragment !== null) { + run_all($$.on_destroy); + $$.fragment && $$.fragment.d(detaching); + // TODO null out other refs, including component.$$ (but need to + // preserve final state?) + $$.on_destroy = $$.fragment = null; + $$.ctx = []; + } +} +function make_dirty(component, i) { + if (component.$$.dirty[0] === -1) { + dirty_components.push(component); + schedule_update(); + component.$$.dirty.fill(0); + } + component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); +} +function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) { + const parent_component = current_component; + set_current_component(component); + const $$ = component.$$ = { + fragment: null, + ctx: [], + // state + props, + update: noop, + not_equal, + bound: blank_object(), + // lifecycle + on_mount: [], + on_destroy: [], + on_disconnect: [], + before_update: [], + after_update: [], + context: new Map((parent_component ? parent_component.$$.context : [])), + // everything else + callbacks: blank_object(), + dirty, + skip_bound: false, + root: options.target || parent_component.$$.root + }; + append_styles && append_styles($$.root); + let ready = false; + $$.ctx = instance + ? instance(component, options.props || {}, (i, ret, ...rest) => { + const value = rest.length ? rest[0] : ret; + if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { + if (!$$.skip_bound && $$.bound[i]) + $$.bound[i](value); + if (ready) + make_dirty(component, i); + } + return ret; + }) + : []; + $$.update(); + ready = true; + run_all($$.before_update); + // `false` as a special case of no DOM component + $$.fragment = create_fragment ? create_fragment($$.ctx) : false; + if (options.target) { + { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.c(); + } + mount_component(component, options.target, undefined, undefined); + flush(); + } + set_current_component(parent_component); +} +/** + * Base class for Svelte components. Used when dev=false. + */ +class SvelteComponent { + $destroy() { + destroy_component(this, 1); + this.$destroy = noop; + } + $on(type, callback) { + if (!is_function(callback)) { + return noop; + } + const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); + callbacks.push(callback); + return () => { + const index = callbacks.indexOf(callback); + if (index !== -1) + callbacks.splice(index, 1); + }; + } + $set($$props) { + if (this.$$set && !is_empty($$props)) { + this.$$.skip_bound = true; + this.$$set($$props); + this.$$.skip_bound = false; + } + } +} + +// via https://unpkg.com/browse/emojibase-data@6.0.0/meta/groups.json +const allGroups = [ + [-1, '✨', 'custom'], + [0, '😀', 'smileys-emotion'], + [1, '👋', 'people-body'], + [3, '🐱', 'animals-nature'], + [4, '🍎', 'food-drink'], + [5, '🏠️', 'travel-places'], + [6, '⚽', 'activities'], + [7, '📝', 'objects'], + [8, '⛔️', 'symbols'], + [9, '🏁', 'flags'] +].map(([id, emoji, name]) => ({ id, emoji, name })); + +const groups = allGroups.slice(1); +const customGroup = allGroups[0]; + +const MIN_SEARCH_TEXT_LENGTH = 2; +const NUM_SKIN_TONES = 6; + +/* istanbul ignore next */ +const rIC = typeof requestIdleCallback === 'function' ? requestIdleCallback : setTimeout; + +// check for ZWJ (zero width joiner) character +function hasZwj (emoji) { + return emoji.unicode.includes('\u200d') +} + +// Find one good representative emoji from each version to test by checking its color. +// Ideally it should have color in the center. For some inspiration, see: +// https://about.gitlab.com/blog/2018/05/30/journey-in-native-unicode-emoji/ +// +// Note that for certain versions (12.1, 13.1), there is no point in testing them explicitly, because +// all the emoji from this version are compound-emoji from previous versions. So they would pass a color +// test, even in browsers that display them as double emoji. (E.g. "face in clouds" might render as +// "face without mouth" plus "fog".) These emoji can only be filtered using the width test, +// which happens in checkZwjSupport.js. +const versionsAndTestEmoji = { + '🫠': 14, + '🥲': 13.1, // smiling face with tear, technically from v13 but see note above + '🥻': 12.1, // sari, technically from v12 but see note above + '🥰': 11, + '🤩': 5, + '👱‍♀️': 4, + '🤣': 3, + '👁️‍🗨️': 2, + '😀': 1, + '😐️': 0.7, + '😃': 0.6 +}; + +const TIMEOUT_BEFORE_LOADING_MESSAGE = 1000; // 1 second +const DEFAULT_SKIN_TONE_EMOJI = '🖐️'; +const DEFAULT_NUM_COLUMNS = 8; + +// Based on https://fivethirtyeight.com/features/the-100-most-used-emojis/ and +// https://blog.emojipedia.org/facebook-reveals-most-and-least-used-emojis/ with +// a bit of my own curation. (E.g. avoid the "OK" gesture because of connotations: +// https://emojipedia.org/ok-hand/) +const MOST_COMMONLY_USED_EMOJI = [ + '😊', + '😒', + '♥️', + '👍️', + '😍', + '😂', + '😭', + '☺️', + '😔', + '😩', + '😏', + '💕', + '🙌', + '😘' +]; + +// It's important to list Twemoji Mozilla before everything else, because Mozilla bundles their +// own font on some platforms (notably Windows and Linux as of this writing). Typically, Mozilla +// updates faster than the underlying OS, and we don't want to render older emoji in one font and +// newer emoji in another font: +// https://github.com/nolanlawson/emoji-picker-element/pull/268#issuecomment-1073347283 +const FONT_FAMILY = '"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",' + + '"Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif'; + +/* istanbul ignore next */ +const DEFAULT_CATEGORY_SORTING = (a, b) => a < b ? -1 : a > b ? 1 : 0; + +// Test if an emoji is supported by rendering it to canvas and checking that the color is not black + +const getTextFeature = (text, color) => { + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + + const ctx = canvas.getContext('2d'); + ctx.textBaseline = 'top'; + ctx.font = `100px ${FONT_FAMILY}`; + ctx.fillStyle = color; + ctx.scale(0.01, 0.01); + ctx.fillText(text, 0, 0); + + return ctx.getImageData(0, 0, 1, 1).data +}; + +const compareFeatures = (feature1, feature2) => { + const feature1Str = [...feature1].join(','); + const feature2Str = [...feature2].join(','); + // This is RGBA, so for 0,0,0, we are checking that the first RGB is not all zeroes. + // Most of the time when unsupported this is 0,0,0,0, but on Chrome on Mac it is + // 0,0,0,61 - there is a transparency here. + return feature1Str === feature2Str && !feature1Str.startsWith('0,0,0,') +}; + +function testColorEmojiSupported (text) { + // Render white and black and then compare them to each other and ensure they're the same + // color, and neither one is black. This shows that the emoji was rendered in color. + const feature1 = getTextFeature(text, '#000'); + const feature2 = getTextFeature(text, '#fff'); + return feature1 && feature2 && compareFeatures(feature1, feature2) +} + +// rather than check every emoji ever, which would be expensive, just check some representatives from the + +function determineEmojiSupportLevel () { + const entries = Object.entries(versionsAndTestEmoji); + try { + // start with latest emoji and work backwards + for (const [emoji, version] of entries) { + if (testColorEmojiSupported(emoji)) { + return version + } + } + } catch (e) { // canvas error + } finally { + } + // In case of an error, be generous and just assume all emoji are supported (e.g. for canvas errors + // due to anti-fingerprinting add-ons). Better to show some gray boxes than nothing at all. + return entries[0][1] // first one in the list is the most recent version +} + +// Check which emojis we know for sure aren't supported, based on Unicode version level +let promise; +const detectEmojiSupportLevel = () => { + if (!promise) { + // Delay so it can run while the IDB database is being created by the browser (on another thread). + // This helps especially with first load – we want to start pre-populating the database on the main thread, + // and then wait for IDB to commit everything, and while waiting we run this check. + promise = new Promise(resolve => ( + rIC(() => ( + resolve(determineEmojiSupportLevel()) // delay so ideally this can run while IDB is first populating + )) + )); + } + return promise +}; +// determine which emojis containing ZWJ (zero width joiner) characters +// are supported (rendered as one glyph) rather than unsupported (rendered as two or more glyphs) +const supportedZwjEmojis = new Map(); + +const VARIATION_SELECTOR = '\ufe0f'; +const SKINTONE_MODIFIER = '\ud83c'; +const ZWJ = '\u200d'; +const LIGHT_SKIN_TONE = 0x1F3FB; +const LIGHT_SKIN_TONE_MODIFIER = 0xdffb; + +// TODO: this is a naive implementation, we can improve it later +// It's only used for the skintone picker, so as long as people don't customize with +// really exotic emoji then it should work fine +function applySkinTone (str, skinTone) { + if (skinTone === 0) { + return str + } + const zwjIndex = str.indexOf(ZWJ); + if (zwjIndex !== -1) { + return str.substring(0, zwjIndex) + + String.fromCodePoint(LIGHT_SKIN_TONE + skinTone - 1) + + str.substring(zwjIndex) + } + if (str.endsWith(VARIATION_SELECTOR)) { + str = str.substring(0, str.length - 1); + } + return str + SKINTONE_MODIFIER + String.fromCodePoint(LIGHT_SKIN_TONE_MODIFIER + skinTone - 1) +} + +function halt (event) { + event.preventDefault(); + event.stopPropagation(); +} + +// Implementation left/right or up/down navigation, circling back when you +// reach the start/end of the list +function incrementOrDecrement (decrement, val, arr) { + val += (decrement ? -1 : 1); + if (val < 0) { + val = arr.length - 1; + } else if (val >= arr.length) { + val = 0; + } + return val +} + +// like lodash's uniqBy but much smaller +function uniqBy (arr, func) { + const set = new Set(); + const res = []; + for (const item of arr) { + const key = func(item); + if (!set.has(key)) { + set.add(key); + res.push(item); + } + } + return res +} + +// We don't need all the data on every emoji, and there are specific things we need +// for the UI, so build a "view model" from the emoji object we got from the database + +function summarizeEmojisForUI (emojis, emojiSupportLevel) { + const toSimpleSkinsMap = skins => { + const res = {}; + for (const skin of skins) { + // ignore arrays like [1, 2] with multiple skin tones + // also ignore variants that are in an unsupported emoji version + // (these do exist - variants from a different version than their base emoji) + if (typeof skin.tone === 'number' && skin.version <= emojiSupportLevel) { + res[skin.tone] = skin.unicode; + } + } + return res + }; + + return emojis.map(({ unicode, skins, shortcodes, url, name, category }) => ({ + unicode, + name, + shortcodes, + url, + category, + id: unicode || name, + skins: skins && toSimpleSkinsMap(skins), + title: (shortcodes || []).join(', ') + })) +} + +// import rAF from one place so that the bundle size is a bit smaller +const rAF = requestAnimationFrame; + +// Svelte action to calculate the width of an element and auto-update + +let resizeObserverSupported = typeof ResizeObserver === 'function'; + +function calculateWidth (node, onUpdate) { + let resizeObserver; + if (resizeObserverSupported) { + resizeObserver = new ResizeObserver(entries => ( + onUpdate(entries[0].contentRect.width) + )); + resizeObserver.observe(node); + } else { // just set the width once, don't bother trying to track it + rAF(() => ( + onUpdate(node.getBoundingClientRect().width) + )); + } + + // cleanup function (called on destroy) + return { + destroy () { + if (resizeObserver) { + resizeObserver.disconnect(); + } + } + } +} + +// get the width of the text inside of a DOM node, via https://stackoverflow.com/a/59525891/680742 +function calculateTextWidth (node) { + /* istanbul ignore else */ + { + const range = document.createRange(); + range.selectNode(node.firstChild); + return range.getBoundingClientRect().width + } +} + +let baselineEmojiWidth; + +function checkZwjSupport (zwjEmojisToCheck, baselineEmoji, emojiToDomNode) { + for (const emoji of zwjEmojisToCheck) { + const domNode = emojiToDomNode(emoji); + const emojiWidth = calculateTextWidth(domNode); + if (typeof baselineEmojiWidth === 'undefined') { // calculate the baseline emoji width only once + baselineEmojiWidth = calculateTextWidth(baselineEmoji); + } + // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard + // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with + // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.) + // So here we set the threshold at 1.8 times the size of the baseline emoji. + const supported = emojiWidth / 1.8 < baselineEmojiWidth; + supportedZwjEmojis.set(emoji.unicode, supported); + } +} + +// Measure after style/layout are complete + +const requestPostAnimationFrame = callback => { + rAF(() => { + setTimeout(callback); + }); +}; + +// like lodash's uniq + +function uniq (arr) { + return uniqBy(arr, _ => _) +} + +// Note we put this in its own function outside Picker.js to avoid Svelte doing an invalidation on the "setter" here. +// At best the invalidation is useless, at worst it can cause infinite loops: +// https://github.com/nolanlawson/emoji-picker-element/pull/180 +// https://github.com/sveltejs/svelte/issues/6521 +// Also note tabpanelElement can be null if the element is disconnected immediately after connected +function resetScrollTopIfPossible (element) { + if (element) { + element.scrollTop = 0; + } +} + +/* src/picker/components/Picker/Picker.svelte generated by Svelte v3.55.1 */ + +const { Map: Map_1 } = globals; + +function get_each_context(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[64] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +function get_each_context_1(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[67] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +function get_each_context_2(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[64] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +function get_each_context_3(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[70] = list[i]; + return child_ctx; +} + +function get_each_context_4(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[73] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +// (43:38) {#each skinTones as skinTone, i (skinTone)} +function create_each_block_4(key_1, ctx) { + let div; + let t_value = /*skinTone*/ ctx[73] + ""; + let t; + let div_id_value; + let div_class_value; + let div_aria_selected_value; + let div_title_value; + let div_aria_label_value; + + return { + key: key_1, + first: null, + c() { + div = element("div"); + t = text(t_value); + attr(div, "id", div_id_value = "skintone-" + /*i*/ ctx[66]); + + attr(div, "class", div_class_value = "emoji hide-focus " + (/*i*/ ctx[66] === /*activeSkinTone*/ ctx[20] + ? 'active' + : '')); + + attr(div, "aria-selected", div_aria_selected_value = /*i*/ ctx[66] === /*activeSkinTone*/ ctx[20]); + attr(div, "role", "option"); + attr(div, "title", div_title_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]]); + attr(div, "tabindex", "-1"); + attr(div, "aria-label", div_aria_label_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]]); + this.first = div; + }, + m(target, anchor) { + insert(target, div, anchor); + append(div, t); + }, + p(new_ctx, dirty) { + ctx = new_ctx; + if (dirty[0] & /*skinTones*/ 512 && t_value !== (t_value = /*skinTone*/ ctx[73] + "")) set_data(t, t_value); + + if (dirty[0] & /*skinTones*/ 512 && div_id_value !== (div_id_value = "skintone-" + /*i*/ ctx[66])) { + attr(div, "id", div_id_value); + } + + if (dirty[0] & /*skinTones, activeSkinTone*/ 1049088 && div_class_value !== (div_class_value = "emoji hide-focus " + (/*i*/ ctx[66] === /*activeSkinTone*/ ctx[20] + ? 'active' + : ''))) { + attr(div, "class", div_class_value); + } + + if (dirty[0] & /*skinTones, activeSkinTone*/ 1049088 && div_aria_selected_value !== (div_aria_selected_value = /*i*/ ctx[66] === /*activeSkinTone*/ ctx[20])) { + attr(div, "aria-selected", div_aria_selected_value); + } + + if (dirty[0] & /*i18n, skinTones*/ 513 && div_title_value !== (div_title_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]])) { + attr(div, "title", div_title_value); + } + + if (dirty[0] & /*i18n, skinTones*/ 513 && div_aria_label_value !== (div_aria_label_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]])) { + attr(div, "aria-label", div_aria_label_value); + } + }, + d(detaching) { + if (detaching) detach(div); + } + }; +} + +// (53:33) {#each groups as group (group.id)} +function create_each_block_3(key_1, ctx) { + let button; + let div; + let t_value = /*group*/ ctx[70].emoji + ""; + let t; + let button_aria_controls_value; + let button_aria_label_value; + let button_aria_selected_value; + let button_title_value; + let mounted; + let dispose; + + function click_handler() { + return /*click_handler*/ ctx[50](/*group*/ ctx[70]); + } + + return { + key: key_1, + first: null, + c() { + button = element("button"); + div = element("div"); + t = text(t_value); + attr(div, "class", "nav-emoji emoji"); + attr(button, "role", "tab"); + attr(button, "class", "nav-button"); + attr(button, "aria-controls", button_aria_controls_value = "tab-" + /*group*/ ctx[70].id); + attr(button, "aria-label", button_aria_label_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name]); + attr(button, "aria-selected", button_aria_selected_value = !/*searchMode*/ ctx[4] && /*currentGroup*/ ctx[13].id === /*group*/ ctx[70].id); + attr(button, "title", button_title_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name]); + this.first = button; + }, + m(target, anchor) { + insert(target, button, anchor); + append(button, div); + append(div, t); + + if (!mounted) { + dispose = listen(button, "click", click_handler); + mounted = true; + } + }, + p(new_ctx, dirty) { + ctx = new_ctx; + if (dirty[0] & /*groups*/ 4096 && t_value !== (t_value = /*group*/ ctx[70].emoji + "")) set_data(t, t_value); + + if (dirty[0] & /*groups*/ 4096 && button_aria_controls_value !== (button_aria_controls_value = "tab-" + /*group*/ ctx[70].id)) { + attr(button, "aria-controls", button_aria_controls_value); + } + + if (dirty[0] & /*i18n, groups*/ 4097 && button_aria_label_value !== (button_aria_label_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name])) { + attr(button, "aria-label", button_aria_label_value); + } + + if (dirty[0] & /*searchMode, currentGroup, groups*/ 12304 && button_aria_selected_value !== (button_aria_selected_value = !/*searchMode*/ ctx[4] && /*currentGroup*/ ctx[13].id === /*group*/ ctx[70].id)) { + attr(button, "aria-selected", button_aria_selected_value); + } + + if (dirty[0] & /*i18n, groups*/ 4097 && button_title_value !== (button_title_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name])) { + attr(button, "title", button_title_value); + } + }, + d(detaching) { + if (detaching) detach(button); + mounted = false; + dispose(); + } + }; +} + +// (94:100) {:else} +function create_else_block_1(ctx) { + let img; + let img_src_value; + + return { + c() { + img = element("img"); + attr(img, "class", "custom-emoji"); + if (!src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) attr(img, "src", img_src_value); + attr(img, "alt", ""); + attr(img, "loading", "lazy"); + }, + m(target, anchor) { + insert(target, img, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && !src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) { + attr(img, "src", img_src_value); + } + }, + d(detaching) { + if (detaching) detach(img); + } + }; +} + +// (94:40) {#if emoji.unicode} +function create_if_block_1(ctx) { + let t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + ""; + let t; + + return { + c() { + t = text(t_value); + }, + m(target, anchor) { + insert(target, t, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentEmojisWithCategories, currentSkinTone*/ 33024 && t_value !== (t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + "")) set_data(t, t_value); + }, + d(detaching) { + if (detaching) detach(t); + } + }; +} + +// (89:53) {#each emojiWithCategory.emojis as emoji, i (emoji.id)} +function create_each_block_2(key_1, ctx) { + let button; + let button_role_value; + let button_aria_selected_value; + let button_aria_label_value; + let button_title_value; + let button_class_value; + let button_id_value; + + function select_block_type(ctx, dirty) { + if (/*emoji*/ ctx[64].unicode) return create_if_block_1; + return create_else_block_1; + } + + let current_block_type = select_block_type(ctx); + let if_block = current_block_type(ctx); + + return { + key: key_1, + first: null, + c() { + button = element("button"); + if_block.c(); + attr(button, "role", button_role_value = /*searchMode*/ ctx[4] ? 'option' : 'menuitem'); + + attr(button, "aria-selected", button_aria_selected_value = /*searchMode*/ ctx[4] + ? /*i*/ ctx[66] == /*activeSearchItem*/ ctx[5] + : ''); + + attr(button, "aria-label", button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8])); + attr(button, "title", button_title_value = /*emoji*/ ctx[64].title); + + attr(button, "class", button_class_value = "emoji " + (/*searchMode*/ ctx[4] && /*i*/ ctx[66] === /*activeSearchItem*/ ctx[5] + ? 'active' + : '')); + + attr(button, "id", button_id_value = "emo-" + /*emoji*/ ctx[64].id); + this.first = button; + }, + m(target, anchor) { + insert(target, button, anchor); + if_block.m(button, null); + }, + p(new_ctx, dirty) { + ctx = new_ctx; + + if (current_block_type === (current_block_type = select_block_type(ctx)) && if_block) { + if_block.p(ctx, dirty); + } else { + if_block.d(1); + if_block = current_block_type(ctx); + + if (if_block) { + if_block.c(); + if_block.m(button, null); + } + } + + if (dirty[0] & /*searchMode*/ 16 && button_role_value !== (button_role_value = /*searchMode*/ ctx[4] ? 'option' : 'menuitem')) { + attr(button, "role", button_role_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem*/ 32816 && button_aria_selected_value !== (button_aria_selected_value = /*searchMode*/ ctx[4] + ? /*i*/ ctx[66] == /*activeSearchItem*/ ctx[5] + : '')) { + attr(button, "aria-selected", button_aria_selected_value); + } + + if (dirty[0] & /*currentEmojisWithCategories, currentSkinTone*/ 33024 && button_aria_label_value !== (button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]))) { + attr(button, "aria-label", button_aria_label_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && button_title_value !== (button_title_value = /*emoji*/ ctx[64].title)) { + attr(button, "title", button_title_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem*/ 32816 && button_class_value !== (button_class_value = "emoji " + (/*searchMode*/ ctx[4] && /*i*/ ctx[66] === /*activeSearchItem*/ ctx[5] + ? 'active' + : ''))) { + attr(button, "class", button_class_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && button_id_value !== (button_id_value = "emo-" + /*emoji*/ ctx[64].id)) { + attr(button, "id", button_id_value); + } + }, + d(detaching) { + if (detaching) detach(button); + if_block.d(); + } + }; +} + +// (70:36) {#each currentEmojisWithCategories as emojiWithCategory, i (emojiWithCategory.category)} +function create_each_block_1(key_1, ctx) { + let div0; + + let t_value = (/*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*emojiWithCategory*/ ctx[67].category + ? /*emojiWithCategory*/ ctx[67].category + : /*currentEmojisWithCategories*/ ctx[15].length > 1 + ? /*i18n*/ ctx[0].categories.custom + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name]) + ""; + + let t; + let div0_id_value; + let div0_class_value; + let div1; + let each_blocks = []; + let each_1_lookup = new Map_1(); + let div1_role_value; + let div1_aria_labelledby_value; + let div1_id_value; + let each_value_2 = /*emojiWithCategory*/ ctx[67].emojis; + const get_key = ctx => /*emoji*/ ctx[64].id; + + for (let i = 0; i < each_value_2.length; i += 1) { + let child_ctx = get_each_context_2(ctx, each_value_2, i); + let key = get_key(child_ctx); + each_1_lookup.set(key, each_blocks[i] = create_each_block_2(key, child_ctx)); + } + + return { + key: key_1, + first: null, + c() { + div0 = element("div"); + t = text(t_value); + div1 = element("div"); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].c(); + } + + attr(div0, "id", div0_id_value = "menu-label-" + /*i*/ ctx[66]); + + attr(div0, "class", div0_class_value = "category " + (/*currentEmojisWithCategories*/ ctx[15].length === 1 && /*currentEmojisWithCategories*/ ctx[15][0].category === '' + ? 'gone' + : '')); + + attr(div0, "aria-hidden", "true"); + attr(div1, "class", "emoji-menu"); + attr(div1, "role", div1_role_value = /*searchMode*/ ctx[4] ? 'listbox' : 'menu'); + attr(div1, "aria-labelledby", div1_aria_labelledby_value = "menu-label-" + /*i*/ ctx[66]); + attr(div1, "id", div1_id_value = /*searchMode*/ ctx[4] ? 'search-results' : ''); + this.first = div0; + }, + m(target, anchor) { + insert(target, div0, anchor); + append(div0, t); + insert(target, div1, anchor); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].m(div1, null); + } + }, + p(new_ctx, dirty) { + ctx = new_ctx; + + if (dirty[0] & /*searchMode, i18n, currentEmojisWithCategories, currentGroup*/ 40977 && t_value !== (t_value = (/*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*emojiWithCategory*/ ctx[67].category + ? /*emojiWithCategory*/ ctx[67].category + : /*currentEmojisWithCategories*/ ctx[15].length > 1 + ? /*i18n*/ ctx[0].categories.custom + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name]) + "")) set_data(t, t_value); + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && div0_id_value !== (div0_id_value = "menu-label-" + /*i*/ ctx[66])) { + attr(div0, "id", div0_id_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && div0_class_value !== (div0_class_value = "category " + (/*currentEmojisWithCategories*/ ctx[15].length === 1 && /*currentEmojisWithCategories*/ ctx[15][0].category === '' + ? 'gone' + : ''))) { + attr(div0, "class", div0_class_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem, labelWithSkin, currentSkinTone, unicodeWithSkin*/ 402686256) { + each_value_2 = /*emojiWithCategory*/ ctx[67].emojis; + each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value_2, each_1_lookup, div1, destroy_block, create_each_block_2, null, get_each_context_2); + } + + if (dirty[0] & /*searchMode*/ 16 && div1_role_value !== (div1_role_value = /*searchMode*/ ctx[4] ? 'listbox' : 'menu')) { + attr(div1, "role", div1_role_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && div1_aria_labelledby_value !== (div1_aria_labelledby_value = "menu-label-" + /*i*/ ctx[66])) { + attr(div1, "aria-labelledby", div1_aria_labelledby_value); + } + + if (dirty[0] & /*searchMode*/ 16 && div1_id_value !== (div1_id_value = /*searchMode*/ ctx[4] ? 'search-results' : '')) { + attr(div1, "id", div1_id_value); + } + }, + d(detaching) { + if (detaching) detach(div0); + if (detaching) detach(div1); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].d(); + } + } + }; +} + +// (103:94) {:else} +function create_else_block(ctx) { + let img; + let img_src_value; + + return { + c() { + img = element("img"); + attr(img, "class", "custom-emoji"); + if (!src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) attr(img, "src", img_src_value); + attr(img, "alt", ""); + attr(img, "loading", "lazy"); + }, + m(target, anchor) { + insert(target, img, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentFavorites*/ 1024 && !src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) { + attr(img, "src", img_src_value); + } + }, + d(detaching) { + if (detaching) detach(img); + } + }; +} + +// (103:34) {#if emoji.unicode} +function create_if_block(ctx) { + let t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + ""; + let t; + + return { + c() { + t = text(t_value); + }, + m(target, anchor) { + insert(target, t, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentFavorites, currentSkinTone*/ 1280 && t_value !== (t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + "")) set_data(t, t_value); + }, + d(detaching) { + if (detaching) detach(t); + } + }; +} + +// (99:102) {#each currentFavorites as emoji, i (emoji.id)} +function create_each_block(key_1, ctx) { + let button; + let button_aria_label_value; + let button_title_value; + let button_id_value; + + function select_block_type_1(ctx, dirty) { + if (/*emoji*/ ctx[64].unicode) return create_if_block; + return create_else_block; + } + + let current_block_type = select_block_type_1(ctx); + let if_block = current_block_type(ctx); + + return { + key: key_1, + first: null, + c() { + button = element("button"); + if_block.c(); + attr(button, "role", "menuitem"); + attr(button, "aria-label", button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8])); + attr(button, "title", button_title_value = /*emoji*/ ctx[64].title); + attr(button, "class", "emoji"); + attr(button, "id", button_id_value = "fav-" + /*emoji*/ ctx[64].id); + this.first = button; + }, + m(target, anchor) { + insert(target, button, anchor); + if_block.m(button, null); + }, + p(new_ctx, dirty) { + ctx = new_ctx; + + if (current_block_type === (current_block_type = select_block_type_1(ctx)) && if_block) { + if_block.p(ctx, dirty); + } else { + if_block.d(1); + if_block = current_block_type(ctx); + + if (if_block) { + if_block.c(); + if_block.m(button, null); + } + } + + if (dirty[0] & /*currentFavorites, currentSkinTone*/ 1280 && button_aria_label_value !== (button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]))) { + attr(button, "aria-label", button_aria_label_value); + } + + if (dirty[0] & /*currentFavorites*/ 1024 && button_title_value !== (button_title_value = /*emoji*/ ctx[64].title)) { + attr(button, "title", button_title_value); + } + + if (dirty[0] & /*currentFavorites*/ 1024 && button_id_value !== (button_id_value = "fav-" + /*emoji*/ ctx[64].id)) { + attr(button, "id", button_id_value); + } + }, + d(detaching) { + if (detaching) detach(button); + if_block.d(); + } + }; +} + +function create_fragment(ctx) { + let section; + let div0; + let div4; + let div1; + let input; + let input_placeholder_value; + let input_aria_expanded_value; + let input_aria_activedescendant_value; + let label; + let t0_value = /*i18n*/ ctx[0].searchLabel + ""; + let t0; + let span0; + let t1_value = /*i18n*/ ctx[0].searchDescription + ""; + let t1; + let div2; + let button0; + let t2; + let button0_class_value; + let div2_class_value; + let span1; + let t3_value = /*i18n*/ ctx[0].skinToneDescription + ""; + let t3; + let div3; + let each_blocks_3 = []; + let each0_lookup = new Map_1(); + let div3_class_value; + let div3_aria_label_value; + let div3_aria_activedescendant_value; + let div3_aria_hidden_value; + let div5; + let each_blocks_2 = []; + let each1_lookup = new Map_1(); + let div5_aria_label_value; + let div7; + let div6; + let div8; + let t4; + let div8_class_value; + let div10; + let div9; + let each_blocks_1 = []; + let each2_lookup = new Map_1(); + let div10_class_value; + let div10_role_value; + let div10_aria_label_value; + let div10_id_value; + let div11; + let each_blocks = []; + let each3_lookup = new Map_1(); + let div11_class_value; + let div11_aria_label_value; + let button1; + let section_aria_label_value; + let mounted; + let dispose; + let each_value_4 = /*skinTones*/ ctx[9]; + const get_key = ctx => /*skinTone*/ ctx[73]; + + for (let i = 0; i < each_value_4.length; i += 1) { + let child_ctx = get_each_context_4(ctx, each_value_4, i); + let key = get_key(child_ctx); + each0_lookup.set(key, each_blocks_3[i] = create_each_block_4(key, child_ctx)); + } + + let each_value_3 = /*groups*/ ctx[12]; + const get_key_1 = ctx => /*group*/ ctx[70].id; + + for (let i = 0; i < each_value_3.length; i += 1) { + let child_ctx = get_each_context_3(ctx, each_value_3, i); + let key = get_key_1(child_ctx); + each1_lookup.set(key, each_blocks_2[i] = create_each_block_3(key, child_ctx)); + } + + let each_value_1 = /*currentEmojisWithCategories*/ ctx[15]; + const get_key_2 = ctx => /*emojiWithCategory*/ ctx[67].category; + + for (let i = 0; i < each_value_1.length; i += 1) { + let child_ctx = get_each_context_1(ctx, each_value_1, i); + let key = get_key_2(child_ctx); + each2_lookup.set(key, each_blocks_1[i] = create_each_block_1(key, child_ctx)); + } + + let each_value = /*currentFavorites*/ ctx[10]; + const get_key_3 = ctx => /*emoji*/ ctx[64].id; + + for (let i = 0; i < each_value.length; i += 1) { + let child_ctx = get_each_context(ctx, each_value, i); + let key = get_key_3(child_ctx); + each3_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx)); + } + + return { + c() { + section = element("section"); + div0 = element("div"); + div4 = element("div"); + div1 = element("div"); + input = element("input"); + label = element("label"); + t0 = text(t0_value); + span0 = element("span"); + t1 = text(t1_value); + div2 = element("div"); + button0 = element("button"); + t2 = text(/*skinToneButtonText*/ ctx[21]); + span1 = element("span"); + t3 = text(t3_value); + div3 = element("div"); + + for (let i = 0; i < each_blocks_3.length; i += 1) { + each_blocks_3[i].c(); + } + + div5 = element("div"); + + for (let i = 0; i < each_blocks_2.length; i += 1) { + each_blocks_2[i].c(); + } + + div7 = element("div"); + div6 = element("div"); + div8 = element("div"); + t4 = text(/*message*/ ctx[18]); + div10 = element("div"); + div9 = element("div"); + + for (let i = 0; i < each_blocks_1.length; i += 1) { + each_blocks_1[i].c(); + } + + div11 = element("div"); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].c(); + } + + button1 = element("button"); + button1.textContent = "😀"; + attr(div0, "class", "pad-top"); + attr(input, "id", "search"); + attr(input, "class", "search"); + attr(input, "type", "search"); + attr(input, "role", "combobox"); + attr(input, "enterkeyhint", "search"); + attr(input, "placeholder", input_placeholder_value = /*i18n*/ ctx[0].searchLabel); + attr(input, "autocapitalize", "none"); + attr(input, "autocomplete", "off"); + attr(input, "spellcheck", "true"); + attr(input, "aria-expanded", input_aria_expanded_value = !!(/*searchMode*/ ctx[4] && /*currentEmojis*/ ctx[1].length)); + attr(input, "aria-controls", "search-results"); + attr(input, "aria-describedby", "search-description"); + attr(input, "aria-autocomplete", "list"); + + attr(input, "aria-activedescendant", input_aria_activedescendant_value = /*activeSearchItemId*/ ctx[26] + ? `emo-${/*activeSearchItemId*/ ctx[26]}` + : ''); + + attr(label, "class", "sr-only"); + attr(label, "for", "search"); + attr(span0, "id", "search-description"); + attr(span0, "class", "sr-only"); + attr(div1, "class", "search-wrapper"); + attr(button0, "id", "skintone-button"); + attr(button0, "class", button0_class_value = "emoji " + (/*skinTonePickerExpanded*/ ctx[6] ? 'hide-focus' : '')); + attr(button0, "aria-label", /*skinToneButtonLabel*/ ctx[23]); + attr(button0, "title", /*skinToneButtonLabel*/ ctx[23]); + attr(button0, "aria-describedby", "skintone-description"); + attr(button0, "aria-haspopup", "listbox"); + attr(button0, "aria-expanded", /*skinTonePickerExpanded*/ ctx[6]); + attr(button0, "aria-controls", "skintone-list"); + + attr(div2, "class", div2_class_value = "skintone-button-wrapper " + (/*skinTonePickerExpandedAfterAnimation*/ ctx[19] + ? 'expanded' + : '')); + + attr(span1, "id", "skintone-description"); + attr(span1, "class", "sr-only"); + attr(div3, "id", "skintone-list"); + + attr(div3, "class", div3_class_value = "skintone-list " + (/*skinTonePickerExpanded*/ ctx[6] + ? '' + : 'hidden no-animate')); + + set_style(div3, "transform", "translateY(" + (/*skinTonePickerExpanded*/ ctx[6] + ? 0 + : 'calc(-1 * var(--num-skintones) * var(--total-emoji-size))') + ")"); + + attr(div3, "role", "listbox"); + attr(div3, "aria-label", div3_aria_label_value = /*i18n*/ ctx[0].skinTonesLabel); + attr(div3, "aria-activedescendant", div3_aria_activedescendant_value = "skintone-" + /*activeSkinTone*/ ctx[20]); + attr(div3, "aria-hidden", div3_aria_hidden_value = !/*skinTonePickerExpanded*/ ctx[6]); + attr(div4, "class", "search-row"); + attr(div5, "class", "nav"); + attr(div5, "role", "tablist"); + set_style(div5, "grid-template-columns", "repeat(" + /*groups*/ ctx[12].length + ", 1fr)"); + attr(div5, "aria-label", div5_aria_label_value = /*i18n*/ ctx[0].categoriesLabel); + attr(div6, "class", "indicator"); + set_style(div6, "transform", "translateX(" + (/*isRtl*/ ctx[24] ? -1 : 1) * /*currentGroupIndex*/ ctx[11] * 100 + "%)"); + attr(div7, "class", "indicator-wrapper"); + attr(div8, "class", div8_class_value = "message " + (/*message*/ ctx[18] ? '' : 'gone')); + attr(div8, "role", "alert"); + attr(div8, "aria-live", "polite"); + + attr(div10, "class", div10_class_value = "tabpanel " + (!/*databaseLoaded*/ ctx[14] || /*message*/ ctx[18] + ? 'gone' + : '')); + + attr(div10, "role", div10_role_value = /*searchMode*/ ctx[4] ? 'region' : 'tabpanel'); + + attr(div10, "aria-label", div10_aria_label_value = /*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name]); + + attr(div10, "id", div10_id_value = /*searchMode*/ ctx[4] + ? '' + : `tab-${/*currentGroup*/ ctx[13].id}`); + + attr(div10, "tabindex", "0"); + attr(div11, "class", div11_class_value = "favorites emoji-menu " + (/*message*/ ctx[18] ? 'gone' : '')); + attr(div11, "role", "menu"); + attr(div11, "aria-label", div11_aria_label_value = /*i18n*/ ctx[0].favoritesLabel); + set_style(div11, "padding-inline-end", /*scrollbarWidth*/ ctx[25] + "px"); + attr(button1, "aria-hidden", "true"); + attr(button1, "tabindex", "-1"); + attr(button1, "class", "abs-pos hidden emoji"); + attr(section, "class", "picker"); + attr(section, "aria-label", section_aria_label_value = /*i18n*/ ctx[0].regionLabel); + attr(section, "style", /*pickerStyle*/ ctx[22]); + }, + m(target, anchor) { + insert(target, section, anchor); + append(section, div0); + append(section, div4); + append(div4, div1); + append(div1, input); + set_input_value(input, /*rawSearchText*/ ctx[2]); + append(div1, label); + append(label, t0); + append(div1, span0); + append(span0, t1); + append(div4, div2); + append(div2, button0); + append(button0, t2); + append(div4, span1); + append(span1, t3); + append(div4, div3); + + for (let i = 0; i < each_blocks_3.length; i += 1) { + each_blocks_3[i].m(div3, null); + } + + /*div3_binding*/ ctx[49](div3); + append(section, div5); + + for (let i = 0; i < each_blocks_2.length; i += 1) { + each_blocks_2[i].m(div5, null); + } + + append(section, div7); + append(div7, div6); + append(section, div8); + append(div8, t4); + append(section, div10); + append(div10, div9); + + for (let i = 0; i < each_blocks_1.length; i += 1) { + each_blocks_1[i].m(div9, null); + } + + /*div10_binding*/ ctx[51](div10); + append(section, div11); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].m(div11, null); + } + + append(section, button1); + /*button1_binding*/ ctx[52](button1); + /*section_binding*/ ctx[53](section); + + if (!mounted) { + dispose = [ + listen(input, "input", /*input_input_handler*/ ctx[48]), + listen(input, "keydown", /*onSearchKeydown*/ ctx[30]), + listen(button0, "click", /*onClickSkinToneButton*/ ctx[35]), + listen(div3, "focusout", /*onSkinToneOptionsFocusOut*/ ctx[38]), + listen(div3, "click", /*onSkinToneOptionsClick*/ ctx[34]), + listen(div3, "keydown", /*onSkinToneOptionsKeydown*/ ctx[36]), + listen(div3, "keyup", /*onSkinToneOptionsKeyup*/ ctx[37]), + listen(div5, "keydown", /*onNavKeydown*/ ctx[32]), + action_destroyer(/*calculateEmojiGridStyle*/ ctx[29].call(null, div9)), + listen(div10, "click", /*onEmojiClick*/ ctx[33]), + listen(div11, "click", /*onEmojiClick*/ ctx[33]) + ]; + + mounted = true; + } + }, + p(ctx, dirty) { + if (dirty[0] & /*i18n*/ 1 && input_placeholder_value !== (input_placeholder_value = /*i18n*/ ctx[0].searchLabel)) { + attr(input, "placeholder", input_placeholder_value); + } + + if (dirty[0] & /*searchMode, currentEmojis*/ 18 && input_aria_expanded_value !== (input_aria_expanded_value = !!(/*searchMode*/ ctx[4] && /*currentEmojis*/ ctx[1].length))) { + attr(input, "aria-expanded", input_aria_expanded_value); + } + + if (dirty[0] & /*activeSearchItemId*/ 67108864 && input_aria_activedescendant_value !== (input_aria_activedescendant_value = /*activeSearchItemId*/ ctx[26] + ? `emo-${/*activeSearchItemId*/ ctx[26]}` + : '')) { + attr(input, "aria-activedescendant", input_aria_activedescendant_value); + } + + if (dirty[0] & /*rawSearchText*/ 4) { + set_input_value(input, /*rawSearchText*/ ctx[2]); + } + + if (dirty[0] & /*i18n*/ 1 && t0_value !== (t0_value = /*i18n*/ ctx[0].searchLabel + "")) set_data(t0, t0_value); + if (dirty[0] & /*i18n*/ 1 && t1_value !== (t1_value = /*i18n*/ ctx[0].searchDescription + "")) set_data(t1, t1_value); + if (dirty[0] & /*skinToneButtonText*/ 2097152) set_data(t2, /*skinToneButtonText*/ ctx[21]); + + if (dirty[0] & /*skinTonePickerExpanded*/ 64 && button0_class_value !== (button0_class_value = "emoji " + (/*skinTonePickerExpanded*/ ctx[6] ? 'hide-focus' : ''))) { + attr(button0, "class", button0_class_value); + } + + if (dirty[0] & /*skinToneButtonLabel*/ 8388608) { + attr(button0, "aria-label", /*skinToneButtonLabel*/ ctx[23]); + } + + if (dirty[0] & /*skinToneButtonLabel*/ 8388608) { + attr(button0, "title", /*skinToneButtonLabel*/ ctx[23]); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64) { + attr(button0, "aria-expanded", /*skinTonePickerExpanded*/ ctx[6]); + } + + if (dirty[0] & /*skinTonePickerExpandedAfterAnimation*/ 524288 && div2_class_value !== (div2_class_value = "skintone-button-wrapper " + (/*skinTonePickerExpandedAfterAnimation*/ ctx[19] + ? 'expanded' + : ''))) { + attr(div2, "class", div2_class_value); + } + + if (dirty[0] & /*i18n*/ 1 && t3_value !== (t3_value = /*i18n*/ ctx[0].skinToneDescription + "")) set_data(t3, t3_value); + + if (dirty[0] & /*skinTones, activeSkinTone, i18n*/ 1049089) { + each_value_4 = /*skinTones*/ ctx[9]; + each_blocks_3 = update_keyed_each(each_blocks_3, dirty, get_key, 1, ctx, each_value_4, each0_lookup, div3, destroy_block, create_each_block_4, null, get_each_context_4); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64 && div3_class_value !== (div3_class_value = "skintone-list " + (/*skinTonePickerExpanded*/ ctx[6] + ? '' + : 'hidden no-animate'))) { + attr(div3, "class", div3_class_value); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64) { + set_style(div3, "transform", "translateY(" + (/*skinTonePickerExpanded*/ ctx[6] + ? 0 + : 'calc(-1 * var(--num-skintones) * var(--total-emoji-size))') + ")"); + } + + if (dirty[0] & /*i18n*/ 1 && div3_aria_label_value !== (div3_aria_label_value = /*i18n*/ ctx[0].skinTonesLabel)) { + attr(div3, "aria-label", div3_aria_label_value); + } + + if (dirty[0] & /*activeSkinTone*/ 1048576 && div3_aria_activedescendant_value !== (div3_aria_activedescendant_value = "skintone-" + /*activeSkinTone*/ ctx[20])) { + attr(div3, "aria-activedescendant", div3_aria_activedescendant_value); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64 && div3_aria_hidden_value !== (div3_aria_hidden_value = !/*skinTonePickerExpanded*/ ctx[6])) { + attr(div3, "aria-hidden", div3_aria_hidden_value); + } + + if (dirty[0] & /*groups, i18n, searchMode, currentGroup*/ 12305 | dirty[1] & /*onNavClick*/ 1) { + each_value_3 = /*groups*/ ctx[12]; + each_blocks_2 = update_keyed_each(each_blocks_2, dirty, get_key_1, 1, ctx, each_value_3, each1_lookup, div5, destroy_block, create_each_block_3, null, get_each_context_3); + } + + if (dirty[0] & /*groups*/ 4096) { + set_style(div5, "grid-template-columns", "repeat(" + /*groups*/ ctx[12].length + ", 1fr)"); + } + + if (dirty[0] & /*i18n*/ 1 && div5_aria_label_value !== (div5_aria_label_value = /*i18n*/ ctx[0].categoriesLabel)) { + attr(div5, "aria-label", div5_aria_label_value); + } + + if (dirty[0] & /*isRtl, currentGroupIndex*/ 16779264) { + set_style(div6, "transform", "translateX(" + (/*isRtl*/ ctx[24] ? -1 : 1) * /*currentGroupIndex*/ ctx[11] * 100 + "%)"); + } + + if (dirty[0] & /*message*/ 262144) set_data(t4, /*message*/ ctx[18]); + + if (dirty[0] & /*message*/ 262144 && div8_class_value !== (div8_class_value = "message " + (/*message*/ ctx[18] ? '' : 'gone'))) { + attr(div8, "class", div8_class_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem, labelWithSkin, currentSkinTone, unicodeWithSkin, i18n, currentGroup*/ 402694449) { + each_value_1 = /*currentEmojisWithCategories*/ ctx[15]; + each_blocks_1 = update_keyed_each(each_blocks_1, dirty, get_key_2, 1, ctx, each_value_1, each2_lookup, div9, destroy_block, create_each_block_1, null, get_each_context_1); + } + + if (dirty[0] & /*databaseLoaded, message*/ 278528 && div10_class_value !== (div10_class_value = "tabpanel " + (!/*databaseLoaded*/ ctx[14] || /*message*/ ctx[18] + ? 'gone' + : ''))) { + attr(div10, "class", div10_class_value); + } + + if (dirty[0] & /*searchMode*/ 16 && div10_role_value !== (div10_role_value = /*searchMode*/ ctx[4] ? 'region' : 'tabpanel')) { + attr(div10, "role", div10_role_value); + } + + if (dirty[0] & /*searchMode, i18n, currentGroup*/ 8209 && div10_aria_label_value !== (div10_aria_label_value = /*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name])) { + attr(div10, "aria-label", div10_aria_label_value); + } + + if (dirty[0] & /*searchMode, currentGroup*/ 8208 && div10_id_value !== (div10_id_value = /*searchMode*/ ctx[4] + ? '' + : `tab-${/*currentGroup*/ ctx[13].id}`)) { + attr(div10, "id", div10_id_value); + } + + if (dirty[0] & /*labelWithSkin, currentFavorites, currentSkinTone, unicodeWithSkin*/ 402654464) { + each_value = /*currentFavorites*/ ctx[10]; + each_blocks = update_keyed_each(each_blocks, dirty, get_key_3, 1, ctx, each_value, each3_lookup, div11, destroy_block, create_each_block, null, get_each_context); + } + + if (dirty[0] & /*message*/ 262144 && div11_class_value !== (div11_class_value = "favorites emoji-menu " + (/*message*/ ctx[18] ? 'gone' : ''))) { + attr(div11, "class", div11_class_value); + } + + if (dirty[0] & /*i18n*/ 1 && div11_aria_label_value !== (div11_aria_label_value = /*i18n*/ ctx[0].favoritesLabel)) { + attr(div11, "aria-label", div11_aria_label_value); + } + + if (dirty[0] & /*scrollbarWidth*/ 33554432) { + set_style(div11, "padding-inline-end", /*scrollbarWidth*/ ctx[25] + "px"); + } + + if (dirty[0] & /*i18n*/ 1 && section_aria_label_value !== (section_aria_label_value = /*i18n*/ ctx[0].regionLabel)) { + attr(section, "aria-label", section_aria_label_value); + } + + if (dirty[0] & /*pickerStyle*/ 4194304) { + attr(section, "style", /*pickerStyle*/ ctx[22]); + } + }, + i: noop, + o: noop, + d(detaching) { + if (detaching) detach(section); + + for (let i = 0; i < each_blocks_3.length; i += 1) { + each_blocks_3[i].d(); + } + + /*div3_binding*/ ctx[49](null); + + for (let i = 0; i < each_blocks_2.length; i += 1) { + each_blocks_2[i].d(); + } + + for (let i = 0; i < each_blocks_1.length; i += 1) { + each_blocks_1[i].d(); + } + + /*div10_binding*/ ctx[51](null); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].d(); + } + + /*button1_binding*/ ctx[52](null); + /*section_binding*/ ctx[53](null); + mounted = false; + run_all(dispose); + } + }; +} + +function instance($$self, $$props, $$invalidate) { + let { skinToneEmoji } = $$props; + let { i18n } = $$props; + let { database } = $$props; + let { customEmoji } = $$props; + let { customCategorySorting } = $$props; + let { emojiVersion } = $$props; + + // private + let initialLoad = true; + + let currentEmojis = []; + let currentEmojisWithCategories = []; // eslint-disable-line no-unused-vars + let rawSearchText = ''; + let searchText = ''; + let rootElement; + let baselineEmoji; + let tabpanelElement; + let searchMode = false; // eslint-disable-line no-unused-vars + let activeSearchItem = -1; + let message; // eslint-disable-line no-unused-vars + let skinTonePickerExpanded = false; + let skinTonePickerExpandedAfterAnimation = false; // eslint-disable-line no-unused-vars + let skinToneDropdown; + let currentSkinTone = 0; + let activeSkinTone = 0; + let skinToneButtonText; // eslint-disable-line no-unused-vars + let pickerStyle; // eslint-disable-line no-unused-vars + let skinToneButtonLabel = ''; // eslint-disable-line no-unused-vars + let skinTones = []; + let currentFavorites = []; // eslint-disable-line no-unused-vars + let defaultFavoriteEmojis; + let numColumns = DEFAULT_NUM_COLUMNS; + let isRtl = false; // eslint-disable-line no-unused-vars + let scrollbarWidth = 0; // eslint-disable-line no-unused-vars + let currentGroupIndex = 0; + let groups$1 = groups; + let currentGroup; + let databaseLoaded = false; // eslint-disable-line no-unused-vars + let activeSearchItemId; // eslint-disable-line no-unused-vars + + // + // Utils/helpers + // + const focus = id => { + rootElement.getRootNode().getElementById(id).focus(); + }; + + // fire a custom event that crosses the shadow boundary + const fireEvent = (name, detail) => { + rootElement.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); + }; + + // eslint-disable-next-line no-unused-vars + const unicodeWithSkin = (emoji, currentSkinTone) => currentSkinTone && emoji.skins && emoji.skins[currentSkinTone] || emoji.unicode; + + // eslint-disable-next-line no-unused-vars + const labelWithSkin = (emoji, currentSkinTone) => uniq([ + emoji.name || unicodeWithSkin(emoji, currentSkinTone), + ...emoji.shortcodes || [] + ]).join(', '); + + // Detect a skintone option button + const isSkinToneOption = element => (/^skintone-/).test(element.id); + + // + // Determine the emoji support level (in requestIdleCallback) + // + onMount(() => { + if (!emojiVersion) { + detectEmojiSupportLevel().then(level => { + // Can't actually test emoji support in Jest/JSDom, emoji never render in color in Cairo + /* istanbul ignore next */ + if (!level) { + $$invalidate(18, message = i18n.emojiUnsupportedMessage); + } + }); + } + }); + + // + // Calculate the width of the emoji grid. This serves two purposes: + // 1) Re-calculate the --num-columns var because it may have changed + // 2) Re-calculate the scrollbar width because it may have changed + // (i.e. because the number of items changed) + // 3) Re-calculate whether we're in RTL mode or not. + // + // The benefit of doing this in one place is to align with rAF/ResizeObserver + // and do all the calculations in one go. RTL vs LTR is not strictly width-related, + // but since we're already reading the style here, and since it's already aligned with + // the rAF loop, this is the most appropriate place to do it perf-wise. + // + // eslint-disable-next-line no-unused-vars + function calculateEmojiGridStyle(node) { + return calculateWidth(node, width => { + /* istanbul ignore next */ + if ("production" !== 'test') { + // jsdom throws errors for this kind of fancy stuff + // read all the style/layout calculations we need to make + const style = getComputedStyle(rootElement); + + const newNumColumns = parseInt(style.getPropertyValue('--num-columns'), 10); + const newIsRtl = style.getPropertyValue('direction') === 'rtl'; + const parentWidth = node.parentElement.getBoundingClientRect().width; + const newScrollbarWidth = parentWidth - width; + + // write to Svelte variables + $$invalidate(47, numColumns = newNumColumns); + + $$invalidate(25, scrollbarWidth = newScrollbarWidth); // eslint-disable-line no-unused-vars + $$invalidate(24, isRtl = newIsRtl); // eslint-disable-line no-unused-vars + } + }); + } + + function checkZwjSupportAndUpdate(zwjEmojisToCheck) { + const rootNode = rootElement.getRootNode(); + const emojiToDomNode = emoji => rootNode.getElementById(`emo-${emoji.id}`); + checkZwjSupport(zwjEmojisToCheck, baselineEmoji, emojiToDomNode); + + // force update + $$invalidate(1, currentEmojis = currentEmojis); // eslint-disable-line no-self-assign + } + + function isZwjSupported(emoji) { + return !emoji.unicode || !hasZwj(emoji) || supportedZwjEmojis.get(emoji.unicode); + } + + async function filterEmojisByVersion(emojis) { + const emojiSupportLevel = emojiVersion || await detectEmojiSupportLevel(); + + // !version corresponds to custom emoji + return emojis.filter(({ version }) => !version || version <= emojiSupportLevel); + } + + async function summarizeEmojis(emojis) { + return summarizeEmojisForUI(emojis, emojiVersion || await detectEmojiSupportLevel()); + } + + async function getEmojisByGroup(group) { + + // -1 is custom emoji + const emoji = group === -1 + ? customEmoji + : await database.getEmojiByGroup(group); + + return summarizeEmojis(await filterEmojisByVersion(emoji)); + } + + async function getEmojisBySearchQuery(query) { + return summarizeEmojis(await filterEmojisByVersion(await database.getEmojiBySearchQuery(query))); + } + + // eslint-disable-next-line no-unused-vars + function onSearchKeydown(event) { + if (!searchMode || !currentEmojis.length) { + return; + } + + const goToNextOrPrevious = previous => { + halt(event); + $$invalidate(5, activeSearchItem = incrementOrDecrement(previous, activeSearchItem, currentEmojis)); + }; + + switch (event.key) { + case 'ArrowDown': + return goToNextOrPrevious(false); + case 'ArrowUp': + return goToNextOrPrevious(true); + case 'Enter': + if (activeSearchItem !== -1) { + halt(event); + return clickEmoji(currentEmojis[activeSearchItem].id); + } else if (currentEmojis.length) { + $$invalidate(5, activeSearchItem = 0); + } + } + } + + // + // Handle user input on nav + // + // eslint-disable-next-line no-unused-vars + function onNavClick(group) { + $$invalidate(2, rawSearchText = ''); + $$invalidate(45, searchText = ''); + $$invalidate(5, activeSearchItem = -1); + $$invalidate(11, currentGroupIndex = groups$1.findIndex(_ => _.id === group.id)); + } + + // eslint-disable-next-line no-unused-vars + function onNavKeydown(event) { + const { target, key } = event; + + const doFocus = el => { + if (el) { + halt(event); + el.focus(); + } + }; + + switch (key) { + case 'ArrowLeft': + return doFocus(target.previousSibling); + case 'ArrowRight': + return doFocus(target.nextSibling); + case 'Home': + return doFocus(target.parentElement.firstChild); + case 'End': + return doFocus(target.parentElement.lastChild); + } + } + + // + // Handle user input on an emoji + // + async function clickEmoji(unicodeOrName) { + const emoji = await database.getEmojiByUnicodeOrName(unicodeOrName); + const emojiSummary = [...currentEmojis, ...currentFavorites].find(_ => _.id === unicodeOrName); + const skinTonedUnicode = emojiSummary.unicode && unicodeWithSkin(emojiSummary, currentSkinTone); + await database.incrementFavoriteEmojiCount(unicodeOrName); + + fireEvent('emoji-click', { + emoji, + skinTone: currentSkinTone, + ...skinTonedUnicode && { unicode: skinTonedUnicode }, + ...emojiSummary.name && { name: emojiSummary.name } + }); + } + + // eslint-disable-next-line no-unused-vars + async function onEmojiClick(event) { + const { target } = event; + + if (!target.classList.contains('emoji')) { + return; + } + + halt(event); + const id = target.id.substring(4); // replace 'emo-' or 'fav-' prefix + + /* no await */ + clickEmoji(id); + } + + // + // Handle user input on the skintone picker + // + // eslint-disable-next-line no-unused-vars + async function onSkinToneOptionsClick(event) { + const { target } = event; + + if (!isSkinToneOption(target)) { + return; + } + + halt(event); + const skinTone = parseInt(target.id.slice(9), 10); // remove 'skintone-' prefix + $$invalidate(8, currentSkinTone = skinTone); + $$invalidate(6, skinTonePickerExpanded = false); + focus('skintone-button'); + fireEvent('skin-tone-change', { skinTone }); + + /* no await */ + database.setPreferredSkinTone(skinTone); + } + + // eslint-disable-next-line no-unused-vars + async function onClickSkinToneButton(event) { + $$invalidate(6, skinTonePickerExpanded = !skinTonePickerExpanded); + $$invalidate(20, activeSkinTone = currentSkinTone); + + if (skinTonePickerExpanded) { + halt(event); + rAF(() => focus(`skintone-${activeSkinTone}`)); + } + } + + // eslint-disable-next-line no-unused-vars + function onSkinToneOptionsKeydown(event) { + if (!skinTonePickerExpanded) { + return; + } + + const changeActiveSkinTone = async nextSkinTone => { + halt(event); + $$invalidate(20, activeSkinTone = nextSkinTone); + await tick(); + focus(`skintone-${activeSkinTone}`); + }; + + switch (event.key) { + case 'ArrowUp': + return changeActiveSkinTone(incrementOrDecrement(true, activeSkinTone, skinTones)); + case 'ArrowDown': + return changeActiveSkinTone(incrementOrDecrement(false, activeSkinTone, skinTones)); + case 'Home': + return changeActiveSkinTone(0); + case 'End': + return changeActiveSkinTone(skinTones.length - 1); + case 'Enter': + // enter on keydown, space on keyup. this is just how browsers work for buttons + // https://lists.w3.org/Archives/Public/w3c-wai-ig/2019JanMar/0086.html + return onSkinToneOptionsClick(event); + case 'Escape': + halt(event); + $$invalidate(6, skinTonePickerExpanded = false); + return focus('skintone-button'); + } + } + + // eslint-disable-next-line no-unused-vars + function onSkinToneOptionsKeyup(event) { + if (!skinTonePickerExpanded) { + return; + } + + switch (event.key) { + case ' ': + // enter on keydown, space on keyup. this is just how browsers work for buttons + // https://lists.w3.org/Archives/Public/w3c-wai-ig/2019JanMar/0086.html + return onSkinToneOptionsClick(event); + } + } + + // eslint-disable-next-line no-unused-vars + async function onSkinToneOptionsFocusOut(event) { + // On blur outside of the skintone options, collapse the skintone picker. + // Except if focus is just moving to another skintone option, e.g. pressing up/down to change focus + const { relatedTarget } = event; + + if (!relatedTarget || !isSkinToneOption(relatedTarget)) { + $$invalidate(6, skinTonePickerExpanded = false); + } + } + + function input_input_handler() { + rawSearchText = this.value; + $$invalidate(2, rawSearchText); + } + + function div3_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + skinToneDropdown = $$value; + $$invalidate(7, skinToneDropdown); + }); + } + + const click_handler = group => onNavClick(group); + + function div10_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + tabpanelElement = $$value; + $$invalidate(3, tabpanelElement); + }); + } + + function button1_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + baselineEmoji = $$value; + $$invalidate(17, baselineEmoji); + }); + } + + function section_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + rootElement = $$value; + $$invalidate(16, rootElement); + }); + } + + $$self.$$set = $$props => { + if ('skinToneEmoji' in $$props) $$invalidate(40, skinToneEmoji = $$props.skinToneEmoji); + if ('i18n' in $$props) $$invalidate(0, i18n = $$props.i18n); + if ('database' in $$props) $$invalidate(39, database = $$props.database); + if ('customEmoji' in $$props) $$invalidate(41, customEmoji = $$props.customEmoji); + if ('customCategorySorting' in $$props) $$invalidate(42, customCategorySorting = $$props.customCategorySorting); + if ('emojiVersion' in $$props) $$invalidate(43, emojiVersion = $$props.emojiVersion); + }; + + $$self.$$.update = () => { + if ($$self.$$.dirty[1] & /*customEmoji, database*/ 1280) { + /* eslint-enable no-unused-vars */ + // + // Set or update the customEmoji + // + { + if (customEmoji && database) { + $$invalidate(39, database.customEmoji = customEmoji, database); + } + } + } + + if ($$self.$$.dirty[0] & /*i18n*/ 1 | $$self.$$.dirty[1] & /*database*/ 256) { + // + // Set or update the database object + // + { + // show a Loading message if it takes a long time, or show an error if there's a network/IDB error + async function handleDatabaseLoading() { + let showingLoadingMessage = false; + + const timeoutHandle = setTimeout( + () => { + showingLoadingMessage = true; + $$invalidate(18, message = i18n.loadingMessage); + }, + TIMEOUT_BEFORE_LOADING_MESSAGE + ); + + try { + await database.ready(); + $$invalidate(14, databaseLoaded = true); // eslint-disable-line no-unused-vars + } catch(err) { + console.error(err); + $$invalidate(18, message = i18n.networkErrorMessage); + } finally { + clearTimeout(timeoutHandle); + + if (showingLoadingMessage) { + // Seems safer than checking the i18n string, which may change + showingLoadingMessage = false; + + $$invalidate(18, message = ''); // eslint-disable-line no-unused-vars + } + } + } + + if (database) { + /* no await */ + handleDatabaseLoading(); + } + } + } + + if ($$self.$$.dirty[0] & /*groups, currentGroupIndex*/ 6144 | $$self.$$.dirty[1] & /*customEmoji*/ 1024) { + { + if (customEmoji && customEmoji.length) { + $$invalidate(12, groups$1 = [customGroup, ...groups]); + } else if (groups$1 !== groups) { + if (currentGroupIndex) { + // If the current group is anything other than "custom" (which is first), decrement. + // This fixes the odd case where you set customEmoji, then pick a category, then unset customEmoji + $$invalidate(11, currentGroupIndex--, currentGroupIndex); + } + + $$invalidate(12, groups$1 = groups); + } + } + } + + if ($$self.$$.dirty[0] & /*rawSearchText*/ 4) { + /* eslint-enable no-unused-vars */ + // + // Handle user input on the search input + // + { + rIC(() => { + $$invalidate(45, searchText = (rawSearchText || '').trim()); // defer to avoid input delays, plus we can trim here + $$invalidate(5, activeSearchItem = -1); + }); + } + } + + if ($$self.$$.dirty[0] & /*groups, currentGroupIndex*/ 6144) { + // + // Update the current group based on the currentGroupIndex + // + $$invalidate(13, currentGroup = groups$1[currentGroupIndex]); + } + + if ($$self.$$.dirty[0] & /*databaseLoaded, currentGroup*/ 24576 | $$self.$$.dirty[1] & /*searchText*/ 16384) { + // + // Set or update the currentEmojis. Check for invalid ZWJ renderings + // (i.e. double emoji). + // + { + async function updateEmojis() { + + if (!databaseLoaded) { + $$invalidate(1, currentEmojis = []); + $$invalidate(4, searchMode = false); + } else if (searchText.length >= MIN_SEARCH_TEXT_LENGTH) { + const currentSearchText = searchText; + const newEmojis = await getEmojisBySearchQuery(currentSearchText); + + if (currentSearchText === searchText) { + // if the situation changes asynchronously, do not update + $$invalidate(1, currentEmojis = newEmojis); + + $$invalidate(4, searchMode = true); + } + } else if (currentGroup) { + const currentGroupId = currentGroup.id; + const newEmojis = await getEmojisByGroup(currentGroupId); + + if (currentGroupId === currentGroup.id) { + // if the situation changes asynchronously, do not update + $$invalidate(1, currentEmojis = newEmojis); + + $$invalidate(4, searchMode = false); + } + } + } + + /* no await */ + updateEmojis(); + } + } + + if ($$self.$$.dirty[0] & /*groups, searchMode*/ 4112) { + // + // Global styles for the entire picker + // + /* eslint-disable no-unused-vars */ + $$invalidate(22, pickerStyle = ` + --num-groups: ${groups$1.length}; + --indicator-opacity: ${searchMode ? 0 : 1}; + --num-skintones: ${NUM_SKIN_TONES};`); + } + + if ($$self.$$.dirty[0] & /*databaseLoaded*/ 16384 | $$self.$$.dirty[1] & /*database*/ 256) { + // + // Set or update the preferred skin tone + // + { + async function updatePreferredSkinTone() { + if (databaseLoaded) { + $$invalidate(8, currentSkinTone = await database.getPreferredSkinTone()); + } + } + + /* no await */ + updatePreferredSkinTone(); + } + } + + if ($$self.$$.dirty[1] & /*skinToneEmoji*/ 512) { + $$invalidate(9, skinTones = Array(NUM_SKIN_TONES).fill().map((_, i) => applySkinTone(skinToneEmoji, i))); + } + + if ($$self.$$.dirty[0] & /*skinTones, currentSkinTone*/ 768) { + /* eslint-disable no-unused-vars */ + $$invalidate(21, skinToneButtonText = skinTones[currentSkinTone]); + } + + if ($$self.$$.dirty[0] & /*i18n, currentSkinTone*/ 257) { + $$invalidate(23, skinToneButtonLabel = i18n.skinToneLabel.replace('{skinTone}', i18n.skinTones[currentSkinTone])); + } + + if ($$self.$$.dirty[0] & /*databaseLoaded*/ 16384 | $$self.$$.dirty[1] & /*database*/ 256) { + /* eslint-enable no-unused-vars */ + // + // Set or update the favorites emojis + // + { + async function updateDefaultFavoriteEmojis() { + $$invalidate(46, defaultFavoriteEmojis = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map(unicode => database.getEmojiByUnicodeOrName(unicode)))).filter(Boolean)); // filter because in Jest tests we don't have all the emoji in the DB + } + + if (databaseLoaded) { + /* no await */ + updateDefaultFavoriteEmojis(); + } + } + } + + if ($$self.$$.dirty[0] & /*databaseLoaded*/ 16384 | $$self.$$.dirty[1] & /*database, numColumns, defaultFavoriteEmojis*/ 98560) { + { + async function updateFavorites() { + const dbFavorites = await database.getTopFavoriteEmoji(numColumns); + const favorites = await summarizeEmojis(uniqBy([...dbFavorites, ...defaultFavoriteEmojis], _ => _.unicode || _.name).slice(0, numColumns)); + $$invalidate(10, currentFavorites = favorites); + } + + if (databaseLoaded && defaultFavoriteEmojis) { + /* no await */ + updateFavorites(); + } + } + } + + if ($$self.$$.dirty[0] & /*currentEmojis, tabpanelElement*/ 10 | $$self.$$.dirty[1] & /*emojiVersion*/ 4096) { + // Some emojis have their ligatures rendered as two or more consecutive emojis + // We want to treat these the same as unsupported emojis, so we compare their + // widths against the baseline widths and remove them as necessary + { + const zwjEmojisToCheck = currentEmojis.filter(emoji => emoji.unicode).filter(emoji => hasZwj(emoji) && !supportedZwjEmojis.has(emoji.unicode)); // filter custom emoji + + if (!emojiVersion && zwjEmojisToCheck.length) { + // render now, check their length later + rAF(() => checkZwjSupportAndUpdate(zwjEmojisToCheck)); + } else { + $$invalidate(1, currentEmojis = emojiVersion + ? currentEmojis + : currentEmojis.filter(isZwjSupported)); + + // Reset scroll top to 0 when emojis change + rAF(() => resetScrollTopIfPossible(tabpanelElement)); + } + } + } + + if ($$self.$$.dirty[0] & /*currentEmojis, currentFavorites*/ 1026 | $$self.$$.dirty[1] & /*initialLoad*/ 8192) { + { + // consider initialLoad to be complete when the first tabpanel and favorites are rendered + /* istanbul ignore next */ + if ("production" !== 'production' || false) { + if (currentEmojis.length && currentFavorites.length && initialLoad) { + $$invalidate(44, initialLoad = false); + requestPostAnimationFrame(() => (void 0)); + } + } + } + } + + if ($$self.$$.dirty[0] & /*searchMode, currentEmojis*/ 18 | $$self.$$.dirty[1] & /*customCategorySorting*/ 2048) { + // + // Derive currentEmojisWithCategories from currentEmojis. This is always done even if there + // are no categories, because it's just easier to code the HTML this way. + // + { + function calculateCurrentEmojisWithCategories() { + if (searchMode) { + return [{ category: '', emojis: currentEmojis }]; + } + + const categoriesToEmoji = new Map(); + + for (const emoji of currentEmojis) { + const category = emoji.category || ''; + let emojis = categoriesToEmoji.get(category); + + if (!emojis) { + emojis = []; + categoriesToEmoji.set(category, emojis); + } + + emojis.push(emoji); + } + + return [...categoriesToEmoji.entries()].map(([category, emojis]) => ({ category, emojis })).sort((a, b) => customCategorySorting(a.category, b.category)); + } + + // eslint-disable-next-line no-unused-vars + $$invalidate(15, currentEmojisWithCategories = calculateCurrentEmojisWithCategories()); + } + } + + if ($$self.$$.dirty[0] & /*activeSearchItem, currentEmojis*/ 34) { + // + // Handle active search item (i.e. pressing up or down while searching) + // + /* eslint-disable no-unused-vars */ + $$invalidate(26, activeSearchItemId = activeSearchItem !== -1 && currentEmojis[activeSearchItem].id); + } + + if ($$self.$$.dirty[0] & /*skinTonePickerExpanded, skinToneDropdown*/ 192) { + // To make the animation nicer, change the z-index of the skintone picker button + // *after* the animation has played. This makes it appear that the picker box + // is expanding "below" the button + { + if (skinTonePickerExpanded) { + skinToneDropdown.addEventListener( + 'transitionend', + () => { + $$invalidate(19, skinTonePickerExpandedAfterAnimation = true); // eslint-disable-line no-unused-vars + }, + { once: true } + ); + } else { + $$invalidate(19, skinTonePickerExpandedAfterAnimation = false); // eslint-disable-line no-unused-vars + } + } + } + }; + + return [ + i18n, + currentEmojis, + rawSearchText, + tabpanelElement, + searchMode, + activeSearchItem, + skinTonePickerExpanded, + skinToneDropdown, + currentSkinTone, + skinTones, + currentFavorites, + currentGroupIndex, + groups$1, + currentGroup, + databaseLoaded, + currentEmojisWithCategories, + rootElement, + baselineEmoji, + message, + skinTonePickerExpandedAfterAnimation, + activeSkinTone, + skinToneButtonText, + pickerStyle, + skinToneButtonLabel, + isRtl, + scrollbarWidth, + activeSearchItemId, + unicodeWithSkin, + labelWithSkin, + calculateEmojiGridStyle, + onSearchKeydown, + onNavClick, + onNavKeydown, + onEmojiClick, + onSkinToneOptionsClick, + onClickSkinToneButton, + onSkinToneOptionsKeydown, + onSkinToneOptionsKeyup, + onSkinToneOptionsFocusOut, + database, + skinToneEmoji, + customEmoji, + customCategorySorting, + emojiVersion, + initialLoad, + searchText, + defaultFavoriteEmojis, + numColumns, + input_input_handler, + div3_binding, + click_handler, + div10_binding, + button1_binding, + section_binding + ]; +} + +class Picker extends SvelteComponent { + constructor(options) { + super(); + + init( + this, + options, + instance, + create_fragment, + safe_not_equal, + { + skinToneEmoji: 40, + i18n: 0, + database: 39, + customEmoji: 41, + customCategorySorting: 42, + emojiVersion: 43 + }, + null, + [-1, -1, -1] + ); + } +} + +const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json'; +const DEFAULT_LOCALE = 'en'; + +var enI18n = { + categoriesLabel: 'Categories', + emojiUnsupportedMessage: 'Your browser does not support color emoji.', + favoritesLabel: 'Favorites', + loadingMessage: 'Loading…', + networkErrorMessage: 'Could not load emoji.', + regionLabel: 'Emoji picker', + searchDescription: 'When search results are available, press up or down to select and enter to choose.', + searchLabel: 'Search', + searchResultsLabel: 'Search results', + skinToneDescription: 'When expanded, press up or down to select and enter to choose.', + skinToneLabel: 'Choose a skin tone (currently {skinTone})', + skinTonesLabel: 'Skin tones', + skinTones: [ + 'Default', + 'Light', + 'Medium-Light', + 'Medium', + 'Medium-Dark', + 'Dark' + ], + categories: { + custom: 'Custom', + 'smileys-emotion': 'Smileys and emoticons', + 'people-body': 'People and body', + 'animals-nature': 'Animals and nature', + 'food-drink': 'Food and drink', + 'travel-places': 'Travel and places', + activities: 'Activities', + objects: 'Objects', + symbols: 'Symbols', + flags: 'Flags' + } +}; + +const PROPS = [ + 'customEmoji', + 'customCategorySorting', + 'database', + 'dataSource', + 'i18n', + 'locale', + 'skinToneEmoji', + 'emojiVersion' +]; + +// Styles injected ourselves, so we can declare the FONT_FAMILY variable in one place +const EXTRA_STYLES = `:host{--emoji-font-family:${FONT_FAMILY}}`; + +class PickerElement extends HTMLElement { + constructor (props) { + super(); + this.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = ":host{--emoji-size:1.375rem;--emoji-padding:0.5rem;--category-emoji-size:var(--emoji-size);--category-emoji-padding:var(--emoji-padding);--indicator-height:3px;--input-border-radius:0.5rem;--input-border-size:1px;--input-font-size:1rem;--input-line-height:1.5;--input-padding:0.25rem;--num-columns:8;--outline-size:2px;--border-size:1px;--skintone-border-radius:1rem;--category-font-size:1rem;display:flex;width:min-content;height:400px}:host,:host(.light){color-scheme:light;--background:#fff;--border-color:#e0e0e0;--indicator-color:#385ac1;--input-border-color:#999;--input-font-color:#111;--input-placeholder-color:#999;--outline-color:#999;--category-font-color:#111;--button-active-background:#e6e6e6;--button-hover-background:#d9d9d9}:host(.dark){color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}@media (prefers-color-scheme:dark){:host{color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}}:host([hidden]){display:none}button{margin:0;padding:0;border:0;background:0 0;box-shadow:none;-webkit-tap-highlight-color:transparent}button::-moz-focus-inner{border:0}input{padding:0;margin:0;line-height:1.15;font-family:inherit}input[type=search]{-webkit-appearance:none}:focus{outline:var(--outline-color) solid var(--outline-size);outline-offset:calc(-1*var(--outline-size))}:host([data-js-focus-visible]) :focus:not([data-focus-visible-added]){outline:0}:focus:not(:focus-visible){outline:0}.hide-focus{outline:0}*{box-sizing:border-box}.picker{contain:content;display:flex;flex-direction:column;background:var(--background);border:var(--border-size) solid var(--border-color);width:100%;height:100%;overflow:hidden;--total-emoji-size:calc(var(--emoji-size) + (2 * var(--emoji-padding)));--total-category-emoji-size:calc(var(--category-emoji-size) + (2 * var(--category-emoji-padding)))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.hidden{opacity:0;pointer-events:none}.abs-pos{position:absolute;left:0;top:0}.gone{display:none!important}.skintone-button-wrapper,.skintone-list{background:var(--background);z-index:3}.skintone-button-wrapper.expanded{z-index:1}.skintone-list{position:absolute;inset-inline-end:0;top:0;z-index:2;overflow:visible;border-bottom:var(--border-size) solid var(--border-color);border-radius:0 0 var(--skintone-border-radius) var(--skintone-border-radius);will-change:transform;transition:transform .2s ease-in-out;transform-origin:center 0}@media (prefers-reduced-motion:reduce){.skintone-list{transition-duration:.001s}}@supports not (inset-inline-end:0){.skintone-list{right:0}}.skintone-list.no-animate{transition:none}.tabpanel{overflow-y:auto;-webkit-overflow-scrolling:touch;will-change:transform;min-height:0;flex:1;contain:content}.emoji-menu{display:grid;grid-template-columns:repeat(var(--num-columns),var(--total-emoji-size));justify-content:space-around;align-items:flex-start;width:100%}.category{padding:var(--emoji-padding);font-size:var(--category-font-size);color:var(--category-font-color)}.custom-emoji,.emoji,button.emoji{height:var(--total-emoji-size);width:var(--total-emoji-size)}.emoji,button.emoji{font-size:var(--emoji-size);display:flex;align-items:center;justify-content:center;border-radius:100%;line-height:1;overflow:hidden;font-family:var(--emoji-font-family);cursor:pointer}@media (hover:hover) and (pointer:fine){.emoji:hover,button.emoji:hover{background:var(--button-hover-background)}}.emoji.active,.emoji:active,button.emoji.active,button.emoji:active{background:var(--button-active-background)}.custom-emoji{padding:var(--emoji-padding);object-fit:contain;pointer-events:none;background-repeat:no-repeat;background-position:center center;background-size:var(--emoji-size) var(--emoji-size)}.nav,.nav-button{align-items:center}.nav{display:grid;justify-content:space-between;contain:content}.nav-button{display:flex;justify-content:center}.nav-emoji{font-size:var(--category-emoji-size);width:var(--total-category-emoji-size);height:var(--total-category-emoji-size)}.indicator-wrapper{display:flex;border-bottom:1px solid var(--border-color)}.indicator{width:calc(100%/var(--num-groups));height:var(--indicator-height);opacity:var(--indicator-opacity);background-color:var(--indicator-color);will-change:transform,opacity;transition:opacity .1s linear,transform .25s ease-in-out}@media (prefers-reduced-motion:reduce){.indicator{will-change:opacity;transition:opacity .1s linear}}.pad-top,input.search{background:var(--background);width:100%}.pad-top{height:var(--emoji-padding);z-index:3}.search-row{display:flex;align-items:center;position:relative;padding-inline-start:var(--emoji-padding);padding-bottom:var(--emoji-padding)}.search-wrapper{flex:1;min-width:0}input.search{padding:var(--input-padding);border-radius:var(--input-border-radius);border:var(--input-border-size) solid var(--input-border-color);color:var(--input-font-color);font-size:var(--input-font-size);line-height:var(--input-line-height)}input.search::placeholder{color:var(--input-placeholder-color)}.favorites{display:flex;flex-direction:row;border-top:var(--border-size) solid var(--border-color);contain:content}.message{padding:var(--emoji-padding)}" + EXTRA_STYLES; + this.shadowRoot.appendChild(style); + this._ctx = { + // Set defaults + locale: DEFAULT_LOCALE, + dataSource: DEFAULT_DATA_SOURCE, + skinToneEmoji: DEFAULT_SKIN_TONE_EMOJI, + customCategorySorting: DEFAULT_CATEGORY_SORTING, + customEmoji: null, + i18n: enI18n, + emojiVersion: null, + ...props + }; + // Handle properties set before the element was upgraded + for (const prop of PROPS) { + if (prop !== 'database' && Object.prototype.hasOwnProperty.call(this, prop)) { + this._ctx[prop] = this[prop]; + delete this[prop]; + } + } + this._dbFlush(); // wait for a flush before creating the db, in case the user calls e.g. a setter or setAttribute + } + + connectedCallback () { + // The _cmp may be defined if the component was immediately disconnected and then reconnected. In that case, + // do nothing (preserve the state) + if (!this._cmp) { + this._cmp = new Picker({ + target: this.shadowRoot, + props: this._ctx + }); + } + } + + disconnectedCallback () { + // Check in a microtask if the element is still connected. If so, treat this as a "move" rather than a disconnect + // Inspired by Vue: https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue + Promise.resolve().then(() => { + // this._cmp may be defined if connect-disconnect-connect-disconnect occurs synchronously + if (!this.isConnected && this._cmp) { + this._cmp.$destroy(); + this._cmp = undefined; + + const { database } = this._ctx; + database.close() + // only happens if the database failed to load in the first place, so we don't care + .catch(err => console.error(err)); + } + }); + } + + static get observedAttributes () { + return ['locale', 'data-source', 'skin-tone-emoji', 'emoji-version'] // complex objects aren't supported, also use kebab-case + } + + attributeChangedCallback (attrName, oldValue, newValue) { + this._set( + // convert from kebab-case to camelcase + // see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015 + attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()), + // convert string attribute to float if necessary + attrName === 'emoji-version' ? parseFloat(newValue) : newValue + ); + } + + _set (prop, newValue) { + this._ctx[prop] = newValue; + if (this._cmp) { + this._cmp.$set({ [prop]: newValue }); + } + if (['locale', 'dataSource'].includes(prop)) { + this._dbFlush(); + } + } + + _dbCreate () { + const { locale, dataSource, database } = this._ctx; + // only create a new database if we really need to + if (!database || database.locale !== locale || database.dataSource !== dataSource) { + this._set('database', new Database({ locale, dataSource })); + } + } + + // Update the Database in one microtask if the locale/dataSource change. We do one microtask + // so we don't create two Databases if e.g. both the locale and the dataSource change + _dbFlush () { + Promise.resolve().then(() => ( + this._dbCreate() + )); + } +} + +const definitions = {}; + +for (const prop of PROPS) { + definitions[prop] = { + get () { + if (prop === 'database') { + // in rare cases, the microtask may not be flushed yet, so we need to instantiate the DB + // now if the user is asking for it + this._dbCreate(); + } + return this._ctx[prop] + }, + set (val) { + if (prop === 'database') { + throw new Error('database is read-only') + } + this._set(prop, val); + } + }; +} + +Object.defineProperties(PickerElement.prototype, definitions); + +/* istanbul ignore else */ +if (!customElements.get('emoji-picker')) { // if already defined, do nothing (e.g. same script imported twice) + customElements.define('emoji-picker', PickerElement); +} + +export { PickerElement as default }; diff --git a/app/static/scripts/emoji-picker-element/svelte.js b/app/static/scripts/emoji-picker-element/svelte.js new file mode 100644 index 0000000..ddc68ff --- /dev/null +++ b/app/static/scripts/emoji-picker-element/svelte.js @@ -0,0 +1,2294 @@ +import { SvelteComponent, init, safe_not_equal, element, text, attr, set_style, insert, append, set_input_value, listen, action_destroyer, set_data, update_keyed_each, destroy_block, noop, detach, run_all, globals, binding_callbacks, src_url_equal } from 'svelte/internal'; +import { onMount, tick } from 'svelte'; +import Database from './database.js'; + +// via https://unpkg.com/browse/emojibase-data@6.0.0/meta/groups.json +const allGroups = [ + [-1, '✨', 'custom'], + [0, '😀', 'smileys-emotion'], + [1, '👋', 'people-body'], + [3, '🐱', 'animals-nature'], + [4, '🍎', 'food-drink'], + [5, '🏠️', 'travel-places'], + [6, '⚽', 'activities'], + [7, '📝', 'objects'], + [8, '⛔️', 'symbols'], + [9, '🏁', 'flags'] +].map(([id, emoji, name]) => ({ id, emoji, name })); + +const groups = allGroups.slice(1); +const customGroup = allGroups[0]; + +const MIN_SEARCH_TEXT_LENGTH = 2; +const NUM_SKIN_TONES = 6; + +/* istanbul ignore next */ +const rIC = typeof requestIdleCallback === 'function' ? requestIdleCallback : setTimeout; + +// check for ZWJ (zero width joiner) character +function hasZwj (emoji) { + return emoji.unicode.includes('\u200d') +} + +// Find one good representative emoji from each version to test by checking its color. +// Ideally it should have color in the center. For some inspiration, see: +// https://about.gitlab.com/blog/2018/05/30/journey-in-native-unicode-emoji/ +// +// Note that for certain versions (12.1, 13.1), there is no point in testing them explicitly, because +// all the emoji from this version are compound-emoji from previous versions. So they would pass a color +// test, even in browsers that display them as double emoji. (E.g. "face in clouds" might render as +// "face without mouth" plus "fog".) These emoji can only be filtered using the width test, +// which happens in checkZwjSupport.js. +const versionsAndTestEmoji = { + '🫠': 14, + '🥲': 13.1, // smiling face with tear, technically from v13 but see note above + '🥻': 12.1, // sari, technically from v12 but see note above + '🥰': 11, + '🤩': 5, + '👱‍♀️': 4, + '🤣': 3, + '👁️‍🗨️': 2, + '😀': 1, + '😐️': 0.7, + '😃': 0.6 +}; + +const TIMEOUT_BEFORE_LOADING_MESSAGE = 1000; // 1 second +const DEFAULT_SKIN_TONE_EMOJI = '🖐️'; +const DEFAULT_NUM_COLUMNS = 8; + +// Based on https://fivethirtyeight.com/features/the-100-most-used-emojis/ and +// https://blog.emojipedia.org/facebook-reveals-most-and-least-used-emojis/ with +// a bit of my own curation. (E.g. avoid the "OK" gesture because of connotations: +// https://emojipedia.org/ok-hand/) +const MOST_COMMONLY_USED_EMOJI = [ + '😊', + '😒', + '♥️', + '👍️', + '😍', + '😂', + '😭', + '☺️', + '😔', + '😩', + '😏', + '💕', + '🙌', + '😘' +]; + +// It's important to list Twemoji Mozilla before everything else, because Mozilla bundles their +// own font on some platforms (notably Windows and Linux as of this writing). Typically, Mozilla +// updates faster than the underlying OS, and we don't want to render older emoji in one font and +// newer emoji in another font: +// https://github.com/nolanlawson/emoji-picker-element/pull/268#issuecomment-1073347283 +const FONT_FAMILY = '"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",' + + '"Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif'; + +/* istanbul ignore next */ +const DEFAULT_CATEGORY_SORTING = (a, b) => a < b ? -1 : a > b ? 1 : 0; + +// Test if an emoji is supported by rendering it to canvas and checking that the color is not black + +const getTextFeature = (text, color) => { + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + + const ctx = canvas.getContext('2d'); + ctx.textBaseline = 'top'; + ctx.font = `100px ${FONT_FAMILY}`; + ctx.fillStyle = color; + ctx.scale(0.01, 0.01); + ctx.fillText(text, 0, 0); + + return ctx.getImageData(0, 0, 1, 1).data +}; + +const compareFeatures = (feature1, feature2) => { + const feature1Str = [...feature1].join(','); + const feature2Str = [...feature2].join(','); + // This is RGBA, so for 0,0,0, we are checking that the first RGB is not all zeroes. + // Most of the time when unsupported this is 0,0,0,0, but on Chrome on Mac it is + // 0,0,0,61 - there is a transparency here. + return feature1Str === feature2Str && !feature1Str.startsWith('0,0,0,') +}; + +function testColorEmojiSupported (text) { + // Render white and black and then compare them to each other and ensure they're the same + // color, and neither one is black. This shows that the emoji was rendered in color. + const feature1 = getTextFeature(text, '#000'); + const feature2 = getTextFeature(text, '#fff'); + return feature1 && feature2 && compareFeatures(feature1, feature2) +} + +// rather than check every emoji ever, which would be expensive, just check some representatives from the + +function determineEmojiSupportLevel () { + const entries = Object.entries(versionsAndTestEmoji); + try { + // start with latest emoji and work backwards + for (const [emoji, version] of entries) { + if (testColorEmojiSupported(emoji)) { + return version + } + } + } catch (e) { // canvas error + } finally { + } + // In case of an error, be generous and just assume all emoji are supported (e.g. for canvas errors + // due to anti-fingerprinting add-ons). Better to show some gray boxes than nothing at all. + return entries[0][1] // first one in the list is the most recent version +} + +// Check which emojis we know for sure aren't supported, based on Unicode version level +let promise; +const detectEmojiSupportLevel = () => { + if (!promise) { + // Delay so it can run while the IDB database is being created by the browser (on another thread). + // This helps especially with first load – we want to start pre-populating the database on the main thread, + // and then wait for IDB to commit everything, and while waiting we run this check. + promise = new Promise(resolve => ( + rIC(() => ( + resolve(determineEmojiSupportLevel()) // delay so ideally this can run while IDB is first populating + )) + )); + } + return promise +}; +// determine which emojis containing ZWJ (zero width joiner) characters +// are supported (rendered as one glyph) rather than unsupported (rendered as two or more glyphs) +const supportedZwjEmojis = new Map(); + +const VARIATION_SELECTOR = '\ufe0f'; +const SKINTONE_MODIFIER = '\ud83c'; +const ZWJ = '\u200d'; +const LIGHT_SKIN_TONE = 0x1F3FB; +const LIGHT_SKIN_TONE_MODIFIER = 0xdffb; + +// TODO: this is a naive implementation, we can improve it later +// It's only used for the skintone picker, so as long as people don't customize with +// really exotic emoji then it should work fine +function applySkinTone (str, skinTone) { + if (skinTone === 0) { + return str + } + const zwjIndex = str.indexOf(ZWJ); + if (zwjIndex !== -1) { + return str.substring(0, zwjIndex) + + String.fromCodePoint(LIGHT_SKIN_TONE + skinTone - 1) + + str.substring(zwjIndex) + } + if (str.endsWith(VARIATION_SELECTOR)) { + str = str.substring(0, str.length - 1); + } + return str + SKINTONE_MODIFIER + String.fromCodePoint(LIGHT_SKIN_TONE_MODIFIER + skinTone - 1) +} + +function halt (event) { + event.preventDefault(); + event.stopPropagation(); +} + +// Implementation left/right or up/down navigation, circling back when you +// reach the start/end of the list +function incrementOrDecrement (decrement, val, arr) { + val += (decrement ? -1 : 1); + if (val < 0) { + val = arr.length - 1; + } else if (val >= arr.length) { + val = 0; + } + return val +} + +// like lodash's uniqBy but much smaller +function uniqBy (arr, func) { + const set = new Set(); + const res = []; + for (const item of arr) { + const key = func(item); + if (!set.has(key)) { + set.add(key); + res.push(item); + } + } + return res +} + +// We don't need all the data on every emoji, and there are specific things we need +// for the UI, so build a "view model" from the emoji object we got from the database + +function summarizeEmojisForUI (emojis, emojiSupportLevel) { + const toSimpleSkinsMap = skins => { + const res = {}; + for (const skin of skins) { + // ignore arrays like [1, 2] with multiple skin tones + // also ignore variants that are in an unsupported emoji version + // (these do exist - variants from a different version than their base emoji) + if (typeof skin.tone === 'number' && skin.version <= emojiSupportLevel) { + res[skin.tone] = skin.unicode; + } + } + return res + }; + + return emojis.map(({ unicode, skins, shortcodes, url, name, category }) => ({ + unicode, + name, + shortcodes, + url, + category, + id: unicode || name, + skins: skins && toSimpleSkinsMap(skins), + title: (shortcodes || []).join(', ') + })) +} + +// import rAF from one place so that the bundle size is a bit smaller +const rAF = requestAnimationFrame; + +// Svelte action to calculate the width of an element and auto-update + +let resizeObserverSupported = typeof ResizeObserver === 'function'; + +function calculateWidth (node, onUpdate) { + let resizeObserver; + if (resizeObserverSupported) { + resizeObserver = new ResizeObserver(entries => ( + onUpdate(entries[0].contentRect.width) + )); + resizeObserver.observe(node); + } else { // just set the width once, don't bother trying to track it + rAF(() => ( + onUpdate(node.getBoundingClientRect().width) + )); + } + + // cleanup function (called on destroy) + return { + destroy () { + if (resizeObserver) { + resizeObserver.disconnect(); + } + } + } +} + +// get the width of the text inside of a DOM node, via https://stackoverflow.com/a/59525891/680742 +function calculateTextWidth (node) { + /* istanbul ignore else */ + { + const range = document.createRange(); + range.selectNode(node.firstChild); + return range.getBoundingClientRect().width + } +} + +let baselineEmojiWidth; + +function checkZwjSupport (zwjEmojisToCheck, baselineEmoji, emojiToDomNode) { + for (const emoji of zwjEmojisToCheck) { + const domNode = emojiToDomNode(emoji); + const emojiWidth = calculateTextWidth(domNode); + if (typeof baselineEmojiWidth === 'undefined') { // calculate the baseline emoji width only once + baselineEmojiWidth = calculateTextWidth(baselineEmoji); + } + // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard + // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with + // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.) + // So here we set the threshold at 1.8 times the size of the baseline emoji. + const supported = emojiWidth / 1.8 < baselineEmojiWidth; + supportedZwjEmojis.set(emoji.unicode, supported); + } +} + +// Measure after style/layout are complete + +const requestPostAnimationFrame = callback => { + rAF(() => { + setTimeout(callback); + }); +}; + +// like lodash's uniq + +function uniq (arr) { + return uniqBy(arr, _ => _) +} + +// Note we put this in its own function outside Picker.js to avoid Svelte doing an invalidation on the "setter" here. +// At best the invalidation is useless, at worst it can cause infinite loops: +// https://github.com/nolanlawson/emoji-picker-element/pull/180 +// https://github.com/sveltejs/svelte/issues/6521 +// Also note tabpanelElement can be null if the element is disconnected immediately after connected +function resetScrollTopIfPossible (element) { + if (element) { + element.scrollTop = 0; + } +} + +/* src/picker/components/Picker/Picker.svelte generated by Svelte v3.55.1 */ + +const { Map: Map_1 } = globals; + +function get_each_context(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[64] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +function get_each_context_1(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[67] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +function get_each_context_2(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[64] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +function get_each_context_3(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[70] = list[i]; + return child_ctx; +} + +function get_each_context_4(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[73] = list[i]; + child_ctx[66] = i; + return child_ctx; +} + +// (43:38) {#each skinTones as skinTone, i (skinTone)} +function create_each_block_4(key_1, ctx) { + let div; + let t_value = /*skinTone*/ ctx[73] + ""; + let t; + let div_id_value; + let div_class_value; + let div_aria_selected_value; + let div_title_value; + let div_aria_label_value; + + return { + key: key_1, + first: null, + c() { + div = element("div"); + t = text(t_value); + attr(div, "id", div_id_value = "skintone-" + /*i*/ ctx[66]); + + attr(div, "class", div_class_value = "emoji hide-focus " + (/*i*/ ctx[66] === /*activeSkinTone*/ ctx[20] + ? 'active' + : '')); + + attr(div, "aria-selected", div_aria_selected_value = /*i*/ ctx[66] === /*activeSkinTone*/ ctx[20]); + attr(div, "role", "option"); + attr(div, "title", div_title_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]]); + attr(div, "tabindex", "-1"); + attr(div, "aria-label", div_aria_label_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]]); + this.first = div; + }, + m(target, anchor) { + insert(target, div, anchor); + append(div, t); + }, + p(new_ctx, dirty) { + ctx = new_ctx; + if (dirty[0] & /*skinTones*/ 512 && t_value !== (t_value = /*skinTone*/ ctx[73] + "")) set_data(t, t_value); + + if (dirty[0] & /*skinTones*/ 512 && div_id_value !== (div_id_value = "skintone-" + /*i*/ ctx[66])) { + attr(div, "id", div_id_value); + } + + if (dirty[0] & /*skinTones, activeSkinTone*/ 1049088 && div_class_value !== (div_class_value = "emoji hide-focus " + (/*i*/ ctx[66] === /*activeSkinTone*/ ctx[20] + ? 'active' + : ''))) { + attr(div, "class", div_class_value); + } + + if (dirty[0] & /*skinTones, activeSkinTone*/ 1049088 && div_aria_selected_value !== (div_aria_selected_value = /*i*/ ctx[66] === /*activeSkinTone*/ ctx[20])) { + attr(div, "aria-selected", div_aria_selected_value); + } + + if (dirty[0] & /*i18n, skinTones*/ 513 && div_title_value !== (div_title_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]])) { + attr(div, "title", div_title_value); + } + + if (dirty[0] & /*i18n, skinTones*/ 513 && div_aria_label_value !== (div_aria_label_value = /*i18n*/ ctx[0].skinTones[/*i*/ ctx[66]])) { + attr(div, "aria-label", div_aria_label_value); + } + }, + d(detaching) { + if (detaching) detach(div); + } + }; +} + +// (53:33) {#each groups as group (group.id)} +function create_each_block_3(key_1, ctx) { + let button; + let div; + let t_value = /*group*/ ctx[70].emoji + ""; + let t; + let button_aria_controls_value; + let button_aria_label_value; + let button_aria_selected_value; + let button_title_value; + let mounted; + let dispose; + + function click_handler() { + return /*click_handler*/ ctx[50](/*group*/ ctx[70]); + } + + return { + key: key_1, + first: null, + c() { + button = element("button"); + div = element("div"); + t = text(t_value); + attr(div, "class", "nav-emoji emoji"); + attr(button, "role", "tab"); + attr(button, "class", "nav-button"); + attr(button, "aria-controls", button_aria_controls_value = "tab-" + /*group*/ ctx[70].id); + attr(button, "aria-label", button_aria_label_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name]); + attr(button, "aria-selected", button_aria_selected_value = !/*searchMode*/ ctx[4] && /*currentGroup*/ ctx[13].id === /*group*/ ctx[70].id); + attr(button, "title", button_title_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name]); + this.first = button; + }, + m(target, anchor) { + insert(target, button, anchor); + append(button, div); + append(div, t); + + if (!mounted) { + dispose = listen(button, "click", click_handler); + mounted = true; + } + }, + p(new_ctx, dirty) { + ctx = new_ctx; + if (dirty[0] & /*groups*/ 4096 && t_value !== (t_value = /*group*/ ctx[70].emoji + "")) set_data(t, t_value); + + if (dirty[0] & /*groups*/ 4096 && button_aria_controls_value !== (button_aria_controls_value = "tab-" + /*group*/ ctx[70].id)) { + attr(button, "aria-controls", button_aria_controls_value); + } + + if (dirty[0] & /*i18n, groups*/ 4097 && button_aria_label_value !== (button_aria_label_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name])) { + attr(button, "aria-label", button_aria_label_value); + } + + if (dirty[0] & /*searchMode, currentGroup, groups*/ 12304 && button_aria_selected_value !== (button_aria_selected_value = !/*searchMode*/ ctx[4] && /*currentGroup*/ ctx[13].id === /*group*/ ctx[70].id)) { + attr(button, "aria-selected", button_aria_selected_value); + } + + if (dirty[0] & /*i18n, groups*/ 4097 && button_title_value !== (button_title_value = /*i18n*/ ctx[0].categories[/*group*/ ctx[70].name])) { + attr(button, "title", button_title_value); + } + }, + d(detaching) { + if (detaching) detach(button); + mounted = false; + dispose(); + } + }; +} + +// (94:100) {:else} +function create_else_block_1(ctx) { + let img; + let img_src_value; + + return { + c() { + img = element("img"); + attr(img, "class", "custom-emoji"); + if (!src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) attr(img, "src", img_src_value); + attr(img, "alt", ""); + attr(img, "loading", "lazy"); + }, + m(target, anchor) { + insert(target, img, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && !src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) { + attr(img, "src", img_src_value); + } + }, + d(detaching) { + if (detaching) detach(img); + } + }; +} + +// (94:40) {#if emoji.unicode} +function create_if_block_1(ctx) { + let t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + ""; + let t; + + return { + c() { + t = text(t_value); + }, + m(target, anchor) { + insert(target, t, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentEmojisWithCategories, currentSkinTone*/ 33024 && t_value !== (t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + "")) set_data(t, t_value); + }, + d(detaching) { + if (detaching) detach(t); + } + }; +} + +// (89:53) {#each emojiWithCategory.emojis as emoji, i (emoji.id)} +function create_each_block_2(key_1, ctx) { + let button; + let button_role_value; + let button_aria_selected_value; + let button_aria_label_value; + let button_title_value; + let button_class_value; + let button_id_value; + + function select_block_type(ctx, dirty) { + if (/*emoji*/ ctx[64].unicode) return create_if_block_1; + return create_else_block_1; + } + + let current_block_type = select_block_type(ctx); + let if_block = current_block_type(ctx); + + return { + key: key_1, + first: null, + c() { + button = element("button"); + if_block.c(); + attr(button, "role", button_role_value = /*searchMode*/ ctx[4] ? 'option' : 'menuitem'); + + attr(button, "aria-selected", button_aria_selected_value = /*searchMode*/ ctx[4] + ? /*i*/ ctx[66] == /*activeSearchItem*/ ctx[5] + : ''); + + attr(button, "aria-label", button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8])); + attr(button, "title", button_title_value = /*emoji*/ ctx[64].title); + + attr(button, "class", button_class_value = "emoji " + (/*searchMode*/ ctx[4] && /*i*/ ctx[66] === /*activeSearchItem*/ ctx[5] + ? 'active' + : '')); + + attr(button, "id", button_id_value = "emo-" + /*emoji*/ ctx[64].id); + this.first = button; + }, + m(target, anchor) { + insert(target, button, anchor); + if_block.m(button, null); + }, + p(new_ctx, dirty) { + ctx = new_ctx; + + if (current_block_type === (current_block_type = select_block_type(ctx)) && if_block) { + if_block.p(ctx, dirty); + } else { + if_block.d(1); + if_block = current_block_type(ctx); + + if (if_block) { + if_block.c(); + if_block.m(button, null); + } + } + + if (dirty[0] & /*searchMode*/ 16 && button_role_value !== (button_role_value = /*searchMode*/ ctx[4] ? 'option' : 'menuitem')) { + attr(button, "role", button_role_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem*/ 32816 && button_aria_selected_value !== (button_aria_selected_value = /*searchMode*/ ctx[4] + ? /*i*/ ctx[66] == /*activeSearchItem*/ ctx[5] + : '')) { + attr(button, "aria-selected", button_aria_selected_value); + } + + if (dirty[0] & /*currentEmojisWithCategories, currentSkinTone*/ 33024 && button_aria_label_value !== (button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]))) { + attr(button, "aria-label", button_aria_label_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && button_title_value !== (button_title_value = /*emoji*/ ctx[64].title)) { + attr(button, "title", button_title_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem*/ 32816 && button_class_value !== (button_class_value = "emoji " + (/*searchMode*/ ctx[4] && /*i*/ ctx[66] === /*activeSearchItem*/ ctx[5] + ? 'active' + : ''))) { + attr(button, "class", button_class_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && button_id_value !== (button_id_value = "emo-" + /*emoji*/ ctx[64].id)) { + attr(button, "id", button_id_value); + } + }, + d(detaching) { + if (detaching) detach(button); + if_block.d(); + } + }; +} + +// (70:36) {#each currentEmojisWithCategories as emojiWithCategory, i (emojiWithCategory.category)} +function create_each_block_1(key_1, ctx) { + let div0; + + let t_value = (/*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*emojiWithCategory*/ ctx[67].category + ? /*emojiWithCategory*/ ctx[67].category + : /*currentEmojisWithCategories*/ ctx[15].length > 1 + ? /*i18n*/ ctx[0].categories.custom + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name]) + ""; + + let t; + let div0_id_value; + let div0_class_value; + let div1; + let each_blocks = []; + let each_1_lookup = new Map_1(); + let div1_role_value; + let div1_aria_labelledby_value; + let div1_id_value; + let each_value_2 = /*emojiWithCategory*/ ctx[67].emojis; + const get_key = ctx => /*emoji*/ ctx[64].id; + + for (let i = 0; i < each_value_2.length; i += 1) { + let child_ctx = get_each_context_2(ctx, each_value_2, i); + let key = get_key(child_ctx); + each_1_lookup.set(key, each_blocks[i] = create_each_block_2(key, child_ctx)); + } + + return { + key: key_1, + first: null, + c() { + div0 = element("div"); + t = text(t_value); + div1 = element("div"); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].c(); + } + + attr(div0, "id", div0_id_value = "menu-label-" + /*i*/ ctx[66]); + + attr(div0, "class", div0_class_value = "category " + (/*currentEmojisWithCategories*/ ctx[15].length === 1 && /*currentEmojisWithCategories*/ ctx[15][0].category === '' + ? 'gone' + : '')); + + attr(div0, "aria-hidden", "true"); + attr(div1, "class", "emoji-menu"); + attr(div1, "role", div1_role_value = /*searchMode*/ ctx[4] ? 'listbox' : 'menu'); + attr(div1, "aria-labelledby", div1_aria_labelledby_value = "menu-label-" + /*i*/ ctx[66]); + attr(div1, "id", div1_id_value = /*searchMode*/ ctx[4] ? 'search-results' : ''); + this.first = div0; + }, + m(target, anchor) { + insert(target, div0, anchor); + append(div0, t); + insert(target, div1, anchor); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].m(div1, null); + } + }, + p(new_ctx, dirty) { + ctx = new_ctx; + + if (dirty[0] & /*searchMode, i18n, currentEmojisWithCategories, currentGroup*/ 40977 && t_value !== (t_value = (/*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*emojiWithCategory*/ ctx[67].category + ? /*emojiWithCategory*/ ctx[67].category + : /*currentEmojisWithCategories*/ ctx[15].length > 1 + ? /*i18n*/ ctx[0].categories.custom + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name]) + "")) set_data(t, t_value); + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && div0_id_value !== (div0_id_value = "menu-label-" + /*i*/ ctx[66])) { + attr(div0, "id", div0_id_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && div0_class_value !== (div0_class_value = "category " + (/*currentEmojisWithCategories*/ ctx[15].length === 1 && /*currentEmojisWithCategories*/ ctx[15][0].category === '' + ? 'gone' + : ''))) { + attr(div0, "class", div0_class_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem, labelWithSkin, currentSkinTone, unicodeWithSkin*/ 402686256) { + each_value_2 = /*emojiWithCategory*/ ctx[67].emojis; + each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value_2, each_1_lookup, div1, destroy_block, create_each_block_2, null, get_each_context_2); + } + + if (dirty[0] & /*searchMode*/ 16 && div1_role_value !== (div1_role_value = /*searchMode*/ ctx[4] ? 'listbox' : 'menu')) { + attr(div1, "role", div1_role_value); + } + + if (dirty[0] & /*currentEmojisWithCategories*/ 32768 && div1_aria_labelledby_value !== (div1_aria_labelledby_value = "menu-label-" + /*i*/ ctx[66])) { + attr(div1, "aria-labelledby", div1_aria_labelledby_value); + } + + if (dirty[0] & /*searchMode*/ 16 && div1_id_value !== (div1_id_value = /*searchMode*/ ctx[4] ? 'search-results' : '')) { + attr(div1, "id", div1_id_value); + } + }, + d(detaching) { + if (detaching) detach(div0); + if (detaching) detach(div1); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].d(); + } + } + }; +} + +// (103:94) {:else} +function create_else_block(ctx) { + let img; + let img_src_value; + + return { + c() { + img = element("img"); + attr(img, "class", "custom-emoji"); + if (!src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) attr(img, "src", img_src_value); + attr(img, "alt", ""); + attr(img, "loading", "lazy"); + }, + m(target, anchor) { + insert(target, img, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentFavorites*/ 1024 && !src_url_equal(img.src, img_src_value = /*emoji*/ ctx[64].url)) { + attr(img, "src", img_src_value); + } + }, + d(detaching) { + if (detaching) detach(img); + } + }; +} + +// (103:34) {#if emoji.unicode} +function create_if_block(ctx) { + let t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + ""; + let t; + + return { + c() { + t = text(t_value); + }, + m(target, anchor) { + insert(target, t, anchor); + }, + p(ctx, dirty) { + if (dirty[0] & /*currentFavorites, currentSkinTone*/ 1280 && t_value !== (t_value = /*unicodeWithSkin*/ ctx[27](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]) + "")) set_data(t, t_value); + }, + d(detaching) { + if (detaching) detach(t); + } + }; +} + +// (99:102) {#each currentFavorites as emoji, i (emoji.id)} +function create_each_block(key_1, ctx) { + let button; + let button_aria_label_value; + let button_title_value; + let button_id_value; + + function select_block_type_1(ctx, dirty) { + if (/*emoji*/ ctx[64].unicode) return create_if_block; + return create_else_block; + } + + let current_block_type = select_block_type_1(ctx); + let if_block = current_block_type(ctx); + + return { + key: key_1, + first: null, + c() { + button = element("button"); + if_block.c(); + attr(button, "role", "menuitem"); + attr(button, "aria-label", button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8])); + attr(button, "title", button_title_value = /*emoji*/ ctx[64].title); + attr(button, "class", "emoji"); + attr(button, "id", button_id_value = "fav-" + /*emoji*/ ctx[64].id); + this.first = button; + }, + m(target, anchor) { + insert(target, button, anchor); + if_block.m(button, null); + }, + p(new_ctx, dirty) { + ctx = new_ctx; + + if (current_block_type === (current_block_type = select_block_type_1(ctx)) && if_block) { + if_block.p(ctx, dirty); + } else { + if_block.d(1); + if_block = current_block_type(ctx); + + if (if_block) { + if_block.c(); + if_block.m(button, null); + } + } + + if (dirty[0] & /*currentFavorites, currentSkinTone*/ 1280 && button_aria_label_value !== (button_aria_label_value = /*labelWithSkin*/ ctx[28](/*emoji*/ ctx[64], /*currentSkinTone*/ ctx[8]))) { + attr(button, "aria-label", button_aria_label_value); + } + + if (dirty[0] & /*currentFavorites*/ 1024 && button_title_value !== (button_title_value = /*emoji*/ ctx[64].title)) { + attr(button, "title", button_title_value); + } + + if (dirty[0] & /*currentFavorites*/ 1024 && button_id_value !== (button_id_value = "fav-" + /*emoji*/ ctx[64].id)) { + attr(button, "id", button_id_value); + } + }, + d(detaching) { + if (detaching) detach(button); + if_block.d(); + } + }; +} + +function create_fragment(ctx) { + let section; + let div0; + let div4; + let div1; + let input; + let input_placeholder_value; + let input_aria_expanded_value; + let input_aria_activedescendant_value; + let label; + let t0_value = /*i18n*/ ctx[0].searchLabel + ""; + let t0; + let span0; + let t1_value = /*i18n*/ ctx[0].searchDescription + ""; + let t1; + let div2; + let button0; + let t2; + let button0_class_value; + let div2_class_value; + let span1; + let t3_value = /*i18n*/ ctx[0].skinToneDescription + ""; + let t3; + let div3; + let each_blocks_3 = []; + let each0_lookup = new Map_1(); + let div3_class_value; + let div3_aria_label_value; + let div3_aria_activedescendant_value; + let div3_aria_hidden_value; + let div5; + let each_blocks_2 = []; + let each1_lookup = new Map_1(); + let div5_aria_label_value; + let div7; + let div6; + let div8; + let t4; + let div8_class_value; + let div10; + let div9; + let each_blocks_1 = []; + let each2_lookup = new Map_1(); + let div10_class_value; + let div10_role_value; + let div10_aria_label_value; + let div10_id_value; + let div11; + let each_blocks = []; + let each3_lookup = new Map_1(); + let div11_class_value; + let div11_aria_label_value; + let button1; + let section_aria_label_value; + let mounted; + let dispose; + let each_value_4 = /*skinTones*/ ctx[9]; + const get_key = ctx => /*skinTone*/ ctx[73]; + + for (let i = 0; i < each_value_4.length; i += 1) { + let child_ctx = get_each_context_4(ctx, each_value_4, i); + let key = get_key(child_ctx); + each0_lookup.set(key, each_blocks_3[i] = create_each_block_4(key, child_ctx)); + } + + let each_value_3 = /*groups*/ ctx[12]; + const get_key_1 = ctx => /*group*/ ctx[70].id; + + for (let i = 0; i < each_value_3.length; i += 1) { + let child_ctx = get_each_context_3(ctx, each_value_3, i); + let key = get_key_1(child_ctx); + each1_lookup.set(key, each_blocks_2[i] = create_each_block_3(key, child_ctx)); + } + + let each_value_1 = /*currentEmojisWithCategories*/ ctx[15]; + const get_key_2 = ctx => /*emojiWithCategory*/ ctx[67].category; + + for (let i = 0; i < each_value_1.length; i += 1) { + let child_ctx = get_each_context_1(ctx, each_value_1, i); + let key = get_key_2(child_ctx); + each2_lookup.set(key, each_blocks_1[i] = create_each_block_1(key, child_ctx)); + } + + let each_value = /*currentFavorites*/ ctx[10]; + const get_key_3 = ctx => /*emoji*/ ctx[64].id; + + for (let i = 0; i < each_value.length; i += 1) { + let child_ctx = get_each_context(ctx, each_value, i); + let key = get_key_3(child_ctx); + each3_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx)); + } + + return { + c() { + section = element("section"); + div0 = element("div"); + div4 = element("div"); + div1 = element("div"); + input = element("input"); + label = element("label"); + t0 = text(t0_value); + span0 = element("span"); + t1 = text(t1_value); + div2 = element("div"); + button0 = element("button"); + t2 = text(/*skinToneButtonText*/ ctx[21]); + span1 = element("span"); + t3 = text(t3_value); + div3 = element("div"); + + for (let i = 0; i < each_blocks_3.length; i += 1) { + each_blocks_3[i].c(); + } + + div5 = element("div"); + + for (let i = 0; i < each_blocks_2.length; i += 1) { + each_blocks_2[i].c(); + } + + div7 = element("div"); + div6 = element("div"); + div8 = element("div"); + t4 = text(/*message*/ ctx[18]); + div10 = element("div"); + div9 = element("div"); + + for (let i = 0; i < each_blocks_1.length; i += 1) { + each_blocks_1[i].c(); + } + + div11 = element("div"); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].c(); + } + + button1 = element("button"); + button1.textContent = "😀"; + attr(div0, "class", "pad-top"); + attr(input, "id", "search"); + attr(input, "class", "search"); + attr(input, "type", "search"); + attr(input, "role", "combobox"); + attr(input, "enterkeyhint", "search"); + attr(input, "placeholder", input_placeholder_value = /*i18n*/ ctx[0].searchLabel); + attr(input, "autocapitalize", "none"); + attr(input, "autocomplete", "off"); + attr(input, "spellcheck", "true"); + attr(input, "aria-expanded", input_aria_expanded_value = !!(/*searchMode*/ ctx[4] && /*currentEmojis*/ ctx[1].length)); + attr(input, "aria-controls", "search-results"); + attr(input, "aria-describedby", "search-description"); + attr(input, "aria-autocomplete", "list"); + + attr(input, "aria-activedescendant", input_aria_activedescendant_value = /*activeSearchItemId*/ ctx[26] + ? `emo-${/*activeSearchItemId*/ ctx[26]}` + : ''); + + attr(label, "class", "sr-only"); + attr(label, "for", "search"); + attr(span0, "id", "search-description"); + attr(span0, "class", "sr-only"); + attr(div1, "class", "search-wrapper"); + attr(button0, "id", "skintone-button"); + attr(button0, "class", button0_class_value = "emoji " + (/*skinTonePickerExpanded*/ ctx[6] ? 'hide-focus' : '')); + attr(button0, "aria-label", /*skinToneButtonLabel*/ ctx[23]); + attr(button0, "title", /*skinToneButtonLabel*/ ctx[23]); + attr(button0, "aria-describedby", "skintone-description"); + attr(button0, "aria-haspopup", "listbox"); + attr(button0, "aria-expanded", /*skinTonePickerExpanded*/ ctx[6]); + attr(button0, "aria-controls", "skintone-list"); + + attr(div2, "class", div2_class_value = "skintone-button-wrapper " + (/*skinTonePickerExpandedAfterAnimation*/ ctx[19] + ? 'expanded' + : '')); + + attr(span1, "id", "skintone-description"); + attr(span1, "class", "sr-only"); + attr(div3, "id", "skintone-list"); + + attr(div3, "class", div3_class_value = "skintone-list " + (/*skinTonePickerExpanded*/ ctx[6] + ? '' + : 'hidden no-animate')); + + set_style(div3, "transform", "translateY(" + (/*skinTonePickerExpanded*/ ctx[6] + ? 0 + : 'calc(-1 * var(--num-skintones) * var(--total-emoji-size))') + ")"); + + attr(div3, "role", "listbox"); + attr(div3, "aria-label", div3_aria_label_value = /*i18n*/ ctx[0].skinTonesLabel); + attr(div3, "aria-activedescendant", div3_aria_activedescendant_value = "skintone-" + /*activeSkinTone*/ ctx[20]); + attr(div3, "aria-hidden", div3_aria_hidden_value = !/*skinTonePickerExpanded*/ ctx[6]); + attr(div4, "class", "search-row"); + attr(div5, "class", "nav"); + attr(div5, "role", "tablist"); + set_style(div5, "grid-template-columns", "repeat(" + /*groups*/ ctx[12].length + ", 1fr)"); + attr(div5, "aria-label", div5_aria_label_value = /*i18n*/ ctx[0].categoriesLabel); + attr(div6, "class", "indicator"); + set_style(div6, "transform", "translateX(" + (/*isRtl*/ ctx[24] ? -1 : 1) * /*currentGroupIndex*/ ctx[11] * 100 + "%)"); + attr(div7, "class", "indicator-wrapper"); + attr(div8, "class", div8_class_value = "message " + (/*message*/ ctx[18] ? '' : 'gone')); + attr(div8, "role", "alert"); + attr(div8, "aria-live", "polite"); + + attr(div10, "class", div10_class_value = "tabpanel " + (!/*databaseLoaded*/ ctx[14] || /*message*/ ctx[18] + ? 'gone' + : '')); + + attr(div10, "role", div10_role_value = /*searchMode*/ ctx[4] ? 'region' : 'tabpanel'); + + attr(div10, "aria-label", div10_aria_label_value = /*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name]); + + attr(div10, "id", div10_id_value = /*searchMode*/ ctx[4] + ? '' + : `tab-${/*currentGroup*/ ctx[13].id}`); + + attr(div10, "tabindex", "0"); + attr(div11, "class", div11_class_value = "favorites emoji-menu " + (/*message*/ ctx[18] ? 'gone' : '')); + attr(div11, "role", "menu"); + attr(div11, "aria-label", div11_aria_label_value = /*i18n*/ ctx[0].favoritesLabel); + set_style(div11, "padding-inline-end", /*scrollbarWidth*/ ctx[25] + "px"); + attr(button1, "aria-hidden", "true"); + attr(button1, "tabindex", "-1"); + attr(button1, "class", "abs-pos hidden emoji"); + attr(section, "class", "picker"); + attr(section, "aria-label", section_aria_label_value = /*i18n*/ ctx[0].regionLabel); + attr(section, "style", /*pickerStyle*/ ctx[22]); + }, + m(target, anchor) { + insert(target, section, anchor); + append(section, div0); + append(section, div4); + append(div4, div1); + append(div1, input); + set_input_value(input, /*rawSearchText*/ ctx[2]); + append(div1, label); + append(label, t0); + append(div1, span0); + append(span0, t1); + append(div4, div2); + append(div2, button0); + append(button0, t2); + append(div4, span1); + append(span1, t3); + append(div4, div3); + + for (let i = 0; i < each_blocks_3.length; i += 1) { + each_blocks_3[i].m(div3, null); + } + + /*div3_binding*/ ctx[49](div3); + append(section, div5); + + for (let i = 0; i < each_blocks_2.length; i += 1) { + each_blocks_2[i].m(div5, null); + } + + append(section, div7); + append(div7, div6); + append(section, div8); + append(div8, t4); + append(section, div10); + append(div10, div9); + + for (let i = 0; i < each_blocks_1.length; i += 1) { + each_blocks_1[i].m(div9, null); + } + + /*div10_binding*/ ctx[51](div10); + append(section, div11); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].m(div11, null); + } + + append(section, button1); + /*button1_binding*/ ctx[52](button1); + /*section_binding*/ ctx[53](section); + + if (!mounted) { + dispose = [ + listen(input, "input", /*input_input_handler*/ ctx[48]), + listen(input, "keydown", /*onSearchKeydown*/ ctx[30]), + listen(button0, "click", /*onClickSkinToneButton*/ ctx[35]), + listen(div3, "focusout", /*onSkinToneOptionsFocusOut*/ ctx[38]), + listen(div3, "click", /*onSkinToneOptionsClick*/ ctx[34]), + listen(div3, "keydown", /*onSkinToneOptionsKeydown*/ ctx[36]), + listen(div3, "keyup", /*onSkinToneOptionsKeyup*/ ctx[37]), + listen(div5, "keydown", /*onNavKeydown*/ ctx[32]), + action_destroyer(/*calculateEmojiGridStyle*/ ctx[29].call(null, div9)), + listen(div10, "click", /*onEmojiClick*/ ctx[33]), + listen(div11, "click", /*onEmojiClick*/ ctx[33]) + ]; + + mounted = true; + } + }, + p(ctx, dirty) { + if (dirty[0] & /*i18n*/ 1 && input_placeholder_value !== (input_placeholder_value = /*i18n*/ ctx[0].searchLabel)) { + attr(input, "placeholder", input_placeholder_value); + } + + if (dirty[0] & /*searchMode, currentEmojis*/ 18 && input_aria_expanded_value !== (input_aria_expanded_value = !!(/*searchMode*/ ctx[4] && /*currentEmojis*/ ctx[1].length))) { + attr(input, "aria-expanded", input_aria_expanded_value); + } + + if (dirty[0] & /*activeSearchItemId*/ 67108864 && input_aria_activedescendant_value !== (input_aria_activedescendant_value = /*activeSearchItemId*/ ctx[26] + ? `emo-${/*activeSearchItemId*/ ctx[26]}` + : '')) { + attr(input, "aria-activedescendant", input_aria_activedescendant_value); + } + + if (dirty[0] & /*rawSearchText*/ 4) { + set_input_value(input, /*rawSearchText*/ ctx[2]); + } + + if (dirty[0] & /*i18n*/ 1 && t0_value !== (t0_value = /*i18n*/ ctx[0].searchLabel + "")) set_data(t0, t0_value); + if (dirty[0] & /*i18n*/ 1 && t1_value !== (t1_value = /*i18n*/ ctx[0].searchDescription + "")) set_data(t1, t1_value); + if (dirty[0] & /*skinToneButtonText*/ 2097152) set_data(t2, /*skinToneButtonText*/ ctx[21]); + + if (dirty[0] & /*skinTonePickerExpanded*/ 64 && button0_class_value !== (button0_class_value = "emoji " + (/*skinTonePickerExpanded*/ ctx[6] ? 'hide-focus' : ''))) { + attr(button0, "class", button0_class_value); + } + + if (dirty[0] & /*skinToneButtonLabel*/ 8388608) { + attr(button0, "aria-label", /*skinToneButtonLabel*/ ctx[23]); + } + + if (dirty[0] & /*skinToneButtonLabel*/ 8388608) { + attr(button0, "title", /*skinToneButtonLabel*/ ctx[23]); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64) { + attr(button0, "aria-expanded", /*skinTonePickerExpanded*/ ctx[6]); + } + + if (dirty[0] & /*skinTonePickerExpandedAfterAnimation*/ 524288 && div2_class_value !== (div2_class_value = "skintone-button-wrapper " + (/*skinTonePickerExpandedAfterAnimation*/ ctx[19] + ? 'expanded' + : ''))) { + attr(div2, "class", div2_class_value); + } + + if (dirty[0] & /*i18n*/ 1 && t3_value !== (t3_value = /*i18n*/ ctx[0].skinToneDescription + "")) set_data(t3, t3_value); + + if (dirty[0] & /*skinTones, activeSkinTone, i18n*/ 1049089) { + each_value_4 = /*skinTones*/ ctx[9]; + each_blocks_3 = update_keyed_each(each_blocks_3, dirty, get_key, 1, ctx, each_value_4, each0_lookup, div3, destroy_block, create_each_block_4, null, get_each_context_4); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64 && div3_class_value !== (div3_class_value = "skintone-list " + (/*skinTonePickerExpanded*/ ctx[6] + ? '' + : 'hidden no-animate'))) { + attr(div3, "class", div3_class_value); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64) { + set_style(div3, "transform", "translateY(" + (/*skinTonePickerExpanded*/ ctx[6] + ? 0 + : 'calc(-1 * var(--num-skintones) * var(--total-emoji-size))') + ")"); + } + + if (dirty[0] & /*i18n*/ 1 && div3_aria_label_value !== (div3_aria_label_value = /*i18n*/ ctx[0].skinTonesLabel)) { + attr(div3, "aria-label", div3_aria_label_value); + } + + if (dirty[0] & /*activeSkinTone*/ 1048576 && div3_aria_activedescendant_value !== (div3_aria_activedescendant_value = "skintone-" + /*activeSkinTone*/ ctx[20])) { + attr(div3, "aria-activedescendant", div3_aria_activedescendant_value); + } + + if (dirty[0] & /*skinTonePickerExpanded*/ 64 && div3_aria_hidden_value !== (div3_aria_hidden_value = !/*skinTonePickerExpanded*/ ctx[6])) { + attr(div3, "aria-hidden", div3_aria_hidden_value); + } + + if (dirty[0] & /*groups, i18n, searchMode, currentGroup*/ 12305 | dirty[1] & /*onNavClick*/ 1) { + each_value_3 = /*groups*/ ctx[12]; + each_blocks_2 = update_keyed_each(each_blocks_2, dirty, get_key_1, 1, ctx, each_value_3, each1_lookup, div5, destroy_block, create_each_block_3, null, get_each_context_3); + } + + if (dirty[0] & /*groups*/ 4096) { + set_style(div5, "grid-template-columns", "repeat(" + /*groups*/ ctx[12].length + ", 1fr)"); + } + + if (dirty[0] & /*i18n*/ 1 && div5_aria_label_value !== (div5_aria_label_value = /*i18n*/ ctx[0].categoriesLabel)) { + attr(div5, "aria-label", div5_aria_label_value); + } + + if (dirty[0] & /*isRtl, currentGroupIndex*/ 16779264) { + set_style(div6, "transform", "translateX(" + (/*isRtl*/ ctx[24] ? -1 : 1) * /*currentGroupIndex*/ ctx[11] * 100 + "%)"); + } + + if (dirty[0] & /*message*/ 262144) set_data(t4, /*message*/ ctx[18]); + + if (dirty[0] & /*message*/ 262144 && div8_class_value !== (div8_class_value = "message " + (/*message*/ ctx[18] ? '' : 'gone'))) { + attr(div8, "class", div8_class_value); + } + + if (dirty[0] & /*searchMode, currentEmojisWithCategories, activeSearchItem, labelWithSkin, currentSkinTone, unicodeWithSkin, i18n, currentGroup*/ 402694449) { + each_value_1 = /*currentEmojisWithCategories*/ ctx[15]; + each_blocks_1 = update_keyed_each(each_blocks_1, dirty, get_key_2, 1, ctx, each_value_1, each2_lookup, div9, destroy_block, create_each_block_1, null, get_each_context_1); + } + + if (dirty[0] & /*databaseLoaded, message*/ 278528 && div10_class_value !== (div10_class_value = "tabpanel " + (!/*databaseLoaded*/ ctx[14] || /*message*/ ctx[18] + ? 'gone' + : ''))) { + attr(div10, "class", div10_class_value); + } + + if (dirty[0] & /*searchMode*/ 16 && div10_role_value !== (div10_role_value = /*searchMode*/ ctx[4] ? 'region' : 'tabpanel')) { + attr(div10, "role", div10_role_value); + } + + if (dirty[0] & /*searchMode, i18n, currentGroup*/ 8209 && div10_aria_label_value !== (div10_aria_label_value = /*searchMode*/ ctx[4] + ? /*i18n*/ ctx[0].searchResultsLabel + : /*i18n*/ ctx[0].categories[/*currentGroup*/ ctx[13].name])) { + attr(div10, "aria-label", div10_aria_label_value); + } + + if (dirty[0] & /*searchMode, currentGroup*/ 8208 && div10_id_value !== (div10_id_value = /*searchMode*/ ctx[4] + ? '' + : `tab-${/*currentGroup*/ ctx[13].id}`)) { + attr(div10, "id", div10_id_value); + } + + if (dirty[0] & /*labelWithSkin, currentFavorites, currentSkinTone, unicodeWithSkin*/ 402654464) { + each_value = /*currentFavorites*/ ctx[10]; + each_blocks = update_keyed_each(each_blocks, dirty, get_key_3, 1, ctx, each_value, each3_lookup, div11, destroy_block, create_each_block, null, get_each_context); + } + + if (dirty[0] & /*message*/ 262144 && div11_class_value !== (div11_class_value = "favorites emoji-menu " + (/*message*/ ctx[18] ? 'gone' : ''))) { + attr(div11, "class", div11_class_value); + } + + if (dirty[0] & /*i18n*/ 1 && div11_aria_label_value !== (div11_aria_label_value = /*i18n*/ ctx[0].favoritesLabel)) { + attr(div11, "aria-label", div11_aria_label_value); + } + + if (dirty[0] & /*scrollbarWidth*/ 33554432) { + set_style(div11, "padding-inline-end", /*scrollbarWidth*/ ctx[25] + "px"); + } + + if (dirty[0] & /*i18n*/ 1 && section_aria_label_value !== (section_aria_label_value = /*i18n*/ ctx[0].regionLabel)) { + attr(section, "aria-label", section_aria_label_value); + } + + if (dirty[0] & /*pickerStyle*/ 4194304) { + attr(section, "style", /*pickerStyle*/ ctx[22]); + } + }, + i: noop, + o: noop, + d(detaching) { + if (detaching) detach(section); + + for (let i = 0; i < each_blocks_3.length; i += 1) { + each_blocks_3[i].d(); + } + + /*div3_binding*/ ctx[49](null); + + for (let i = 0; i < each_blocks_2.length; i += 1) { + each_blocks_2[i].d(); + } + + for (let i = 0; i < each_blocks_1.length; i += 1) { + each_blocks_1[i].d(); + } + + /*div10_binding*/ ctx[51](null); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].d(); + } + + /*button1_binding*/ ctx[52](null); + /*section_binding*/ ctx[53](null); + mounted = false; + run_all(dispose); + } + }; +} + +function instance($$self, $$props, $$invalidate) { + let { skinToneEmoji } = $$props; + let { i18n } = $$props; + let { database } = $$props; + let { customEmoji } = $$props; + let { customCategorySorting } = $$props; + let { emojiVersion } = $$props; + + // private + let initialLoad = true; + + let currentEmojis = []; + let currentEmojisWithCategories = []; // eslint-disable-line no-unused-vars + let rawSearchText = ''; + let searchText = ''; + let rootElement; + let baselineEmoji; + let tabpanelElement; + let searchMode = false; // eslint-disable-line no-unused-vars + let activeSearchItem = -1; + let message; // eslint-disable-line no-unused-vars + let skinTonePickerExpanded = false; + let skinTonePickerExpandedAfterAnimation = false; // eslint-disable-line no-unused-vars + let skinToneDropdown; + let currentSkinTone = 0; + let activeSkinTone = 0; + let skinToneButtonText; // eslint-disable-line no-unused-vars + let pickerStyle; // eslint-disable-line no-unused-vars + let skinToneButtonLabel = ''; // eslint-disable-line no-unused-vars + let skinTones = []; + let currentFavorites = []; // eslint-disable-line no-unused-vars + let defaultFavoriteEmojis; + let numColumns = DEFAULT_NUM_COLUMNS; + let isRtl = false; // eslint-disable-line no-unused-vars + let scrollbarWidth = 0; // eslint-disable-line no-unused-vars + let currentGroupIndex = 0; + let groups$1 = groups; + let currentGroup; + let databaseLoaded = false; // eslint-disable-line no-unused-vars + let activeSearchItemId; // eslint-disable-line no-unused-vars + + // + // Utils/helpers + // + const focus = id => { + rootElement.getRootNode().getElementById(id).focus(); + }; + + // fire a custom event that crosses the shadow boundary + const fireEvent = (name, detail) => { + rootElement.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); + }; + + // eslint-disable-next-line no-unused-vars + const unicodeWithSkin = (emoji, currentSkinTone) => currentSkinTone && emoji.skins && emoji.skins[currentSkinTone] || emoji.unicode; + + // eslint-disable-next-line no-unused-vars + const labelWithSkin = (emoji, currentSkinTone) => uniq([ + emoji.name || unicodeWithSkin(emoji, currentSkinTone), + ...emoji.shortcodes || [] + ]).join(', '); + + // Detect a skintone option button + const isSkinToneOption = element => (/^skintone-/).test(element.id); + + // + // Determine the emoji support level (in requestIdleCallback) + // + onMount(() => { + if (!emojiVersion) { + detectEmojiSupportLevel().then(level => { + // Can't actually test emoji support in Jest/JSDom, emoji never render in color in Cairo + /* istanbul ignore next */ + if (!level) { + $$invalidate(18, message = i18n.emojiUnsupportedMessage); + } + }); + } + }); + + // + // Calculate the width of the emoji grid. This serves two purposes: + // 1) Re-calculate the --num-columns var because it may have changed + // 2) Re-calculate the scrollbar width because it may have changed + // (i.e. because the number of items changed) + // 3) Re-calculate whether we're in RTL mode or not. + // + // The benefit of doing this in one place is to align with rAF/ResizeObserver + // and do all the calculations in one go. RTL vs LTR is not strictly width-related, + // but since we're already reading the style here, and since it's already aligned with + // the rAF loop, this is the most appropriate place to do it perf-wise. + // + // eslint-disable-next-line no-unused-vars + function calculateEmojiGridStyle(node) { + return calculateWidth(node, width => { + /* istanbul ignore next */ + if ("production" !== 'test') { + // jsdom throws errors for this kind of fancy stuff + // read all the style/layout calculations we need to make + const style = getComputedStyle(rootElement); + + const newNumColumns = parseInt(style.getPropertyValue('--num-columns'), 10); + const newIsRtl = style.getPropertyValue('direction') === 'rtl'; + const parentWidth = node.parentElement.getBoundingClientRect().width; + const newScrollbarWidth = parentWidth - width; + + // write to Svelte variables + $$invalidate(47, numColumns = newNumColumns); + + $$invalidate(25, scrollbarWidth = newScrollbarWidth); // eslint-disable-line no-unused-vars + $$invalidate(24, isRtl = newIsRtl); // eslint-disable-line no-unused-vars + } + }); + } + + function checkZwjSupportAndUpdate(zwjEmojisToCheck) { + const rootNode = rootElement.getRootNode(); + const emojiToDomNode = emoji => rootNode.getElementById(`emo-${emoji.id}`); + checkZwjSupport(zwjEmojisToCheck, baselineEmoji, emojiToDomNode); + + // force update + $$invalidate(1, currentEmojis = currentEmojis); // eslint-disable-line no-self-assign + } + + function isZwjSupported(emoji) { + return !emoji.unicode || !hasZwj(emoji) || supportedZwjEmojis.get(emoji.unicode); + } + + async function filterEmojisByVersion(emojis) { + const emojiSupportLevel = emojiVersion || await detectEmojiSupportLevel(); + + // !version corresponds to custom emoji + return emojis.filter(({ version }) => !version || version <= emojiSupportLevel); + } + + async function summarizeEmojis(emojis) { + return summarizeEmojisForUI(emojis, emojiVersion || await detectEmojiSupportLevel()); + } + + async function getEmojisByGroup(group) { + + // -1 is custom emoji + const emoji = group === -1 + ? customEmoji + : await database.getEmojiByGroup(group); + + return summarizeEmojis(await filterEmojisByVersion(emoji)); + } + + async function getEmojisBySearchQuery(query) { + return summarizeEmojis(await filterEmojisByVersion(await database.getEmojiBySearchQuery(query))); + } + + // eslint-disable-next-line no-unused-vars + function onSearchKeydown(event) { + if (!searchMode || !currentEmojis.length) { + return; + } + + const goToNextOrPrevious = previous => { + halt(event); + $$invalidate(5, activeSearchItem = incrementOrDecrement(previous, activeSearchItem, currentEmojis)); + }; + + switch (event.key) { + case 'ArrowDown': + return goToNextOrPrevious(false); + case 'ArrowUp': + return goToNextOrPrevious(true); + case 'Enter': + if (activeSearchItem !== -1) { + halt(event); + return clickEmoji(currentEmojis[activeSearchItem].id); + } else if (currentEmojis.length) { + $$invalidate(5, activeSearchItem = 0); + } + } + } + + // + // Handle user input on nav + // + // eslint-disable-next-line no-unused-vars + function onNavClick(group) { + $$invalidate(2, rawSearchText = ''); + $$invalidate(45, searchText = ''); + $$invalidate(5, activeSearchItem = -1); + $$invalidate(11, currentGroupIndex = groups$1.findIndex(_ => _.id === group.id)); + } + + // eslint-disable-next-line no-unused-vars + function onNavKeydown(event) { + const { target, key } = event; + + const doFocus = el => { + if (el) { + halt(event); + el.focus(); + } + }; + + switch (key) { + case 'ArrowLeft': + return doFocus(target.previousSibling); + case 'ArrowRight': + return doFocus(target.nextSibling); + case 'Home': + return doFocus(target.parentElement.firstChild); + case 'End': + return doFocus(target.parentElement.lastChild); + } + } + + // + // Handle user input on an emoji + // + async function clickEmoji(unicodeOrName) { + const emoji = await database.getEmojiByUnicodeOrName(unicodeOrName); + const emojiSummary = [...currentEmojis, ...currentFavorites].find(_ => _.id === unicodeOrName); + const skinTonedUnicode = emojiSummary.unicode && unicodeWithSkin(emojiSummary, currentSkinTone); + await database.incrementFavoriteEmojiCount(unicodeOrName); + + fireEvent('emoji-click', { + emoji, + skinTone: currentSkinTone, + ...skinTonedUnicode && { unicode: skinTonedUnicode }, + ...emojiSummary.name && { name: emojiSummary.name } + }); + } + + // eslint-disable-next-line no-unused-vars + async function onEmojiClick(event) { + const { target } = event; + + if (!target.classList.contains('emoji')) { + return; + } + + halt(event); + const id = target.id.substring(4); // replace 'emo-' or 'fav-' prefix + + /* no await */ + clickEmoji(id); + } + + // + // Handle user input on the skintone picker + // + // eslint-disable-next-line no-unused-vars + async function onSkinToneOptionsClick(event) { + const { target } = event; + + if (!isSkinToneOption(target)) { + return; + } + + halt(event); + const skinTone = parseInt(target.id.slice(9), 10); // remove 'skintone-' prefix + $$invalidate(8, currentSkinTone = skinTone); + $$invalidate(6, skinTonePickerExpanded = false); + focus('skintone-button'); + fireEvent('skin-tone-change', { skinTone }); + + /* no await */ + database.setPreferredSkinTone(skinTone); + } + + // eslint-disable-next-line no-unused-vars + async function onClickSkinToneButton(event) { + $$invalidate(6, skinTonePickerExpanded = !skinTonePickerExpanded); + $$invalidate(20, activeSkinTone = currentSkinTone); + + if (skinTonePickerExpanded) { + halt(event); + rAF(() => focus(`skintone-${activeSkinTone}`)); + } + } + + // eslint-disable-next-line no-unused-vars + function onSkinToneOptionsKeydown(event) { + if (!skinTonePickerExpanded) { + return; + } + + const changeActiveSkinTone = async nextSkinTone => { + halt(event); + $$invalidate(20, activeSkinTone = nextSkinTone); + await tick(); + focus(`skintone-${activeSkinTone}`); + }; + + switch (event.key) { + case 'ArrowUp': + return changeActiveSkinTone(incrementOrDecrement(true, activeSkinTone, skinTones)); + case 'ArrowDown': + return changeActiveSkinTone(incrementOrDecrement(false, activeSkinTone, skinTones)); + case 'Home': + return changeActiveSkinTone(0); + case 'End': + return changeActiveSkinTone(skinTones.length - 1); + case 'Enter': + // enter on keydown, space on keyup. this is just how browsers work for buttons + // https://lists.w3.org/Archives/Public/w3c-wai-ig/2019JanMar/0086.html + return onSkinToneOptionsClick(event); + case 'Escape': + halt(event); + $$invalidate(6, skinTonePickerExpanded = false); + return focus('skintone-button'); + } + } + + // eslint-disable-next-line no-unused-vars + function onSkinToneOptionsKeyup(event) { + if (!skinTonePickerExpanded) { + return; + } + + switch (event.key) { + case ' ': + // enter on keydown, space on keyup. this is just how browsers work for buttons + // https://lists.w3.org/Archives/Public/w3c-wai-ig/2019JanMar/0086.html + return onSkinToneOptionsClick(event); + } + } + + // eslint-disable-next-line no-unused-vars + async function onSkinToneOptionsFocusOut(event) { + // On blur outside of the skintone options, collapse the skintone picker. + // Except if focus is just moving to another skintone option, e.g. pressing up/down to change focus + const { relatedTarget } = event; + + if (!relatedTarget || !isSkinToneOption(relatedTarget)) { + $$invalidate(6, skinTonePickerExpanded = false); + } + } + + function input_input_handler() { + rawSearchText = this.value; + $$invalidate(2, rawSearchText); + } + + function div3_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + skinToneDropdown = $$value; + $$invalidate(7, skinToneDropdown); + }); + } + + const click_handler = group => onNavClick(group); + + function div10_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + tabpanelElement = $$value; + $$invalidate(3, tabpanelElement); + }); + } + + function button1_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + baselineEmoji = $$value; + $$invalidate(17, baselineEmoji); + }); + } + + function section_binding($$value) { + binding_callbacks[$$value ? 'unshift' : 'push'](() => { + rootElement = $$value; + $$invalidate(16, rootElement); + }); + } + + $$self.$$set = $$props => { + if ('skinToneEmoji' in $$props) $$invalidate(40, skinToneEmoji = $$props.skinToneEmoji); + if ('i18n' in $$props) $$invalidate(0, i18n = $$props.i18n); + if ('database' in $$props) $$invalidate(39, database = $$props.database); + if ('customEmoji' in $$props) $$invalidate(41, customEmoji = $$props.customEmoji); + if ('customCategorySorting' in $$props) $$invalidate(42, customCategorySorting = $$props.customCategorySorting); + if ('emojiVersion' in $$props) $$invalidate(43, emojiVersion = $$props.emojiVersion); + }; + + $$self.$$.update = () => { + if ($$self.$$.dirty[1] & /*customEmoji, database*/ 1280) { + /* eslint-enable no-unused-vars */ + // + // Set or update the customEmoji + // + { + if (customEmoji && database) { + $$invalidate(39, database.customEmoji = customEmoji, database); + } + } + } + + if ($$self.$$.dirty[0] & /*i18n*/ 1 | $$self.$$.dirty[1] & /*database*/ 256) { + // + // Set or update the database object + // + { + // show a Loading message if it takes a long time, or show an error if there's a network/IDB error + async function handleDatabaseLoading() { + let showingLoadingMessage = false; + + const timeoutHandle = setTimeout( + () => { + showingLoadingMessage = true; + $$invalidate(18, message = i18n.loadingMessage); + }, + TIMEOUT_BEFORE_LOADING_MESSAGE + ); + + try { + await database.ready(); + $$invalidate(14, databaseLoaded = true); // eslint-disable-line no-unused-vars + } catch(err) { + console.error(err); + $$invalidate(18, message = i18n.networkErrorMessage); + } finally { + clearTimeout(timeoutHandle); + + if (showingLoadingMessage) { + // Seems safer than checking the i18n string, which may change + showingLoadingMessage = false; + + $$invalidate(18, message = ''); // eslint-disable-line no-unused-vars + } + } + } + + if (database) { + /* no await */ + handleDatabaseLoading(); + } + } + } + + if ($$self.$$.dirty[0] & /*groups, currentGroupIndex*/ 6144 | $$self.$$.dirty[1] & /*customEmoji*/ 1024) { + { + if (customEmoji && customEmoji.length) { + $$invalidate(12, groups$1 = [customGroup, ...groups]); + } else if (groups$1 !== groups) { + if (currentGroupIndex) { + // If the current group is anything other than "custom" (which is first), decrement. + // This fixes the odd case where you set customEmoji, then pick a category, then unset customEmoji + $$invalidate(11, currentGroupIndex--, currentGroupIndex); + } + + $$invalidate(12, groups$1 = groups); + } + } + } + + if ($$self.$$.dirty[0] & /*rawSearchText*/ 4) { + /* eslint-enable no-unused-vars */ + // + // Handle user input on the search input + // + { + rIC(() => { + $$invalidate(45, searchText = (rawSearchText || '').trim()); // defer to avoid input delays, plus we can trim here + $$invalidate(5, activeSearchItem = -1); + }); + } + } + + if ($$self.$$.dirty[0] & /*groups, currentGroupIndex*/ 6144) { + // + // Update the current group based on the currentGroupIndex + // + $$invalidate(13, currentGroup = groups$1[currentGroupIndex]); + } + + if ($$self.$$.dirty[0] & /*databaseLoaded, currentGroup*/ 24576 | $$self.$$.dirty[1] & /*searchText*/ 16384) { + // + // Set or update the currentEmojis. Check for invalid ZWJ renderings + // (i.e. double emoji). + // + { + async function updateEmojis() { + + if (!databaseLoaded) { + $$invalidate(1, currentEmojis = []); + $$invalidate(4, searchMode = false); + } else if (searchText.length >= MIN_SEARCH_TEXT_LENGTH) { + const currentSearchText = searchText; + const newEmojis = await getEmojisBySearchQuery(currentSearchText); + + if (currentSearchText === searchText) { + // if the situation changes asynchronously, do not update + $$invalidate(1, currentEmojis = newEmojis); + + $$invalidate(4, searchMode = true); + } + } else if (currentGroup) { + const currentGroupId = currentGroup.id; + const newEmojis = await getEmojisByGroup(currentGroupId); + + if (currentGroupId === currentGroup.id) { + // if the situation changes asynchronously, do not update + $$invalidate(1, currentEmojis = newEmojis); + + $$invalidate(4, searchMode = false); + } + } + } + + /* no await */ + updateEmojis(); + } + } + + if ($$self.$$.dirty[0] & /*groups, searchMode*/ 4112) { + // + // Global styles for the entire picker + // + /* eslint-disable no-unused-vars */ + $$invalidate(22, pickerStyle = ` + --num-groups: ${groups$1.length}; + --indicator-opacity: ${searchMode ? 0 : 1}; + --num-skintones: ${NUM_SKIN_TONES};`); + } + + if ($$self.$$.dirty[0] & /*databaseLoaded*/ 16384 | $$self.$$.dirty[1] & /*database*/ 256) { + // + // Set or update the preferred skin tone + // + { + async function updatePreferredSkinTone() { + if (databaseLoaded) { + $$invalidate(8, currentSkinTone = await database.getPreferredSkinTone()); + } + } + + /* no await */ + updatePreferredSkinTone(); + } + } + + if ($$self.$$.dirty[1] & /*skinToneEmoji*/ 512) { + $$invalidate(9, skinTones = Array(NUM_SKIN_TONES).fill().map((_, i) => applySkinTone(skinToneEmoji, i))); + } + + if ($$self.$$.dirty[0] & /*skinTones, currentSkinTone*/ 768) { + /* eslint-disable no-unused-vars */ + $$invalidate(21, skinToneButtonText = skinTones[currentSkinTone]); + } + + if ($$self.$$.dirty[0] & /*i18n, currentSkinTone*/ 257) { + $$invalidate(23, skinToneButtonLabel = i18n.skinToneLabel.replace('{skinTone}', i18n.skinTones[currentSkinTone])); + } + + if ($$self.$$.dirty[0] & /*databaseLoaded*/ 16384 | $$self.$$.dirty[1] & /*database*/ 256) { + /* eslint-enable no-unused-vars */ + // + // Set or update the favorites emojis + // + { + async function updateDefaultFavoriteEmojis() { + $$invalidate(46, defaultFavoriteEmojis = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map(unicode => database.getEmojiByUnicodeOrName(unicode)))).filter(Boolean)); // filter because in Jest tests we don't have all the emoji in the DB + } + + if (databaseLoaded) { + /* no await */ + updateDefaultFavoriteEmojis(); + } + } + } + + if ($$self.$$.dirty[0] & /*databaseLoaded*/ 16384 | $$self.$$.dirty[1] & /*database, numColumns, defaultFavoriteEmojis*/ 98560) { + { + async function updateFavorites() { + const dbFavorites = await database.getTopFavoriteEmoji(numColumns); + const favorites = await summarizeEmojis(uniqBy([...dbFavorites, ...defaultFavoriteEmojis], _ => _.unicode || _.name).slice(0, numColumns)); + $$invalidate(10, currentFavorites = favorites); + } + + if (databaseLoaded && defaultFavoriteEmojis) { + /* no await */ + updateFavorites(); + } + } + } + + if ($$self.$$.dirty[0] & /*currentEmojis, tabpanelElement*/ 10 | $$self.$$.dirty[1] & /*emojiVersion*/ 4096) { + // Some emojis have their ligatures rendered as two or more consecutive emojis + // We want to treat these the same as unsupported emojis, so we compare their + // widths against the baseline widths and remove them as necessary + { + const zwjEmojisToCheck = currentEmojis.filter(emoji => emoji.unicode).filter(emoji => hasZwj(emoji) && !supportedZwjEmojis.has(emoji.unicode)); // filter custom emoji + + if (!emojiVersion && zwjEmojisToCheck.length) { + // render now, check their length later + rAF(() => checkZwjSupportAndUpdate(zwjEmojisToCheck)); + } else { + $$invalidate(1, currentEmojis = emojiVersion + ? currentEmojis + : currentEmojis.filter(isZwjSupported)); + + // Reset scroll top to 0 when emojis change + rAF(() => resetScrollTopIfPossible(tabpanelElement)); + } + } + } + + if ($$self.$$.dirty[0] & /*currentEmojis, currentFavorites*/ 1026 | $$self.$$.dirty[1] & /*initialLoad*/ 8192) { + { + // consider initialLoad to be complete when the first tabpanel and favorites are rendered + /* istanbul ignore next */ + if ("production" !== 'production' || false) { + if (currentEmojis.length && currentFavorites.length && initialLoad) { + $$invalidate(44, initialLoad = false); + requestPostAnimationFrame(() => (void 0)); + } + } + } + } + + if ($$self.$$.dirty[0] & /*searchMode, currentEmojis*/ 18 | $$self.$$.dirty[1] & /*customCategorySorting*/ 2048) { + // + // Derive currentEmojisWithCategories from currentEmojis. This is always done even if there + // are no categories, because it's just easier to code the HTML this way. + // + { + function calculateCurrentEmojisWithCategories() { + if (searchMode) { + return [{ category: '', emojis: currentEmojis }]; + } + + const categoriesToEmoji = new Map(); + + for (const emoji of currentEmojis) { + const category = emoji.category || ''; + let emojis = categoriesToEmoji.get(category); + + if (!emojis) { + emojis = []; + categoriesToEmoji.set(category, emojis); + } + + emojis.push(emoji); + } + + return [...categoriesToEmoji.entries()].map(([category, emojis]) => ({ category, emojis })).sort((a, b) => customCategorySorting(a.category, b.category)); + } + + // eslint-disable-next-line no-unused-vars + $$invalidate(15, currentEmojisWithCategories = calculateCurrentEmojisWithCategories()); + } + } + + if ($$self.$$.dirty[0] & /*activeSearchItem, currentEmojis*/ 34) { + // + // Handle active search item (i.e. pressing up or down while searching) + // + /* eslint-disable no-unused-vars */ + $$invalidate(26, activeSearchItemId = activeSearchItem !== -1 && currentEmojis[activeSearchItem].id); + } + + if ($$self.$$.dirty[0] & /*skinTonePickerExpanded, skinToneDropdown*/ 192) { + // To make the animation nicer, change the z-index of the skintone picker button + // *after* the animation has played. This makes it appear that the picker box + // is expanding "below" the button + { + if (skinTonePickerExpanded) { + skinToneDropdown.addEventListener( + 'transitionend', + () => { + $$invalidate(19, skinTonePickerExpandedAfterAnimation = true); // eslint-disable-line no-unused-vars + }, + { once: true } + ); + } else { + $$invalidate(19, skinTonePickerExpandedAfterAnimation = false); // eslint-disable-line no-unused-vars + } + } + } + }; + + return [ + i18n, + currentEmojis, + rawSearchText, + tabpanelElement, + searchMode, + activeSearchItem, + skinTonePickerExpanded, + skinToneDropdown, + currentSkinTone, + skinTones, + currentFavorites, + currentGroupIndex, + groups$1, + currentGroup, + databaseLoaded, + currentEmojisWithCategories, + rootElement, + baselineEmoji, + message, + skinTonePickerExpandedAfterAnimation, + activeSkinTone, + skinToneButtonText, + pickerStyle, + skinToneButtonLabel, + isRtl, + scrollbarWidth, + activeSearchItemId, + unicodeWithSkin, + labelWithSkin, + calculateEmojiGridStyle, + onSearchKeydown, + onNavClick, + onNavKeydown, + onEmojiClick, + onSkinToneOptionsClick, + onClickSkinToneButton, + onSkinToneOptionsKeydown, + onSkinToneOptionsKeyup, + onSkinToneOptionsFocusOut, + database, + skinToneEmoji, + customEmoji, + customCategorySorting, + emojiVersion, + initialLoad, + searchText, + defaultFavoriteEmojis, + numColumns, + input_input_handler, + div3_binding, + click_handler, + div10_binding, + button1_binding, + section_binding + ]; +} + +class Picker extends SvelteComponent { + constructor(options) { + super(); + + init( + this, + options, + instance, + create_fragment, + safe_not_equal, + { + skinToneEmoji: 40, + i18n: 0, + database: 39, + customEmoji: 41, + customCategorySorting: 42, + emojiVersion: 43 + }, + null, + [-1, -1, -1] + ); + } +} + +const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json'; +const DEFAULT_LOCALE = 'en'; + +var enI18n = { + categoriesLabel: 'Categories', + emojiUnsupportedMessage: 'Your browser does not support color emoji.', + favoritesLabel: 'Favorites', + loadingMessage: 'Loading…', + networkErrorMessage: 'Could not load emoji.', + regionLabel: 'Emoji picker', + searchDescription: 'When search results are available, press up or down to select and enter to choose.', + searchLabel: 'Search', + searchResultsLabel: 'Search results', + skinToneDescription: 'When expanded, press up or down to select and enter to choose.', + skinToneLabel: 'Choose a skin tone (currently {skinTone})', + skinTonesLabel: 'Skin tones', + skinTones: [ + 'Default', + 'Light', + 'Medium-Light', + 'Medium', + 'Medium-Dark', + 'Dark' + ], + categories: { + custom: 'Custom', + 'smileys-emotion': 'Smileys and emoticons', + 'people-body': 'People and body', + 'animals-nature': 'Animals and nature', + 'food-drink': 'Food and drink', + 'travel-places': 'Travel and places', + activities: 'Activities', + objects: 'Objects', + symbols: 'Symbols', + flags: 'Flags' + } +}; + +const PROPS = [ + 'customEmoji', + 'customCategorySorting', + 'database', + 'dataSource', + 'i18n', + 'locale', + 'skinToneEmoji', + 'emojiVersion' +]; + +// Styles injected ourselves, so we can declare the FONT_FAMILY variable in one place +const EXTRA_STYLES = `:host{--emoji-font-family:${FONT_FAMILY}}`; + +class PickerElement extends HTMLElement { + constructor (props) { + super(); + this.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = ":host{--emoji-size:1.375rem;--emoji-padding:0.5rem;--category-emoji-size:var(--emoji-size);--category-emoji-padding:var(--emoji-padding);--indicator-height:3px;--input-border-radius:0.5rem;--input-border-size:1px;--input-font-size:1rem;--input-line-height:1.5;--input-padding:0.25rem;--num-columns:8;--outline-size:2px;--border-size:1px;--skintone-border-radius:1rem;--category-font-size:1rem;display:flex;width:min-content;height:400px}:host,:host(.light){color-scheme:light;--background:#fff;--border-color:#e0e0e0;--indicator-color:#385ac1;--input-border-color:#999;--input-font-color:#111;--input-placeholder-color:#999;--outline-color:#999;--category-font-color:#111;--button-active-background:#e6e6e6;--button-hover-background:#d9d9d9}:host(.dark){color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}@media (prefers-color-scheme:dark){:host{color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}}:host([hidden]){display:none}button{margin:0;padding:0;border:0;background:0 0;box-shadow:none;-webkit-tap-highlight-color:transparent}button::-moz-focus-inner{border:0}input{padding:0;margin:0;line-height:1.15;font-family:inherit}input[type=search]{-webkit-appearance:none}:focus{outline:var(--outline-color) solid var(--outline-size);outline-offset:calc(-1*var(--outline-size))}:host([data-js-focus-visible]) :focus:not([data-focus-visible-added]){outline:0}:focus:not(:focus-visible){outline:0}.hide-focus{outline:0}*{box-sizing:border-box}.picker{contain:content;display:flex;flex-direction:column;background:var(--background);border:var(--border-size) solid var(--border-color);width:100%;height:100%;overflow:hidden;--total-emoji-size:calc(var(--emoji-size) + (2 * var(--emoji-padding)));--total-category-emoji-size:calc(var(--category-emoji-size) + (2 * var(--category-emoji-padding)))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.hidden{opacity:0;pointer-events:none}.abs-pos{position:absolute;left:0;top:0}.gone{display:none!important}.skintone-button-wrapper,.skintone-list{background:var(--background);z-index:3}.skintone-button-wrapper.expanded{z-index:1}.skintone-list{position:absolute;inset-inline-end:0;top:0;z-index:2;overflow:visible;border-bottom:var(--border-size) solid var(--border-color);border-radius:0 0 var(--skintone-border-radius) var(--skintone-border-radius);will-change:transform;transition:transform .2s ease-in-out;transform-origin:center 0}@media (prefers-reduced-motion:reduce){.skintone-list{transition-duration:.001s}}@supports not (inset-inline-end:0){.skintone-list{right:0}}.skintone-list.no-animate{transition:none}.tabpanel{overflow-y:auto;-webkit-overflow-scrolling:touch;will-change:transform;min-height:0;flex:1;contain:content}.emoji-menu{display:grid;grid-template-columns:repeat(var(--num-columns),var(--total-emoji-size));justify-content:space-around;align-items:flex-start;width:100%}.category{padding:var(--emoji-padding);font-size:var(--category-font-size);color:var(--category-font-color)}.custom-emoji,.emoji,button.emoji{height:var(--total-emoji-size);width:var(--total-emoji-size)}.emoji,button.emoji{font-size:var(--emoji-size);display:flex;align-items:center;justify-content:center;border-radius:100%;line-height:1;overflow:hidden;font-family:var(--emoji-font-family);cursor:pointer}@media (hover:hover) and (pointer:fine){.emoji:hover,button.emoji:hover{background:var(--button-hover-background)}}.emoji.active,.emoji:active,button.emoji.active,button.emoji:active{background:var(--button-active-background)}.custom-emoji{padding:var(--emoji-padding);object-fit:contain;pointer-events:none;background-repeat:no-repeat;background-position:center center;background-size:var(--emoji-size) var(--emoji-size)}.nav,.nav-button{align-items:center}.nav{display:grid;justify-content:space-between;contain:content}.nav-button{display:flex;justify-content:center}.nav-emoji{font-size:var(--category-emoji-size);width:var(--total-category-emoji-size);height:var(--total-category-emoji-size)}.indicator-wrapper{display:flex;border-bottom:1px solid var(--border-color)}.indicator{width:calc(100%/var(--num-groups));height:var(--indicator-height);opacity:var(--indicator-opacity);background-color:var(--indicator-color);will-change:transform,opacity;transition:opacity .1s linear,transform .25s ease-in-out}@media (prefers-reduced-motion:reduce){.indicator{will-change:opacity;transition:opacity .1s linear}}.pad-top,input.search{background:var(--background);width:100%}.pad-top{height:var(--emoji-padding);z-index:3}.search-row{display:flex;align-items:center;position:relative;padding-inline-start:var(--emoji-padding);padding-bottom:var(--emoji-padding)}.search-wrapper{flex:1;min-width:0}input.search{padding:var(--input-padding);border-radius:var(--input-border-radius);border:var(--input-border-size) solid var(--input-border-color);color:var(--input-font-color);font-size:var(--input-font-size);line-height:var(--input-line-height)}input.search::placeholder{color:var(--input-placeholder-color)}.favorites{display:flex;flex-direction:row;border-top:var(--border-size) solid var(--border-color);contain:content}.message{padding:var(--emoji-padding)}" + EXTRA_STYLES; + this.shadowRoot.appendChild(style); + this._ctx = { + // Set defaults + locale: DEFAULT_LOCALE, + dataSource: DEFAULT_DATA_SOURCE, + skinToneEmoji: DEFAULT_SKIN_TONE_EMOJI, + customCategorySorting: DEFAULT_CATEGORY_SORTING, + customEmoji: null, + i18n: enI18n, + emojiVersion: null, + ...props + }; + // Handle properties set before the element was upgraded + for (const prop of PROPS) { + if (prop !== 'database' && Object.prototype.hasOwnProperty.call(this, prop)) { + this._ctx[prop] = this[prop]; + delete this[prop]; + } + } + this._dbFlush(); // wait for a flush before creating the db, in case the user calls e.g. a setter or setAttribute + } + + connectedCallback () { + // The _cmp may be defined if the component was immediately disconnected and then reconnected. In that case, + // do nothing (preserve the state) + if (!this._cmp) { + this._cmp = new Picker({ + target: this.shadowRoot, + props: this._ctx + }); + } + } + + disconnectedCallback () { + // Check in a microtask if the element is still connected. If so, treat this as a "move" rather than a disconnect + // Inspired by Vue: https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue + Promise.resolve().then(() => { + // this._cmp may be defined if connect-disconnect-connect-disconnect occurs synchronously + if (!this.isConnected && this._cmp) { + this._cmp.$destroy(); + this._cmp = undefined; + + const { database } = this._ctx; + database.close() + // only happens if the database failed to load in the first place, so we don't care + .catch(err => console.error(err)); + } + }); + } + + static get observedAttributes () { + return ['locale', 'data-source', 'skin-tone-emoji', 'emoji-version'] // complex objects aren't supported, also use kebab-case + } + + attributeChangedCallback (attrName, oldValue, newValue) { + this._set( + // convert from kebab-case to camelcase + // see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015 + attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()), + // convert string attribute to float if necessary + attrName === 'emoji-version' ? parseFloat(newValue) : newValue + ); + } + + _set (prop, newValue) { + this._ctx[prop] = newValue; + if (this._cmp) { + this._cmp.$set({ [prop]: newValue }); + } + if (['locale', 'dataSource'].includes(prop)) { + this._dbFlush(); + } + } + + _dbCreate () { + const { locale, dataSource, database } = this._ctx; + // only create a new database if we really need to + if (!database || database.locale !== locale || database.dataSource !== dataSource) { + this._set('database', new Database({ locale, dataSource })); + } + } + + // Update the Database in one microtask if the locale/dataSource change. We do one microtask + // so we don't create two Databases if e.g. both the locale and the dataSource change + _dbFlush () { + Promise.resolve().then(() => ( + this._dbCreate() + )); + } +} + +const definitions = {}; + +for (const prop of PROPS) { + definitions[prop] = { + get () { + if (prop === 'database') { + // in rare cases, the microtask may not be flushed yet, so we need to instantiate the DB + // now if the user is asking for it + this._dbCreate(); + } + return this._ctx[prop] + }, + set (val) { + if (prop === 'database') { + throw new Error('database is read-only') + } + this._set(prop, val); + } + }; +} + +Object.defineProperties(PickerElement.prototype, definitions); + +/* istanbul ignore else */ +if (!customElements.get('emoji-picker')) { // if already defined, do nothing (e.g. same script imported twice) + customElements.define('emoji-picker', PickerElement); +} + +export { PickerElement as default }; diff --git a/app/templates/base/scripts.html b/app/templates/base/scripts.html index 0f08ce6..09062fb 100644 --- a/app/templates/base/scripts.html +++ b/app/templates/base/scripts.html @@ -1,4 +1,4 @@ {% for s in scripts %} {% endfor %} - + diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index bb75b2e..2d30dc7 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -98,7 +98,7 @@ From 85830f18935fbcb13bb195d8b80a3698ec0cc59c Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 20 May 2023 21:16:37 +0200 Subject: [PATCH 79/92] home: incident information --- app/templates/base/header.html | 2 +- app/templates/index.html | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/templates/base/header.html b/app/templates/base/header.html index 932bae6..8ef274d 100644 --- a/app/templates/base/header.html +++ b/app/templates/base/header.html @@ -9,7 +9,7 @@ {% if current_user.is_authenticated and current_user.priv('misc.dev-infos') %} diff --git a/app/templates/index.html b/app/templates/index.html index 2807bb7..62381bb 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -7,7 +7,15 @@ {% block content %}
    -

    du contenu....

    +

    Site temporaire de Planète Casio

    + +

    Le site habituel de Planète Casio est indisponible en raison de problèmes techniques avec l'hébergement. Vous êtes sur la prochaine version du site (v5) qui est en développement sur un serveur séparé.

    + +
      +
    • Inscription : dans le menu "Compte" à gauche (les comptes seront ultimement reconnectés à la version originale du site)
    • +
    • Le forum est fonctionnel, les programmes arrivent sous peu.
    • +
    • Pour toute demande particulière, vous pouvez envoyer un email à contact (at) planet-casio (dot) com.
    • +
    {% endblock %} From e7d28570c75d087578a3e13207ae5c3cb2b6fba3 Mon Sep 17 00:00:00 2001 From: Darks Date: Fri, 2 Jun 2023 15:37:26 +0200 Subject: [PATCH 80/92] programs: fix index table template A column was missing in the header --- app/templates/programs/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html index 7f888f6..79bb493 100644 --- a/app/templates/programs/index.html +++ b/app/templates/programs/index.html @@ -17,7 +17,7 @@

    Tous les programmes

    IDNomAuteurPublié leProgrankTags
    IDNomAuteurLabelProgrankTags
    {{ p.id }} {{ p.name }}{{ p.author.name }} + {%- if p.real_author -%} + {{ p.real_author }} (posté par {{ p.author.name }}) + {%- else -%} + {{ p.author.name }} + {%- endif -%} + {{ p.date_created | dyndate }}{{ "Oui" if p.label else "Non" }} {{ p.progrank }} {%- for tag in p.tags %} diff --git a/app/templates/programs/program.html b/app/templates/programs/program.html index 2c83c47..207ad02 100644 --- a/app/templates/programs/program.html +++ b/app/templates/programs/program.html @@ -12,78 +12,18 @@ {% block content %}
    -
    -
    - {{ widget_user.profile(p.author) }} -
    -
    -
    - {{ p.title }} -
    -
    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae - feugiat ante. Pellentesque luctus lorem tincidunt vestibulum condimentum. - Nullam sed tempus sem. Phasellus quis diam vitae sapien luctus consequat - ac eget lacus. Sed rutrum condimentum sagittis. Nullam erat nibh, euismod - ac metus at, consequat tincidunt ipsum. Fusce sagittis iaculis orci sedporta. - Etiam bibendum purus et ipsum pellentesque, quis sodales libero congue. Nunc - lectus quam, cursus non dictum nec, volutpat eget felis. Praesent sollicitudin - massa erat, nec venenatis lorem lacinia et. Etiam ullamcorper neque quis - nisi sodales vulputate. Integer scelerisque luctus arcu, ut elementum justo - auctor a. Praesent sit amet libero risus.

    - - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae - feugiat ante. Pellentesque luctus lorem tincidunt vestibulum condimentum. - Nullam sed tempus sem. Phasellus quis diam vitae sapien luctus consequat - ac eget lacus. Sed rutrum condimentum sagittis. Nullam erat nibh, euismod - ac metus at, consequat tincidunt ipsum. Fusce sagittis iaculis orci sedporta. - Etiam bibendum purus et ipsum pellentesque, quis sodales libero congue. Nunc - lectus quam, cursus non dictum nec, volutpat eget felis. Praesent sollicitudin - massa erat, nec venenatis lorem lacinia et. Etiam ullamcorper neque quis - nisi sodales vulputate. Integer scelerisque luctus arcu, ut elementum justo - auctor a. Praesent sit amet libero risus.

    - - - -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae - feugiat ante. Pellentesque luctus lorem tincidunt vestibulum condimentum. - Nullam sed tempus sem. Phasellus quis diam vitae sapien luctus consequat - ac eget lacus. Sed rutrum condimentum sagittis. Nullam erat nibh, euismod - ac metus at, consequat tincidunt ipsum. Fusce sagittis iaculis orci sedporta. - Etiam bibendum purus et ipsum pellentesque, quis sodales libero congue. Nunc - lectus quam, cursus non dictum nec, volutpat eget felis. Praesent sollicitudin - massa erat, nec venenatis lorem lacinia et. Etiam ullamcorper neque quis - nisi sodales vulputate. Integer scelerisque luctus arcu, ut elementum justo - auctor a. Praesent sit amet libero risus.

    -
    - {% if p.thread.top_comment %} {% call widget_thread.thread_leader(p.thread.top_comment) %}
    Posté le {{ p.date_created | dyndate }}
    {{ widget_thread.post_actions(p) }}
    +

    {{ p.name }}

    + + Infos infos infos + +
    + {{ p.thread.top_comment.text | md }} {{ widget_attachments.attachments(p.thread.top_comment) }} {% endcall %} diff --git a/migrations/versions/ba47de949e59_add_program_metadata_and_a_base_for_.py b/migrations/versions/ba47de949e59_add_program_metadata_and_a_base_for_.py new file mode 100644 index 0000000..8a2cc50 --- /dev/null +++ b/migrations/versions/ba47de949e59_add_program_metadata_and_a_base_for_.py @@ -0,0 +1,48 @@ +"""add program metadata and a base for events + +Revision ID: ba47de949e59 +Revises: daa5d5913ef8 +Create Date: 2022-06-16 12:05:40.797694 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ba47de949e59' +down_revision = 'daa5d5913ef8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.Unicode(length=128), nullable=True), + sa.Column('main_topic', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['main_topic'], ['topic.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('program', sa.Column('real_author', sa.Unicode(length=128), nullable=True)) + op.add_column('program', sa.Column('version', sa.Unicode(length=64), nullable=True)) + op.add_column('program', sa.Column('size', sa.Unicode(length=64), nullable=True)) + op.add_column('program', sa.Column('license', sa.String(length=32), nullable=True)) + op.add_column('program', sa.Column('label', sa.Boolean(), server_default='FALSE', nullable=False)) + op.add_column('program', sa.Column('event', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'program', 'event', ['event'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'program', type_='foreignkey') + op.drop_column('program', 'event') + op.drop_column('program', 'label') + op.drop_column('program', 'license') + op.drop_column('program', 'size') + op.drop_column('program', 'version') + op.drop_column('program', 'real_author') + op.drop_table('event') + # ### end Alembic commands ### From 760c2f20b2585a6a84b70f78d48275c1b32ddede Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 16 Jun 2022 17:37:39 +0100 Subject: [PATCH 26/92] programs: a reasonable start for the program page (#93) --- app/models/program.py | 2 +- app/static/css/form.css | 6 ------ app/static/css/global.css | 6 ++++++ app/static/css/programs.css | 22 ++++++++++++++++++++ app/static/less/form.less | 7 ------- app/static/less/global.less | 7 +++++++ app/static/less/programs.less | 26 ++++++++++++++++++++++++ app/templates/programs/program.html | 31 ++++++++++++++++++++++++++--- app/utils/render.py | 1 + 9 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 app/static/css/programs.css create mode 100644 app/static/less/programs.less diff --git a/app/models/program.py b/app/models/program.py index 02a2dfd..d7b13ea 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -24,7 +24,7 @@ class Program(Post): # Event for which the program was posted event = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=True) - # TODO: Number of views and downloads + # TODO: Number of downloads # Thread with the program description (top comment) and comments thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) diff --git a/app/static/css/form.css b/app/static/css/form.css index 9465375..b051d2a 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -84,12 +84,6 @@ .form progress.entropy.high::-webkit-progress-bar { background: var(--ok); } -.form hr { - height: 3px; - border: var(--hr-border); - border-width: 1px 0; - margin: 24px 0; -} .form .msgerror { color: var(--error); font-weight: 400; diff --git a/app/static/css/global.css b/app/static/css/global.css index e96db7f..a37704c 100644 --- a/app/static/css/global.css +++ b/app/static/css/global.css @@ -48,6 +48,12 @@ a:focus { img.pixelated { image-rendering: pixelated; } +hr { + height: 3px; + border: var(--hr-border); + border-width: 1px 0; + margin: 24px 0; +} section p { line-height: 20px; word-wrap: anywhere; diff --git a/app/static/css/programs.css b/app/static/css/programs.css new file mode 100644 index 0000000..c4b41b3 --- /dev/null +++ b/app/static/css/programs.css @@ -0,0 +1,22 @@ +#program-banner { + background: navy; + height: 144px; + margin: 0 0 32px 0; +} +section .program-infos { + display: flex; + width: 100%; + justify-content: space-between; +} +section .program-infos span.progrank { + border-width: 0 0 1px 0; + border-color: var(--color); + border-style: dotted; +} +section .program-infos > div { + flex-shrink: 0; + margin: 0 8px; +} +section .program-infos div.program-tags { + flex-shrink: 1; +} \ No newline at end of file diff --git a/app/static/less/form.less b/app/static/less/form.less index 046b7dc..fa76a8f 100644 --- a/app/static/less/form.less +++ b/app/static/less/form.less @@ -95,13 +95,6 @@ } } - hr { - height: 3px; - border: var(--hr-border); - border-width: 1px 0; - margin: 24px 0; - } - .msgerror { color: var(--error); font-weight: 400; diff --git a/app/static/less/global.less b/app/static/less/global.less index cfbf990..6b42410 100644 --- a/app/static/less/global.less +++ b/app/static/less/global.less @@ -44,6 +44,13 @@ img.pixelated { image-rendering: pixelated; } +hr { + height: 3px; + border: var(--hr-border); + border-width: 1px 0; + margin: 24px 0; +} + section { p { line-height: 20px; diff --git a/app/static/less/programs.less b/app/static/less/programs.less new file mode 100644 index 0000000..416f937 --- /dev/null +++ b/app/static/less/programs.less @@ -0,0 +1,26 @@ +#program-banner { + background: navy; /* debugging */ + height: 144px; + margin: 0 0 32px 0; +} + +section .program-infos { + display: flex; + width: 100%; + justify-content: space-between; + + span.progrank { + border-width: 0 0 1px 0; + border-color: var(--color); /* use text color */ + border-style: dotted; + } + + & > div { + flex-shrink: 0; + margin: 0 8px; + } + + div.program-tags { + flex-shrink: 1; + } +} diff --git a/app/templates/programs/program.html b/app/templates/programs/program.html index 207ad02..1e5b5d0 100644 --- a/app/templates/programs/program.html +++ b/app/templates/programs/program.html @@ -11,19 +11,44 @@ {% endblock %} {% block content %} +
    +
    +
    {% if p.thread.top_comment %} {% call widget_thread.thread_leader(p.thread.top_comment) %}
    -
    Posté le {{ p.date_created | dyndate }}
    {{ widget_thread.post_actions(p) }}

    {{ p.name }}

    - Infos infos infos - +
    +
    + Posté : {{ p.date_created | dyndate }}
    + Modifié : {{ p.date_modified | dyndate }}
    + Version : {{ p.version or "(non spécifié)" }}
    + Taille : {{ p.size or "(non spécifié)" }} +
    +
    + Score au progrank: {{ p.progrank }}
    + Tests : TODO (si si ça vient)
    + [Lien pour soumettre un test]
    + [Lien pour proposer au label]
    + Total de téléchargements : TODO +
    +
    + {%- for tag in p.tags %} + {{ tag.tag.pretty }} + {% endfor -%} +
    +

    + {% if p.event %} + Ce programme a participé à l'événement {{ p.event.name }}! +
    + {% endif %} + {{ p.thread.top_comment.text | md }} {{ widget_attachments.attachments(p.thread.top_comment) }} {% endcall %} diff --git a/app/utils/render.py b/app/utils/render.py index a7cd320..ac2da5f 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -19,6 +19,7 @@ def render(*args, styles=[], scripts=[], **kwargs): 'css/simplemde.min.css', 'css/simplemde-override.css', 'css/debugger.css', + 'css/programs.css', ] scripts_ = [ 'scripts/trigger_menu.js', From 2b9ab64f6ef88300032b671b246297688ff4a359 Mon Sep 17 00:00:00 2001 From: Lephe Date: Tue, 15 Nov 2022 11:03:36 +0100 Subject: [PATCH 27/92] routes: fix constant 404s due to new werkzeug handling of / --- app/utils/converters.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/utils/converters.py b/app/utils/converters.py index a6fe429..c9775b1 100644 --- a/app/utils/converters.py +++ b/app/utils/converters.py @@ -25,9 +25,10 @@ from slugify import slugify class ForumConverter(BaseConverter): - # This regex will decide which portion of the URL is matched by the curtom - # converter. By default, slashes are not included, so we must add them. + # This regex will decide which portion of the URL is converted. regex = r'[a-z/]+' + # Allow slashes to be matched by the regex (this is not by default) + part_isolating = False def to_python(self, url): url = '/' + url @@ -44,6 +45,7 @@ class PageConverter(BaseConverter): # Matches integers for the topic number, then either nothing, a page # number, a slug, or a page number followed by a slug regex = r'(\d+)(?:/(\d+)|/fin)?(?:/[\w-]+)?' + part_isolating = False object = None get_title = lambda o: "empty-title" From d0126e7aba3818eac7660ed8fa1b8404c51b8545 Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 15 Nov 2022 16:10:53 +0100 Subject: [PATCH 28/92] meta: Update REQUIREMENTS.md according to package updates in AUR --- REQUIREMENTS.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 6d4cec8..959b871 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -23,14 +23,10 @@ python-psycopg2 python-pillow python-pyyaml python-slugify -``` - -Non disponibles sur l'AUR, mais disponibles sur pip : -``` -flask-crontab (0.1.2) +flask-crontab ``` Optionnel: ``` -python-flask-debugtoolbar (out-of-date sur l'AUR : bien installer la 0.13) +python-flask-debugtoolbar (Disponible dans l'AUR) ``` From 277ec535e7f38ab3d50e9b88a4ff961cccac0b01 Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 15 Nov 2022 16:19:32 +0100 Subject: [PATCH 29/92] templates: Add slash at end of hardcoded urls --- app/templates/base/navbar/news.html | 10 +++++----- app/templates/base/navbar/tools.html | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/templates/base/navbar/news.html b/app/templates/base/navbar/news.html index 844b7ed..d69883f 100644 --- a/app/templates/base/navbar/news.html +++ b/app/templates/base/navbar/news.html @@ -5,14 +5,14 @@ Actualités - Toutes les nouveautés + Toutes les nouveautés
    - Nouveautés Casio - Projets communutaires - Événements de Planète Casio - Autres nouveautés + Nouveautés Casio + Projets communutaires + Événements de Planète Casio + Autres nouveautés
    diff --git a/app/templates/base/navbar/tools.html b/app/templates/base/navbar/tools.html index 860d180..a55c012 100644 --- a/app/templates/base/navbar/tools.html +++ b/app/templates/base/navbar/tools.html @@ -5,12 +5,12 @@ Outils - + Forge Gitea - + Casio Universal Wiki From 5fb06732ff6824a65a5ff052fc23ac63a596156d Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 14 Apr 2022 23:03:06 +0200 Subject: [PATCH 30/92] Remove old & add buttons for new editor --- app/static/css/editor.css | 7 + app/static/css/form.css | 2 +- app/static/css/global.css | 3 + app/static/css/simplemde-override.css | 71 ---------- app/static/css/simplemde.min.css | 7 - app/static/css/themes/default_theme.css | 8 +- app/static/less/editor.less | 11 ++ app/static/less/global.less | 2 +- app/static/scripts/simplemde.min.js | 15 -- app/templates/widgets/editor.html | 178 ++++++++++++++++-------- app/utils/render.py | 5 +- 11 files changed, 146 insertions(+), 163 deletions(-) create mode 100644 app/static/css/editor.css delete mode 100644 app/static/css/simplemde-override.css delete mode 100644 app/static/css/simplemde.min.css create mode 100644 app/static/less/editor.less delete mode 100644 app/static/scripts/simplemde.min.js diff --git a/app/static/css/editor.css b/app/static/css/editor.css new file mode 100644 index 0000000..8251dc5 --- /dev/null +++ b/app/static/css/editor.css @@ -0,0 +1,7 @@ +.editor > button > svg { + width: 25px; +} +.editor > button > svg > path, +.editor > button > svg > rect { + fill: var(--icons); +} \ No newline at end of file diff --git a/app/static/css/form.css b/app/static/css/form.css index b051d2a..1597f03 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -134,4 +134,4 @@ form .dynamic-tag-selector .tags-selected .tag { background: rgba(0,0,0,.05); padding: 1px 2px; border-radius: 2px; -} \ No newline at end of file +} diff --git a/app/static/css/global.css b/app/static/css/global.css index a37704c..a956df7 100644 --- a/app/static/css/global.css +++ b/app/static/css/global.css @@ -78,6 +78,7 @@ section h2 { color: var(--text-light); padding-bottom: 2px; } +button, .button, input[type="button"], input[type="submit"] { @@ -88,9 +89,11 @@ input[type="submit"] { font-weight: 400; border: 0; } +button:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, +button:focus, .button:focus, input[type="button"]:focus, input[type="submit"]:focus { diff --git a/app/static/css/simplemde-override.css b/app/static/css/simplemde-override.css deleted file mode 100644 index ae75f2b..0000000 --- a/app/static/css/simplemde-override.css +++ /dev/null @@ -1,71 +0,0 @@ -/* SimpleMDE overwrite that allows us to customize from themes */ - -div.editor-toolbar { - border-color: var(--border); -} - -div.editor-toolbar > a { - color: var(--text) !important; -} -div.editor-toolbar > a.active, -div.editor-toolbar > a:hover { - background: var(--background-light); - border-color: var(--background-light); -} - -div.editor-toolbar > i.separator { - border-right-color: transparent; - border-left-color: var(--separator); -} - -div.editor-toolbar.disabled-for-preview a:not(.no-disable) { - background: none; - color: var(--text-disabled) !important; -} -div.editor-toolbar.disabled-for-preview > i.separator { - border-left-color: var(--text-disabled); -} - -div.CodeMirror, -div.editor-preview { - background: var(--background); - color: var(--text); - border-color: var(--border); -} -div.editor-preview { - background: var(--background-preview); -} - -div.editor-preview table th, -div.editor-preview-side table th, -div.editor-preview table td, -div.editor-preview-side table td { - border: inherit; - padding: inherit; -} - -div.editor-preview table.codehilitetable pre, -div.editor-preview-side table.codehilitetable pre { - background: transparent; -} - -div.CodeMirror .CodeMirror-selected, -div.CodeMirror .CodeMirror-selectedtext { - background: var(--background-light); -} -div.CodeMirror .CodeMirror-focused .CodeMirror-selected, -div.CodeMirror .CodeMirror-focused .CodeMirror-selectedtext, -div.CodeMirror .CodeMirror-line::selection, -div.CodeMirror .CodeMirror-line > span::selection, -div.CodeMirror .CodeMirror-line > span > span::selection { - background: var(--background-light); -} -div.CodeMirror .CodeMirror-line::-moz-selection, -div.CodeMirror .CodeMirror-line > span::-moz-selection, -div.CodeMirror .CodeMirror-line > span > span::-moz-selection { - background: var(--background-light); -} - -div.CodeMirror-cursor { - border-color: var(--text); -} diff --git a/app/static/css/simplemde.min.css b/app/static/css/simplemde.min.css deleted file mode 100644 index d62f4d7..0000000 --- a/app/static/css/simplemde.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/** - * simplemde v1.11.2 - * Copyright Next Step Webs, Inc. - * @link https://github.com/NextStepWebs/simplemde-markdown-editor - * @license MIT - */ -.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:300px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:300px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} \ No newline at end of file diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index 1168211..68ec1d6 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -117,12 +117,8 @@ footer { --background-light: #f0f0f0; } -div.editor-toolbar, div.CodeMirror { - --border: #c0c0c0; - --background-light: #d9d9d9; - --background-preview: #f4f4f6; - --separator: #a0a0a0; - --text-disabled: #c0c0c0; +.editor { + --icons: #ffffff; } .dl-button { diff --git a/app/static/less/editor.less b/app/static/less/editor.less new file mode 100644 index 0000000..f51946e --- /dev/null +++ b/app/static/less/editor.less @@ -0,0 +1,11 @@ +@import "vars"; + +.editor { + & > button > svg { + width: 25px; + + & > path, & > rect { + fill: var(--icons); + } + } +} diff --git a/app/static/less/global.less b/app/static/less/global.less index 6b42410..3036468 100644 --- a/app/static/less/global.less +++ b/app/static/less/global.less @@ -80,7 +80,7 @@ section { } /* Buttons */ -.button, input[type="button"], input[type="submit"] { +button, .button, input[type="button"], input[type="submit"] { padding: 6px 10px; border-radius: 2px; cursor: pointer; font-family: 'DejaVu Sans', sans-serif; font-weight: 400; diff --git a/app/static/scripts/simplemde.min.js b/app/static/scripts/simplemde.min.js deleted file mode 100644 index 50c624f..0000000 --- a/app/static/scripts/simplemde.min.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * simplemde v1.11.2 - * Copyright Next Step Webs, Inc. - * @link https://github.com/NextStepWebs/simplemde-markdown-editor - * @license MIT - */ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.SimpleMDE=e()}}(function(){var e;return function t(e,n,r){function i(a,l){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var c=new Error("Cannot find module '"+a+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;at;++t)s[t]=e[t],c[e.charCodeAt(t)]=t;c["-".charCodeAt(0)]=62,c["_".charCodeAt(0)]=63}function i(e){var t,n,r,i,o,a,l=e.length;if(l%4>0)throw new Error("Invalid string. Length must be a multiple of 4");o="="===e[l-2]?2:"="===e[l-1]?1:0,a=new u(3*l/4-o),r=o>0?l-4:l;var s=0;for(t=0,n=0;r>t;t+=4,n+=3)i=c[e.charCodeAt(t)]<<18|c[e.charCodeAt(t+1)]<<12|c[e.charCodeAt(t+2)]<<6|c[e.charCodeAt(t+3)],a[s++]=i>>16&255,a[s++]=i>>8&255,a[s++]=255&i;return 2===o?(i=c[e.charCodeAt(t)]<<2|c[e.charCodeAt(t+1)]>>4,a[s++]=255&i):1===o&&(i=c[e.charCodeAt(t)]<<10|c[e.charCodeAt(t+1)]<<4|c[e.charCodeAt(t+2)]>>2,a[s++]=i>>8&255,a[s++]=255&i),a}function o(e){return s[e>>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}function a(e,t,n){for(var r,i=[],a=t;n>a;a+=3)r=(e[a]<<16)+(e[a+1]<<8)+e[a+2],i.push(o(r));return i.join("")}function l(e){for(var t,n=e.length,r=n%3,i="",o=[],l=16383,c=0,u=n-r;u>c;c+=l)o.push(a(e,c,c+l>u?u:c+l));return 1===r?(t=e[n-1],i+=s[t>>2],i+=s[t<<4&63],i+="=="):2===r&&(t=(e[n-2]<<8)+e[n-1],i+=s[t>>10],i+=s[t>>4&63],i+=s[t<<2&63],i+="="),o.push(i),o.join("")}n.toByteArray=i,n.fromByteArray=l;var s=[],c=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array;r()},{}],2:[function(e,t,n){},{}],3:[function(e,t,n){(function(t){"use strict";function r(){try{var e=new Uint8Array(1);return e.foo=function(){return 42},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(t){return!1}}function i(){return a.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function o(e,t){if(i()=t?o(e,t):void 0!==n?"string"==typeof r?o(e,t).fill(n,r):o(e,t).fill(n):o(e,t)}function u(e,t){if(s(t),e=o(e,0>t?0:0|m(t)),!a.TYPED_ARRAY_SUPPORT)for(var n=0;t>n;n++)e[n]=0;return e}function f(e,t,n){if("string"==typeof n&&""!==n||(n="utf8"),!a.isEncoding(n))throw new TypeError('"encoding" must be a valid string encoding');var r=0|v(t,n);return e=o(e,r),e.write(t,n),e}function h(e,t){var n=0|m(t.length);e=o(e,n);for(var r=0;n>r;r+=1)e[r]=255&t[r];return e}function d(e,t,n,r){if(t.byteLength,0>n||t.byteLength=i())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i().toString(16)+" bytes");return 0|e}function g(e){return+e!=e&&(e=0),a.alloc(+e)}function v(e,t){if(a.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"binary":case"raw":case"raws":return n;case"utf8":case"utf-8":case void 0:return q(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return $(e).length;default:if(r)return q(e).length;t=(""+t).toLowerCase(),r=!0}}function y(e,t,n){var r=!1;if((void 0===t||0>t)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),0>=n)return"";if(n>>>=0,t>>>=0,t>=n)return"";for(e||(e="utf8");;)switch(e){case"hex":return I(this,t,n);case"utf8":case"utf-8":return N(this,t,n);case"ascii":return E(this,t,n);case"binary":return O(this,t,n);case"base64":return M(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function x(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function b(e,t,n,r){function i(e,t){return 1===o?e[t]:e.readUInt16BE(t*o)}var o=1,a=e.length,l=t.length;if(void 0!==r&&(r=String(r).toLowerCase(),"ucs2"===r||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;o=2,a/=2,l/=2,n/=2}for(var s=-1,c=0;a>n+c;c++)if(i(e,n+c)===i(t,-1===s?0:c-s)){if(-1===s&&(s=c),c-s+1===l)return(n+s)*o}else-1!==s&&(c-=c-s),s=-1;return-1}function w(e,t,n,r){n=Number(n)||0;var i=e.length-n;r?(r=Number(r),r>i&&(r=i)):r=i;var o=t.length;if(o%2!==0)throw new Error("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;r>a;a++){var l=parseInt(t.substr(2*a,2),16);if(isNaN(l))return a;e[n+a]=l}return a}function k(e,t,n,r){return V(q(t,e.length-n),e,n,r)}function S(e,t,n,r){return V(G(t),e,n,r)}function C(e,t,n,r){return S(e,t,n,r)}function L(e,t,n,r){return V($(t),e,n,r)}function T(e,t,n,r){return V(Y(t,e.length-n),e,n,r)}function M(e,t,n){return 0===t&&n===e.length?X.fromByteArray(e):X.fromByteArray(e.slice(t,n))}function N(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;n>i;){var o=e[i],a=null,l=o>239?4:o>223?3:o>191?2:1;if(n>=i+l){var s,c,u,f;switch(l){case 1:128>o&&(a=o);break;case 2:s=e[i+1],128===(192&s)&&(f=(31&o)<<6|63&s,f>127&&(a=f));break;case 3:s=e[i+1],c=e[i+2],128===(192&s)&&128===(192&c)&&(f=(15&o)<<12|(63&s)<<6|63&c,f>2047&&(55296>f||f>57343)&&(a=f));break;case 4:s=e[i+1],c=e[i+2],u=e[i+3],128===(192&s)&&128===(192&c)&&128===(192&u)&&(f=(15&o)<<18|(63&s)<<12|(63&c)<<6|63&u,f>65535&&1114112>f&&(a=f))}}null===a?(a=65533,l=1):a>65535&&(a-=65536,r.push(a>>>10&1023|55296),a=56320|1023&a),r.push(a),i+=l}return A(r)}function A(e){var t=e.length;if(Q>=t)return String.fromCharCode.apply(String,e);for(var n="",r=0;t>r;)n+=String.fromCharCode.apply(String,e.slice(r,r+=Q));return n}function E(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(127&e[i]);return r}function O(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(e[i]);return r}function I(e,t,n){var r=e.length;(!t||0>t)&&(t=0),(!n||0>n||n>r)&&(n=r);for(var i="",o=t;n>o;o++)i+=U(e[o]);return i}function P(e,t,n){for(var r=e.slice(t,n),i="",o=0;oe)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function D(e,t,n,r,i,o){if(!a.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||o>t)throw new RangeError('"value" argument is out of bounds');if(n+r>e.length)throw new RangeError("Index out of range")}function H(e,t,n,r){0>t&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);o>i;i++)e[n+i]=(t&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function W(e,t,n,r){0>t&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);o>i;i++)e[n+i]=t>>>8*(r?i:3-i)&255}function B(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(0>n)throw new RangeError("Index out of range")}function _(e,t,n,r,i){return i||B(e,t,n,4,3.4028234663852886e38,-3.4028234663852886e38),Z.write(e,t,n,r,23,4),n+4}function F(e,t,n,r,i){return i||B(e,t,n,8,1.7976931348623157e308,-1.7976931348623157e308),Z.write(e,t,n,r,52,8),n+8}function z(e){if(e=j(e).replace(ee,""),e.length<2)return"";for(;e.length%4!==0;)e+="=";return e}function j(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}function U(e){return 16>e?"0"+e.toString(16):e.toString(16)}function q(e,t){t=t||1/0;for(var n,r=e.length,i=null,o=[],a=0;r>a;a++){if(n=e.charCodeAt(a),n>55295&&57344>n){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(56320>n){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=(i-55296<<10|n-56320)+65536}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,128>n){if((t-=1)<0)break;o.push(n)}else if(2048>n){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(65536>n){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(1114112>n))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function G(e){for(var t=[],n=0;n>8,i=n%256,o.push(i),o.push(r);return o}function $(e){return X.toByteArray(z(e))}function V(e,t,n,r){for(var i=0;r>i&&!(i+n>=t.length||i>=e.length);i++)t[i+n]=e[i];return i}function K(e){return e!==e}var X=e("base64-js"),Z=e("ieee754"),J=e("isarray");n.Buffer=a,n.SlowBuffer=g,n.INSPECT_MAX_BYTES=50,a.TYPED_ARRAY_SUPPORT=void 0!==t.TYPED_ARRAY_SUPPORT?t.TYPED_ARRAY_SUPPORT:r(),n.kMaxLength=i(),a.poolSize=8192,a._augment=function(e){return e.__proto__=a.prototype,e},a.from=function(e,t,n){return l(null,e,t,n)},a.TYPED_ARRAY_SUPPORT&&(a.prototype.__proto__=Uint8Array.prototype,a.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&a[Symbol.species]===a&&Object.defineProperty(a,Symbol.species,{value:null,configurable:!0})),a.alloc=function(e,t,n){return c(null,e,t,n)},a.allocUnsafe=function(e){return u(null,e)},a.allocUnsafeSlow=function(e){return u(null,e)},a.isBuffer=function(e){return!(null==e||!e._isBuffer)},a.compare=function(e,t){if(!a.isBuffer(e)||!a.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);o>i;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return r>n?-1:n>r?1:0},a.isEncoding=function(e){switch(String(e).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"raw":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},a.concat=function(e,t){if(!J(e))throw new TypeError('"list" argument must be an Array of Buffers');if(0===e.length)return a.alloc(0);var n;if(void 0===t)for(t=0,n=0;nt;t+=2)x(this,t,t+1);return this},a.prototype.swap32=function(){var e=this.length;if(e%4!==0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var t=0;e>t;t+=4)x(this,t,t+3),x(this,t+1,t+2);return this},a.prototype.toString=function(){var e=0|this.length;return 0===e?"":0===arguments.length?N(this,0,e):y.apply(this,arguments)},a.prototype.equals=function(e){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e?!0:0===a.compare(this,e)},a.prototype.inspect=function(){var e="",t=n.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,t).match(/.{2}/g).join(" "),this.length>t&&(e+=" ... ")),""},a.prototype.compare=function(e,t,n,r,i){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),0>t||n>e.length||0>r||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,i>>>=0,this===e)return 0;for(var o=i-r,l=n-t,s=Math.min(o,l),c=this.slice(r,i),u=e.slice(t,n),f=0;s>f;++f)if(c[f]!==u[f]){o=c[f],l=u[f];break}return l>o?-1:o>l?1:0},a.prototype.indexOf=function(e,t,n){if("string"==typeof t?(n=t,t=0):t>2147483647?t=2147483647:-2147483648>t&&(t=-2147483648),t>>=0,0===this.length)return-1;if(t>=this.length)return-1;if(0>t&&(t=Math.max(this.length+t,0)),"string"==typeof e&&(e=a.from(e,n)),a.isBuffer(e))return 0===e.length?-1:b(this,e,t,n);if("number"==typeof e)return a.TYPED_ARRAY_SUPPORT&&"function"===Uint8Array.prototype.indexOf?Uint8Array.prototype.indexOf.call(this,e,t):b(this,[e],t,n);throw new TypeError("val must be string, number or Buffer")},a.prototype.includes=function(e,t,n){return-1!==this.indexOf(e,t,n)},a.prototype.write=function(e,t,n,r){if(void 0===t)r="utf8",n=this.length,t=0;else if(void 0===n&&"string"==typeof t)r=t,n=this.length,t=0;else{if(!isFinite(t))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");t=0|t,isFinite(n)?(n=0|n,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-t;if((void 0===n||n>i)&&(n=i),e.length>0&&(0>n||0>t)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return k(this,e,t,n);case"ascii":return S(this,e,t,n);case"binary":return C(this,e,t,n);case"base64":return L(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},a.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var Q=4096;a.prototype.slice=function(e,t){var n=this.length;e=~~e,t=void 0===t?n:~~t,0>e?(e+=n,0>e&&(e=0)):e>n&&(e=n),0>t?(t+=n,0>t&&(t=0)):t>n&&(t=n),e>t&&(t=e);var r;if(a.TYPED_ARRAY_SUPPORT)r=this.subarray(e,t),r.__proto__=a.prototype;else{var i=t-e;r=new a(i,void 0);for(var o=0;i>o;o++)r[o]=this[o+e]}return r},a.prototype.readUIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o0&&(i*=256);)r+=this[e+--t]*i;return r},a.prototype.readUInt8=function(e,t){return t||R(e,1,this.length),this[e]},a.prototype.readUInt16LE=function(e,t){return t||R(e,2,this.length),this[e]|this[e+1]<<8},a.prototype.readUInt16BE=function(e,t){return t||R(e,2,this.length),this[e]<<8|this[e+1]},a.prototype.readUInt32LE=function(e,t){return t||R(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},a.prototype.readUInt32BE=function(e,t){return t||R(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},a.prototype.readIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o=i&&(r-=Math.pow(2,8*t)),r},a.prototype.readIntBE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return i*=128,o>=i&&(o-=Math.pow(2,8*t)),o},a.prototype.readInt8=function(e,t){return t||R(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},a.prototype.readInt16LE=function(e,t){t||R(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt16BE=function(e,t){t||R(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt32LE=function(e,t){return t||R(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},a.prototype.readInt32BE=function(e,t){return t||R(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},a.prototype.readFloatLE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!0,23,4)},a.prototype.readFloatBE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!1,23,4)},a.prototype.readDoubleLE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!0,52,8)},a.prototype.readDoubleBE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!1,52,8)},a.prototype.writeUIntLE=function(e,t,n,r){if(e=+e,t=0|t,n=0|n,!r){var i=Math.pow(2,8*n)-1;D(this,e,t,n,i,0)}var o=1,a=0;for(this[t]=255&e;++a=0&&(a*=256);)this[t+o]=e/a&255;return t+n},a.prototype.writeUInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,255,0),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},a.prototype.writeUInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeUInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeUInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):W(this,e,t,!0),t+4},a.prototype.writeUInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=0,a=1,l=0;for(this[t]=255&e;++oe&&0===l&&0!==this[t+o-1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=n-1,a=1,l=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)0>e&&0===l&&0!==this[t+o+1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,127,-128),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),0>e&&(e=255+e+1),this[t]=255&e,t+1},a.prototype.writeInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):W(this,e,t,!0),t+4},a.prototype.writeInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),0>e&&(e=4294967295+e+1),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeFloatLE=function(e,t,n){return _(this,e,t,!0,n)},a.prototype.writeFloatBE=function(e,t,n){return _(this,e,t,!1,n)},a.prototype.writeDoubleLE=function(e,t,n){return F(this,e,t,!0,n)},a.prototype.writeDoubleBE=function(e,t,n){return F(this,e,t,!1,n)},a.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&n>r&&(r=n),r===n)return 0;if(0===e.length||0===this.length)return 0;if(0>t)throw new RangeError("targetStart out of bounds");if(0>n||n>=this.length)throw new RangeError("sourceStart out of bounds");if(0>r)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-tn&&r>t)for(i=o-1;i>=0;i--)e[i+t]=this[i+n];else if(1e3>o||!a.TYPED_ARRAY_SUPPORT)for(i=0;o>i;i++)e[i+t]=this[i+n];else Uint8Array.prototype.set.call(e,this.subarray(n,n+o),t);return o},a.prototype.fill=function(e,t,n,r){if("string"==typeof e){if("string"==typeof t?(r=t,t=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===e.length){var i=e.charCodeAt(0);256>i&&(e=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!a.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof e&&(e=255&e);if(0>t||this.length=n)return this;t>>>=0,n=void 0===n?this.length:n>>>0,e||(e=0);var o;if("number"==typeof e)for(o=t;n>o;o++)this[o]=e;else{var l=a.isBuffer(e)?e:q(new a(e,r).toString()),s=l.length;for(o=0;n-t>o;o++)this[o+t]=l[o%s]}return this};var ee=/[^+\/0-9A-Za-z-_]/g}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"base64-js":1,ieee754:15,isarray:16}],4:[function(e,t,n){"use strict";function r(e){return e=e||{},"function"!=typeof e.codeMirrorInstance||"function"!=typeof e.codeMirrorInstance.defineMode?void console.log("CodeMirror Spell Checker: You must provide an instance of CodeMirror via the option `codeMirrorInstance`"):(String.prototype.includes||(String.prototype.includes=function(){return-1!==String.prototype.indexOf.apply(this,arguments)}),void e.codeMirrorInstance.defineMode("spell-checker",function(t){if(!r.aff_loading){r.aff_loading=!0;var n=new XMLHttpRequest;n.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.aff",!0),n.onload=function(){4===n.readyState&&200===n.status&&(r.aff_data=n.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},n.send(null)}if(!r.dic_loading){r.dic_loading=!0;var o=new XMLHttpRequest;o.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.dic",!0),o.onload=function(){4===o.readyState&&200===o.status&&(r.dic_data=o.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},o.send(null)}var a='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~ ',l={token:function(e){var t=e.peek(),n="";if(a.includes(t))return e.next(),null;for(;null!=(t=e.peek())&&!a.includes(t);)n+=t,e.next();return r.typo&&!r.typo.check(n)?"spell-error":null}},s=e.codeMirrorInstance.getMode(t,t.backdrop||"text/plain");return e.codeMirrorInstance.overlayMode(s,l,!0)}))}var i=e("typo-js");r.num_loaded=0,r.aff_loading=!1,r.dic_loading=!1,r.aff_data="",r.dic_data="",r.typo,t.exports=r},{"typo-js":18}],5:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";function t(e){var t=e.getWrapperElement();e.state.fullScreenRestore={scrollTop:window.pageYOffset,scrollLeft:window.pageXOffset,width:t.style.width,height:t.style.height},t.style.width="",t.style.height="auto",t.className+=" CodeMirror-fullscreen",document.documentElement.style.overflow="hidden",e.refresh()}function n(e){var t=e.getWrapperElement();t.className=t.className.replace(/\s*CodeMirror-fullscreen\b/,""),document.documentElement.style.overflow="";var n=e.state.fullScreenRestore;t.style.width=n.width,t.style.height=n.height,window.scrollTo(n.scrollLeft,n.scrollTop),e.refresh()}e.defineOption("fullScreen",!1,function(r,i,o){o==e.Init&&(o=!1),!o!=!i&&(i?t(r):n(r))})})},{"../../lib/codemirror":10}],6:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){function t(e){e.state.placeholder&&(e.state.placeholder.parentNode.removeChild(e.state.placeholder),e.state.placeholder=null)}function n(e){t(e);var n=e.state.placeholder=document.createElement("pre");n.style.cssText="height: 0; overflow: visible",n.className="CodeMirror-placeholder";var r=e.getOption("placeholder");"string"==typeof r&&(r=document.createTextNode(r)),n.appendChild(r),e.display.lineSpace.insertBefore(n,e.display.lineSpace.firstChild)}function r(e){o(e)&&n(e)}function i(e){var r=e.getWrapperElement(),i=o(e);r.className=r.className.replace(" CodeMirror-empty","")+(i?" CodeMirror-empty":""),i?n(e):t(e)}function o(e){return 1===e.lineCount()&&""===e.getLine(0)}e.defineOption("placeholder","",function(n,o,a){var l=a&&a!=e.Init;if(o&&!l)n.on("blur",r),n.on("change",i),n.on("swapDoc",i),i(n);else if(!o&&l){n.off("blur",r),n.off("change",i),n.off("swapDoc",i),t(n);var s=n.getWrapperElement();s.className=s.className.replace(" CodeMirror-empty","")}o&&!n.hasFocus()&&r(n)})})},{"../../lib/codemirror":10}],7:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";var t=/^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,n=/^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,r=/[*+-]\s/;e.commands.newlineAndIndentContinueMarkdownList=function(i){if(i.getOption("disableInput"))return e.Pass;for(var o=i.listSelections(),a=[],l=0;l")>=0?d[2]:parseInt(d[3],10)+1+d[4];a[l]="\n"+p+g+m}}i.replaceSelections(a)}})},{"../../lib/codemirror":10}],8:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.overlayMode=function(t,n,r){return{startState:function(){return{base:e.startState(t),overlay:e.startState(n),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(r){return{base:e.copyState(t,r.base),overlay:e.copyState(n,r.overlay),basePos:r.basePos,baseCur:null,overlayPos:r.overlayPos,overlayCur:null}},token:function(e,i){return(e!=i.streamSeen||Math.min(i.basePos,i.overlayPos)=n.line,d=h?n:s(f,0),p=e.markText(u,d,{className:o});if(null==r?i.push(p):i.splice(r++,0,p),h)break;a=f}}function i(e){for(var t=e.state.markedSelection,n=0;n1)return o(e);var t=e.getCursor("start"),n=e.getCursor("end"),a=e.state.markedSelection;if(!a.length)return r(e,t,n);var s=a[0].find(),u=a[a.length-1].find();if(!s||!u||n.line-t.line=0||c(n,s.from)<=0)return o(e);for(;c(t,s.from)>0;)a.shift().clear(),s=a[0].find();for(c(t,s.from)<0&&(s.to.line-t.line0&&(n.line-u.from.linebo&&setTimeout(function(){s.display.input.reset(!0)},20),jt(this),Ki(),bt(this),this.curOp.forceUpdate=!0,Xr(this,i),r.autofocus&&!Ao||s.hasFocus()?setTimeout(Bi(vn,this),20):yn(this);for(var u in ta)ta.hasOwnProperty(u)&&ta[u](this,r[u],na);k(this),r.finishInit&&r.finishInit(this);for(var f=0;fbo&&(r.gutters.style.zIndex=-1,r.scroller.style.paddingRight=0),wo||go&&Ao||(r.scroller.draggable=!0),e&&(e.appendChild?e.appendChild(r.wrapper):e(r.wrapper)),r.viewFrom=r.viewTo=t.first,r.reportedViewFrom=r.reportedViewTo=t.first,r.view=[],r.renderedView=null,r.externalMeasured=null,r.viewOffset=0,r.lastWrapHeight=r.lastWrapWidth=0,r.updateLineNumbers=null,r.nativeBarWidth=r.barHeight=r.barWidth=0,r.scrollbarsClipped=!1,r.lineNumWidth=r.lineNumInnerWidth=r.lineNumChars=null,r.alignWidgets=!1,r.cachedCharWidth=r.cachedTextHeight=r.cachedPaddingH=null, -r.maxLine=null,r.maxLineLength=0,r.maxLineChanged=!1,r.wheelDX=r.wheelDY=r.wheelStartX=r.wheelStartY=null,r.shift=!1,r.selForContextMenu=null,r.activeTouch=null,n.init(r)}function n(t){t.doc.mode=e.getMode(t.options,t.doc.modeOption),r(t)}function r(e){e.doc.iter(function(e){e.stateAfter&&(e.stateAfter=null),e.styles&&(e.styles=null)}),e.doc.frontier=e.doc.first,_e(e,100),e.state.modeGen++,e.curOp&&Dt(e)}function i(e){e.options.lineWrapping?(Ja(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(Za(e.display.wrapper,"CodeMirror-wrap"),h(e)),a(e),Dt(e),lt(e),setTimeout(function(){y(e)},100)}function o(e){var t=yt(e.display),n=e.options.lineWrapping,r=n&&Math.max(5,e.display.scroller.clientWidth/xt(e.display)-3);return function(i){if(kr(e.doc,i))return 0;var o=0;if(i.widgets)for(var a=0;at.maxLineLength&&(t.maxLineLength=n,t.maxLine=e)})}function d(e){var t=Pi(e.gutters,"CodeMirror-linenumbers");-1==t&&e.lineNumbers?e.gutters=e.gutters.concat(["CodeMirror-linenumbers"]):t>-1&&!e.lineNumbers&&(e.gutters=e.gutters.slice(0),e.gutters.splice(t,1))}function p(e){var t=e.display,n=t.gutters.offsetWidth,r=Math.round(e.doc.height+qe(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?n:0,docHeight:r,scrollHeight:r+Ye(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:n}}function m(e,t,n){this.cm=n;var r=this.vert=ji("div",[ji("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=ji("div",[ji("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");e(r),e(i),Ea(r,"scroll",function(){r.clientHeight&&t(r.scrollTop,"vertical")}),Ea(i,"scroll",function(){i.clientWidth&&t(i.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,xo&&8>bo&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")}function g(){}function v(t){t.display.scrollbars&&(t.display.scrollbars.clear(),t.display.scrollbars.addClass&&Za(t.display.wrapper,t.display.scrollbars.addClass)),t.display.scrollbars=new e.scrollbarModel[t.options.scrollbarStyle](function(e){t.display.wrapper.insertBefore(e,t.display.scrollbarFiller),Ea(e,"mousedown",function(){t.state.focused&&setTimeout(function(){t.display.input.focus()},0)}),e.setAttribute("cm-not-content","true")},function(e,n){"horizontal"==n?on(t,e):rn(t,e)},t),t.display.scrollbars.addClass&&Ja(t.display.wrapper,t.display.scrollbars.addClass)}function y(e,t){t||(t=p(e));var n=e.display.barWidth,r=e.display.barHeight;x(e,t);for(var i=0;4>i&&n!=e.display.barWidth||r!=e.display.barHeight;i++)n!=e.display.barWidth&&e.options.lineWrapping&&O(e),x(e,p(e)),n=e.display.barWidth,r=e.display.barHeight}function x(e,t){var n=e.display,r=n.scrollbars.update(t);n.sizer.style.paddingRight=(n.barWidth=r.right)+"px",n.sizer.style.paddingBottom=(n.barHeight=r.bottom)+"px",n.heightForcer.style.borderBottom=r.bottom+"px solid transparent",r.right&&r.bottom?(n.scrollbarFiller.style.display="block",n.scrollbarFiller.style.height=r.bottom+"px",n.scrollbarFiller.style.width=r.right+"px"):n.scrollbarFiller.style.display="",r.bottom&&e.options.coverGutterNextToScrollbar&&e.options.fixedGutter?(n.gutterFiller.style.display="block",n.gutterFiller.style.height=r.bottom+"px",n.gutterFiller.style.width=t.gutterWidth+"px"):n.gutterFiller.style.display=""}function b(e,t,n){var r=n&&null!=n.top?Math.max(0,n.top):e.scroller.scrollTop;r=Math.floor(r-Ue(e));var i=n&&null!=n.bottom?n.bottom:r+e.wrapper.clientHeight,o=ni(t,r),a=ni(t,i);if(n&&n.ensure){var l=n.ensure.from.line,s=n.ensure.to.line;o>l?(o=l,a=ni(t,ri(Zr(t,l))+e.wrapper.clientHeight)):Math.min(s,t.lastLine())>=a&&(o=ni(t,ri(Zr(t,s))-e.wrapper.clientHeight),a=s)}return{from:o,to:Math.max(a,o+1)}}function w(e){var t=e.display,n=t.view;if(t.alignWidgets||t.gutters.firstChild&&e.options.fixedGutter){for(var r=C(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,o=r+"px",a=0;a=n.viewFrom&&t.visible.to<=n.viewTo&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&0==zt(e))return!1;k(e)&&(Wt(e),t.dims=P(e));var i=r.first+r.size,o=Math.max(t.visible.from-e.options.viewportMargin,r.first),a=Math.min(i,t.visible.to+e.options.viewportMargin);n.viewFroma&&n.viewTo-a<20&&(a=Math.min(i,n.viewTo)),Wo&&(o=br(e.doc,o),a=wr(e.doc,a));var l=o!=n.viewFrom||a!=n.viewTo||n.lastWrapHeight!=t.wrapperHeight||n.lastWrapWidth!=t.wrapperWidth;Ft(e,o,a),n.viewOffset=ri(Zr(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px";var s=zt(e);if(!l&&0==s&&!t.force&&n.renderedView==n.view&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo))return!1;var c=Gi();return s>4&&(n.lineDiv.style.display="none"),R(e,n.updateLineNumbers,t.dims),s>4&&(n.lineDiv.style.display=""),n.renderedView=n.view,c&&Gi()!=c&&c.offsetHeight&&c.focus(),Ui(n.cursorDiv),Ui(n.selectionDiv),n.gutters.style.height=n.sizer.style.minHeight=0,l&&(n.lastWrapHeight=t.wrapperHeight,n.lastWrapWidth=t.wrapperWidth,_e(e,400)),n.updateLineNumbers=null,!0}function N(e,t){for(var n=t.viewport,r=!0;(r&&e.options.lineWrapping&&t.oldDisplayWidth!=$e(e)||(n&&null!=n.top&&(n={top:Math.min(e.doc.height+qe(e.display)-Ve(e),n.top)}),t.visible=b(e.display,e.doc,n),!(t.visible.from>=e.display.viewFrom&&t.visible.to<=e.display.viewTo)))&&M(e,t);r=!1){O(e);var i=p(e);Re(e),y(e,i),E(e,i)}t.signal(e,"update",e),e.display.viewFrom==e.display.reportedViewFrom&&e.display.viewTo==e.display.reportedViewTo||(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function A(e,t){var n=new L(e,t);if(M(e,n)){O(e),N(e,n);var r=p(e);Re(e),y(e,r),E(e,r),n.finish()}}function E(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+Ye(e)+"px"}function O(e){for(var t=e.display,n=t.lineDiv.offsetTop,r=0;rbo){var a=o.node.offsetTop+o.node.offsetHeight;i=a-n,n=a}else{var l=o.node.getBoundingClientRect();i=l.bottom-l.top}var s=o.line.height-i;if(2>i&&(i=yt(t)),(s>.001||-.001>s)&&(ei(o.line,i),I(o.line),o.rest))for(var c=0;c=t&&f.lineNumber;f.changes&&(Pi(f.changes,"gutter")>-1&&(h=!1),D(e,f,c,n)),h&&(Ui(f.lineNumber),f.lineNumber.appendChild(document.createTextNode(S(e.options,c)))),l=f.node.nextSibling}else{var d=U(e,f,c,n);a.insertBefore(d,l)}c+=f.size}for(;l;)l=r(l)}function D(e,t,n,r){for(var i=0;ibo&&(e.node.style.zIndex=2)),e.node}function W(e){var t=e.bgClass?e.bgClass+" "+(e.line.bgClass||""):e.line.bgClass;if(t&&(t+=" CodeMirror-linebackground"),e.background)t?e.background.className=t:(e.background.parentNode.removeChild(e.background),e.background=null);else if(t){var n=H(e);e.background=n.insertBefore(ji("div",null,t),n.firstChild)}}function B(e,t){var n=e.display.externalMeasured;return n&&n.line==t.line?(e.display.externalMeasured=null,t.measure=n.measure,n.built):Br(e,t)}function _(e,t){var n=t.text.className,r=B(e,t);t.text==t.node&&(t.node=r.pre),t.text.parentNode.replaceChild(r.pre,t.text),t.text=r.pre,r.bgClass!=t.bgClass||r.textClass!=t.textClass?(t.bgClass=r.bgClass,t.textClass=r.textClass,F(t)):n&&(t.text.className=n)}function F(e){W(e),e.line.wrapClass?H(e).className=e.line.wrapClass:e.node!=e.text&&(e.node.className="");var t=e.textClass?e.textClass+" "+(e.line.textClass||""):e.line.textClass;e.text.className=t||""}function z(e,t,n,r){if(t.gutter&&(t.node.removeChild(t.gutter),t.gutter=null),t.gutterBackground&&(t.node.removeChild(t.gutterBackground),t.gutterBackground=null),t.line.gutterClass){var i=H(t);t.gutterBackground=ji("div",null,"CodeMirror-gutter-background "+t.line.gutterClass,"left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px; width: "+r.gutterTotalWidth+"px"),i.insertBefore(t.gutterBackground,t.text)}var o=t.line.gutterMarkers;if(e.options.lineNumbers||o){var i=H(t),a=t.gutter=ji("div",null,"CodeMirror-gutter-wrapper","left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px");if(e.display.input.setUneditable(a),i.insertBefore(a,t.text),t.line.gutterClass&&(a.className+=" "+t.line.gutterClass),!e.options.lineNumbers||o&&o["CodeMirror-linenumbers"]||(t.lineNumber=a.appendChild(ji("div",S(e.options,n),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+r.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+e.display.lineNumInnerWidth+"px"))),o)for(var l=0;l1)if(Fo&&Fo.text.join("\n")==t){if(r.ranges.length%Fo.text.length==0){s=[];for(var c=0;c=0;c--){var u=r.ranges[c],f=u.from(),h=u.to();u.empty()&&(n&&n>0?f=Bo(f.line,f.ch-n):e.state.overwrite&&!a?h=Bo(h.line,Math.min(Zr(o,h.line).text.length,h.ch+Ii(l).length)):Fo&&Fo.lineWise&&Fo.text.join("\n")==t&&(f=h=Bo(f.line,0)));var d=e.curOp.updateInput,p={from:f,to:h,text:s?s[c%s.length]:l,origin:i||(a?"paste":e.state.cutIncoming?"cut":"+input")};Tn(e.doc,p),Ci(e,"inputRead",e,p)}t&&!a&&Q(e,t),Bn(e),e.curOp.updateInput=d,e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=!1}function J(e,t){var n=e.clipboardData&&e.clipboardData.getData("text/plain");return n?(e.preventDefault(),t.isReadOnly()||t.options.disableInput||At(t,function(){Z(t,n,0,null,"paste")}),!0):void 0}function Q(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var n=e.doc.sel,r=n.ranges.length-1;r>=0;r--){var i=n.ranges[r];if(!(i.head.ch>100||r&&n.ranges[r-1].head.line==i.head.line)){var o=e.getModeAt(i.head),a=!1;if(o.electricChars){for(var l=0;l-1){a=Fn(e,i.head.line,"smart");break}}else o.electricInput&&o.electricInput.test(Zr(e.doc,i.head.line).text.slice(0,i.head.ch))&&(a=Fn(e,i.head.line,"smart"));a&&Ci(e,"electricInput",e,i.head.line)}}}function ee(e){for(var t=[],n=[],r=0;ri?c.map:u[i],a=0;ai?e.line:e.rest[i]),f=o[a]+r;return(0>r||l!=t)&&(f=o[a+(r?1:0)]),Bo(s,f)}}}var i=e.text.firstChild,o=!1;if(!t||!Va(i,t))return ae(Bo(ti(e.line),0),!0);if(t==i&&(o=!0,t=i.childNodes[n],n=0,!t)){var a=e.rest?Ii(e.rest):e.line;return ae(Bo(ti(a),a.text.length),o)}var l=3==t.nodeType?t:null,s=t;for(l||1!=t.childNodes.length||3!=t.firstChild.nodeType||(l=t.firstChild,n&&(n=l.nodeValue.length));s.parentNode!=i;)s=s.parentNode;var c=e.measure,u=c.maps,f=r(l,s,n);if(f)return ae(f,o);for(var h=s.nextSibling,d=l?l.nodeValue.length-n:0;h;h=h.nextSibling){if(f=r(h,h.firstChild,0))return ae(Bo(f.line,f.ch-d),o);d+=h.textContent.length}for(var p=s.previousSibling,d=n;p;p=p.previousSibling){if(f=r(p,p.firstChild,-1))return ae(Bo(f.line,f.ch+d),o);d+=h.textContent.length}}function ce(e,t,n,r,i){function o(e){return function(t){return t.id==e}}function a(t){if(1==t.nodeType){var n=t.getAttribute("cm-text");if(null!=n)return""==n&&(n=t.textContent.replace(/\u200b/g,"")),void(l+=n);var u,f=t.getAttribute("cm-marker");if(f){var h=e.findMarks(Bo(r,0),Bo(i+1,0),o(+f));return void(h.length&&(u=h[0].find())&&(l+=Jr(e.doc,u.from,u.to).join(c)))}if("false"==t.getAttribute("contenteditable"))return;for(var d=0;d=0){var a=K(o.from(),i.from()),l=V(o.to(),i.to()),s=o.empty()?i.from()==i.head:o.from()==o.head;t>=r&&--t,e.splice(--r,2,new fe(s?l:a,s?a:l))}}return new ue(e,t)}function de(e,t){return new ue([new fe(e,t||e)],0)}function pe(e,t){return Math.max(e.first,Math.min(t,e.first+e.size-1))}function me(e,t){if(t.linen?Bo(n,Zr(e,n).text.length):ge(t,Zr(e,t.line).text.length)}function ge(e,t){var n=e.ch;return null==n||n>t?Bo(e.line,t):0>n?Bo(e.line,0):e}function ve(e,t){return t>=e.first&&t=t.ch:l.to>t.ch))){if(i&&(Pa(s,"beforeCursorEnter"),s.explicitlyCleared)){if(o.markedSpans){--a;continue}break}if(!s.atomic)continue;if(n){var c,u=s.find(0>r?1:-1);if((0>r?s.inclusiveRight:s.inclusiveLeft)&&(u=Pe(e,u,-r,u&&u.line==t.line?o:null)),u&&u.line==t.line&&(c=_o(u,n))&&(0>r?0>c:c>0))return Oe(e,u,t,r,i)}var f=s.find(0>r?-1:1);return(0>r?s.inclusiveLeft:s.inclusiveRight)&&(f=Pe(e,f,r,f.line==t.line?o:null)),f?Oe(e,f,t,r,i):null}}return t}function Ie(e,t,n,r,i){var o=r||1,a=Oe(e,t,n,o,i)||!i&&Oe(e,t,n,o,!0)||Oe(e,t,n,-o,i)||!i&&Oe(e,t,n,-o,!0);return a?a:(e.cantEdit=!0,Bo(e.first,0))}function Pe(e,t,n,r){return 0>n&&0==t.ch?t.line>e.first?me(e,Bo(t.line-1)):null:n>0&&t.ch==(r||Zr(e,t.line)).text.length?t.line=e.display.viewTo||l.to().linet&&(t=0),t=Math.round(t),r=Math.round(r),l.appendChild(ji("div",null,"CodeMirror-selected","position: absolute; left: "+e+"px; top: "+t+"px; width: "+(null==n?u-e:n)+"px; height: "+(r-t)+"px"))}function i(t,n,i){function o(n,r){return ht(e,Bo(t,n),"div",f,r)}var l,s,f=Zr(a,t),h=f.text.length;return eo(ii(f),n||0,null==i?h:i,function(e,t,a){var f,d,p,m=o(e,"left");if(e==t)f=m,d=p=m.left;else{if(f=o(t-1,"right"),"rtl"==a){var g=m;m=f,f=g}d=m.left,p=f.right}null==n&&0==e&&(d=c),f.top-m.top>3&&(r(d,m.top,null,m.bottom),d=c,m.bottoms.bottom||f.bottom==s.bottom&&f.right>s.right)&&(s=f),c+1>d&&(d=c),r(d,f.top,p-d,f.bottom)}),{start:l,end:s}}var o=e.display,a=e.doc,l=document.createDocumentFragment(),s=Ge(e.display),c=s.left,u=Math.max(o.sizerWidth,$e(e)-o.sizer.offsetLeft)-s.right,f=t.from(),h=t.to();if(f.line==h.line)i(f.line,f.ch,h.ch);else{var d=Zr(a,f.line),p=Zr(a,h.line),m=yr(d)==yr(p),g=i(f.line,f.ch,m?d.text.length+1:null).end,v=i(h.line,m?0:null,h.ch).start;m&&(g.top0?t.blinker=setInterval(function(){t.cursorDiv.style.visibility=(n=!n)?"":"hidden"},e.options.cursorBlinkRate):e.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function _e(e,t){e.doc.mode.startState&&e.doc.frontier=e.display.viewTo)){var n=+new Date+e.options.workTime,r=sa(t.mode,je(e,t.frontier)),i=[];t.iter(t.frontier,Math.min(t.first+t.size,e.display.viewTo+500),function(o){if(t.frontier>=e.display.viewFrom){var a=o.styles,l=o.text.length>e.options.maxHighlightLength,s=Rr(e,o,l?sa(t.mode,r):r,!0);o.styles=s.styles;var c=o.styleClasses,u=s.classes;u?o.styleClasses=u:c&&(o.styleClasses=null);for(var f=!a||a.length!=o.styles.length||c!=u&&(!c||!u||c.bgClass!=u.bgClass||c.textClass!=u.textClass),h=0;!f&&hn?(_e(e,e.options.workDelay),!0):void 0}),i.length&&At(e,function(){for(var t=0;ta;--l){if(l<=o.first)return o.first;var s=Zr(o,l-1);if(s.stateAfter&&(!n||l<=o.frontier))return l;var c=Fa(s.text,null,e.options.tabSize);(null==i||r>c)&&(i=l-1,r=c)}return i}function je(e,t,n){var r=e.doc,i=e.display;if(!r.mode.startState)return!0;var o=ze(e,t,n),a=o>r.first&&Zr(r,o-1).stateAfter;return a=a?sa(r.mode,a):ca(r.mode),r.iter(o,t,function(n){Hr(e,n.text,a);var l=o==t-1||o%5==0||o>=i.viewFrom&&o2&&o.push((s.bottom+c.top)/2-n.top)}}o.push(n.bottom-n.top)}}function Xe(e,t,n){if(e.line==t)return{map:e.measure.map,cache:e.measure.cache};for(var r=0;rn)return{map:e.measure.maps[r],cache:e.measure.caches[r],before:!0}}function Ze(e,t){t=yr(t);var n=ti(t),r=e.display.externalMeasured=new Pt(e.doc,t,n);r.lineN=n;var i=r.built=Br(e,r);return r.text=i.pre,qi(e.display.lineMeasure,i.pre),r}function Je(e,t,n,r){return tt(e,et(e,t),n,r)}function Qe(e,t){if(t>=e.display.viewFrom&&t=n.lineN&&tt?(i=0,o=1,a="left"):c>t?(i=t-s,o=i+1):(l==e.length-3||t==c&&e[l+3]>t)&&(o=c-s,i=o-1,t>=c&&(a="right")),null!=i){if(r=e[l+2],s==c&&n==(r.insertLeft?"left":"right")&&(a=n),"left"==n&&0==i)for(;l&&e[l-2]==e[l-3]&&e[l-1].insertLeft;)r=e[(l-=3)+2],a="left";if("right"==n&&i==c-s)for(;lu;u++){for(;l&&zi(t.line.text.charAt(o.coverStart+l));)--l;for(;o.coverStart+sbo&&0==l&&s==o.coverEnd-o.coverStart)i=a.parentNode.getBoundingClientRect();else if(xo&&e.options.lineWrapping){var f=qa(a,l,s).getClientRects();i=f.length?f["right"==r?f.length-1:0]:qo}else i=qa(a,l,s).getBoundingClientRect()||qo;if(i.left||i.right||0==l)break;s=l,l-=1,c="right"}xo&&11>bo&&(i=it(e.display.measure,i))}else{l>0&&(c=r="right");var f;i=e.options.lineWrapping&&(f=a.getClientRects()).length>1?f["right"==r?f.length-1:0]:a.getBoundingClientRect()}if(xo&&9>bo&&!l&&(!i||!i.left&&!i.right)){var h=a.parentNode.getClientRects()[0];i=h?{left:h.left,right:h.left+xt(e.display),top:h.top,bottom:h.bottom}:qo}for(var d=i.top-t.rect.top,p=i.bottom-t.rect.top,m=(d+p)/2,g=t.view.measure.heights,u=0;un.from?a(e-1):a(e,r)}r=r||Zr(e.doc,t.line),i||(i=et(e,r));var s=ii(r),c=t.ch;if(!s)return a(c);var u=co(s,c),f=l(c,u);return null!=al&&(f.other=l(c,al)),f}function pt(e,t){var n=0,t=me(e.doc,t);e.options.lineWrapping||(n=xt(e.display)*t.ch);var r=Zr(e.doc,t.line),i=ri(r)+Ue(e.display);return{left:n,right:n,top:i,bottom:i+r.height}}function mt(e,t,n,r){var i=Bo(e,t);return i.xRel=r,n&&(i.outside=!0),i}function gt(e,t,n){var r=e.doc;if(n+=e.display.viewOffset,0>n)return mt(r.first,0,!0,-1);var i=ni(r,n),o=r.first+r.size-1;if(i>o)return mt(r.first+r.size-1,Zr(r,o).text.length,!0,1);0>t&&(t=0);for(var a=Zr(r,i);;){var l=vt(e,a,i,t,n),s=gr(a),c=s&&s.find(0,!0);if(!s||!(l.ch>c.from.ch||l.ch==c.from.ch&&l.xRel>0))return l;i=ti(a=c.to.line)}}function vt(e,t,n,r,i){function o(r){var i=dt(e,Bo(n,r),"line",t,c);return l=!0,a>i.bottom?i.left-s:ag)return mt(n,d,v,1);for(;;){if(u?d==h||d==fo(t,h,1):1>=d-h){for(var y=p>r||g-r>=r-p?h:d,x=r-(y==h?p:g);zi(t.text.charAt(y));)++y;var b=mt(n,y,y==h?m:v,-1>x?-1:x>1?1:0);return b}var w=Math.ceil(f/2),k=h+w;if(u){k=h;for(var S=0;w>S;++S)k=fo(t,k,1)}var C=o(k);C>r?(d=k,g=C,(v=l)&&(g+=1e3),f=w):(h=k,p=C,m=l,f-=w)}}function yt(e){if(null!=e.cachedTextHeight)return e.cachedTextHeight;if(null==zo){zo=ji("pre");for(var t=0;49>t;++t)zo.appendChild(document.createTextNode("x")),zo.appendChild(ji("br"));zo.appendChild(document.createTextNode("x"))}qi(e.measure,zo);var n=zo.offsetHeight/50;return n>3&&(e.cachedTextHeight=n),Ui(e.measure),n||1}function xt(e){if(null!=e.cachedCharWidth)return e.cachedCharWidth;var t=ji("span","xxxxxxxxxx"),n=ji("pre",[t]);qi(e.measure,n);var r=t.getBoundingClientRect(),i=(r.right-r.left)/10;return i>2&&(e.cachedCharWidth=i),i||10}function bt(e){e.curOp={cm:e,viewChanged:!1,startHeight:e.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Yo},Go?Go.ops.push(e.curOp):e.curOp.ownsGroup=Go={ops:[e.curOp],delayedCallbacks:[]}}function wt(e){var t=e.delayedCallbacks,n=0;do{for(;n=n.viewTo)||n.maxLineChanged&&t.options.lineWrapping,e.update=e.mustUpdate&&new L(t,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}function Lt(e){e.updatedDisplay=e.mustUpdate&&M(e.cm,e.update)}function Tt(e){var t=e.cm,n=t.display;e.updatedDisplay&&O(t),e.barMeasure=p(t),n.maxLineChanged&&!t.options.lineWrapping&&(e.adjustWidthTo=Je(t,n.maxLine,n.maxLine.text.length).left+3,t.display.sizerWidth=e.adjustWidthTo,e.barMeasure.scrollWidth=Math.max(n.scroller.clientWidth,n.sizer.offsetLeft+e.adjustWidthTo+Ye(t)+t.display.barWidth),e.maxScrollLeft=Math.max(0,n.sizer.offsetLeft+e.adjustWidthTo-$e(t))),(e.updatedDisplay||e.selectionChanged)&&(e.preparedSelection=n.input.prepareSelection(e.focus))}function Mt(e){var t=e.cm;null!=e.adjustWidthTo&&(t.display.sizer.style.minWidth=e.adjustWidthTo+"px",e.maxScrollLefto;o=r){var a=new Pt(e.doc,Zr(e.doc,o),o);r=o+a.size,i.push(a)}return i}function Dt(e,t,n,r){null==t&&(t=e.doc.first),null==n&&(n=e.doc.first+e.doc.size),r||(r=0);var i=e.display;if(r&&nt)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)Wo&&br(e.doc,t)i.viewFrom?Wt(e):(i.viewFrom+=r,i.viewTo+=r);else if(t<=i.viewFrom&&n>=i.viewTo)Wt(e);else if(t<=i.viewFrom){var o=_t(e,n,n+r,1);o?(i.view=i.view.slice(o.index),i.viewFrom=o.lineN,i.viewTo+=r):Wt(e)}else if(n>=i.viewTo){var o=_t(e,t,t,-1);o?(i.view=i.view.slice(0,o.index),i.viewTo=o.lineN):Wt(e)}else{var a=_t(e,t,t,-1),l=_t(e,n,n+r,1);a&&l?(i.view=i.view.slice(0,a.index).concat(Rt(e,a.lineN,l.lineN)).concat(i.view.slice(l.index)),i.viewTo+=r):Wt(e)}var s=i.externalMeasured;s&&(n=i.lineN&&t=r.viewTo)){var o=r.view[Bt(e,t)];if(null!=o.node){var a=o.changes||(o.changes=[]);-1==Pi(a,n)&&a.push(n)}}}function Wt(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function Bt(e,t){if(t>=e.display.viewTo)return null;if(t-=e.display.viewFrom,0>t)return null;for(var n=e.display.view,r=0;rt)return r}function _t(e,t,n,r){var i,o=Bt(e,t),a=e.display.view;if(!Wo||n==e.doc.first+e.doc.size)return{index:o,lineN:n};for(var l=0,s=e.display.viewFrom;o>l;l++)s+=a[l].size;if(s!=t){if(r>0){if(o==a.length-1)return null;i=s+a[o].size-t,o++}else i=s-t;t+=i,n+=i}for(;br(e.doc,n)!=n;){if(o==(0>r?0:a.length-1))return null;n+=r*a[o-(0>r?1:0)].size,o+=r}return{index:o,lineN:n}}function Ft(e,t,n){var r=e.display,i=r.view;0==i.length||t>=r.viewTo||n<=r.viewFrom?(r.view=Rt(e,t,n),r.viewFrom=t):(r.viewFrom>t?r.view=Rt(e,t,r.viewFrom).concat(r.view):r.viewFromn&&(r.view=r.view.slice(0,Bt(e,n)))),r.viewTo=n}function zt(e){for(var t=e.display.view,n=0,r=0;r400}var i=e.display;Ea(i.scroller,"mousedown",Et(e,$t)),xo&&11>bo?Ea(i.scroller,"dblclick",Et(e,function(t){if(!Ti(e,t)){var n=Yt(e,t);if(n&&!Jt(e,t)&&!Gt(e.display,t)){Ma(t);var r=e.findWordAt(n);be(e.doc,r.anchor,r.head)}}})):Ea(i.scroller,"dblclick",function(t){Ti(e,t)||Ma(t)}),Do||Ea(i.scroller,"contextmenu",function(t){xn(e,t)});var o,a={end:0};Ea(i.scroller,"touchstart",function(t){if(!Ti(e,t)&&!n(t)){clearTimeout(o);var r=+new Date;i.activeTouch={start:r,moved:!1,prev:r-a.end<=300?a:null},1==t.touches.length&&(i.activeTouch.left=t.touches[0].pageX,i.activeTouch.top=t.touches[0].pageY)}}),Ea(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),Ea(i.scroller,"touchend",function(n){var o=i.activeTouch;if(o&&!Gt(i,n)&&null!=o.left&&!o.moved&&new Date-o.start<300){var a,l=e.coordsChar(i.activeTouch,"page");a=!o.prev||r(o,o.prev)?new fe(l,l):!o.prev.prev||r(o,o.prev.prev)?e.findWordAt(l):new fe(Bo(l.line,0),me(e.doc,Bo(l.line+1,0))),e.setSelection(a.anchor,a.head),e.focus(),Ma(n)}t()}),Ea(i.scroller,"touchcancel",t),Ea(i.scroller,"scroll",function(){i.scroller.clientHeight&&(rn(e,i.scroller.scrollTop),on(e,i.scroller.scrollLeft,!0),Pa(e,"scroll",e))}),Ea(i.scroller,"mousewheel",function(t){an(e,t)}),Ea(i.scroller,"DOMMouseScroll",function(t){an(e,t)}),Ea(i.wrapper,"scroll",function(){i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(t){Ti(e,t)||Aa(t)},over:function(t){Ti(e,t)||(tn(e,t),Aa(t))},start:function(t){en(e,t)},drop:Et(e,Qt),leave:function(t){Ti(e,t)||nn(e)}};var l=i.input.getField();Ea(l,"keyup",function(t){pn.call(e,t)}),Ea(l,"keydown",Et(e,hn)),Ea(l,"keypress",Et(e,mn)),Ea(l,"focus",Bi(vn,e)),Ea(l,"blur",Bi(yn,e))}function Ut(t,n,r){var i=r&&r!=e.Init;if(!n!=!i){var o=t.display.dragFunctions,a=n?Ea:Ia;a(t.display.scroller,"dragstart",o.start),a(t.display.scroller,"dragenter",o.enter),a(t.display.scroller,"dragover",o.over),a(t.display.scroller,"dragleave",o.leave),a(t.display.scroller,"drop",o.drop)}}function qt(e){var t=e.display;t.lastWrapHeight==t.wrapper.clientHeight&&t.lastWrapWidth==t.wrapper.clientWidth||(t.cachedCharWidth=t.cachedTextHeight=t.cachedPaddingH=null,t.scrollbarsClipped=!1,e.setSize())}function Gt(e,t){for(var n=wi(t);n!=e.wrapper;n=n.parentNode)if(!n||1==n.nodeType&&"true"==n.getAttribute("cm-ignore-events")||n.parentNode==e.sizer&&n!=e.mover)return!0}function Yt(e,t,n,r){var i=e.display;if(!n&&"true"==wi(t).getAttribute("cm-not-content"))return null;var o,a,l=i.lineSpace.getBoundingClientRect();try{o=t.clientX-l.left,a=t.clientY-l.top}catch(t){return null}var s,c=gt(e,o,a);if(r&&1==c.xRel&&(s=Zr(e.doc,c.line).text).length==c.ch){var u=Fa(s,s.length,e.options.tabSize)-s.length;c=Bo(c.line,Math.max(0,Math.round((o-Ge(e.display).left)/xt(e.display))-u))}return c}function $t(e){var t=this,n=t.display;if(!(Ti(t,e)||n.activeTouch&&n.input.supportsTouch())){if(n.shift=e.shiftKey,Gt(n,e))return void(wo||(n.scroller.draggable=!1,setTimeout(function(){n.scroller.draggable=!0},100)));if(!Jt(t,e)){var r=Yt(t,e);switch(window.focus(),ki(e)){case 1:t.state.selectingText?t.state.selectingText(e):r?Vt(t,e,r):wi(e)==n.scroller&&Ma(e);break;case 2:wo&&(t.state.lastMiddleDown=+new Date),r&&be(t.doc,r),setTimeout(function(){n.input.focus()},20),Ma(e);break;case 3:Do?xn(t,e):gn(t)}}}}function Vt(e,t,n){xo?setTimeout(Bi(X,e),0):e.curOp.focus=Gi();var r,i=+new Date;Uo&&Uo.time>i-400&&0==_o(Uo.pos,n)?r="triple":jo&&jo.time>i-400&&0==_o(jo.pos,n)?(r="double",Uo={time:i,pos:n}):(r="single",jo={time:i,pos:n});var o,a=e.doc.sel,l=Eo?t.metaKey:t.ctrlKey;e.options.dragDrop&&el&&!e.isReadOnly()&&"single"==r&&(o=a.contains(n))>-1&&(_o((o=a.ranges[o]).from(),n)<0||n.xRel>0)&&(_o(o.to(),n)>0||n.xRel<0)?Kt(e,t,n,l):Xt(e,t,n,r,l)}function Kt(e,t,n,r){var i=e.display,o=+new Date,a=Et(e,function(l){wo&&(i.scroller.draggable=!1),e.state.draggingText=!1,Ia(document,"mouseup",a),Ia(i.scroller,"drop",a),Math.abs(t.clientX-l.clientX)+Math.abs(t.clientY-l.clientY)<10&&(Ma(l),!r&&+new Date-200=p;p++){var v=Zr(c,p).text,y=za(v,s,o);s==d?i.push(new fe(Bo(p,y),Bo(p,y))):v.length>y&&i.push(new fe(Bo(p,y),Bo(p,za(v,d,o))))}i.length||i.push(new fe(n,n)),Te(c,he(h.ranges.slice(0,f).concat(i),f),{origin:"*mouse",scroll:!1}),e.scrollIntoView(t)}else{var x=u,b=x.anchor,w=t;if("single"!=r){if("double"==r)var k=e.findWordAt(t);else var k=new fe(Bo(t.line,0),me(c,Bo(t.line+1,0)));_o(k.anchor,b)>0?(w=k.head,b=K(x.from(),k.anchor)):(w=k.anchor,b=V(x.to(),k.head))}var i=h.ranges.slice(0);i[f]=new fe(me(c,b),w),Te(c,he(i,f),Ba)}}function a(t){var n=++y,i=Yt(e,t,!0,"rect"==r);if(i)if(0!=_o(i,g)){e.curOp.focus=Gi(),o(i);var l=b(s,c);(i.line>=l.to||i.linev.bottom?20:0;u&&setTimeout(Et(e,function(){y==n&&(s.scroller.scrollTop+=u,a(t))}),50)}}function l(t){e.state.selectingText=!1,y=1/0,Ma(t),s.input.focus(),Ia(document,"mousemove",x),Ia(document,"mouseup",w),c.history.lastSelOrigin=null}var s=e.display,c=e.doc;Ma(t);var u,f,h=c.sel,d=h.ranges;if(i&&!t.shiftKey?(f=c.sel.contains(n),u=f>-1?d[f]:new fe(n,n)):(u=c.sel.primary(),f=c.sel.primIndex),Oo?t.shiftKey&&t.metaKey:t.altKey)r="rect",i||(u=new fe(n,n)),n=Yt(e,t,!0,!0),f=-1;else if("double"==r){var p=e.findWordAt(n);u=e.display.shift||c.extend?xe(c,u,p.anchor,p.head):p}else if("triple"==r){var m=new fe(Bo(n.line,0),me(c,Bo(n.line+1,0)));u=e.display.shift||c.extend?xe(c,u,m.anchor,m.head):m}else u=xe(c,u,n);i?-1==f?(f=d.length,Te(c,he(d.concat([u]),f),{scroll:!1,origin:"*mouse"})):d.length>1&&d[f].empty()&&"single"==r&&!t.shiftKey?(Te(c,he(d.slice(0,f).concat(d.slice(f+1)),0),{scroll:!1,origin:"*mouse"}),h=c.sel):ke(c,f,u,Ba):(f=0,Te(c,new ue([u],0),Ba),h=c.sel);var g=n,v=s.wrapper.getBoundingClientRect(),y=0,x=Et(e,function(e){ki(e)?a(e):l(e)}),w=Et(e,l);e.state.selectingText=w,Ea(document,"mousemove",x),Ea(document,"mouseup",w)}function Zt(e,t,n,r){try{var i=t.clientX,o=t.clientY}catch(t){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&Ma(t);var a=e.display,l=a.lineDiv.getBoundingClientRect();if(o>l.bottom||!Ni(e,n))return bi(t);o-=l.top-a.viewOffset;for(var s=0;s=i){var u=ni(e.doc,o),f=e.options.gutters[s];return Pa(e,n,e,u,f,t),bi(t)}}}function Jt(e,t){return Zt(e,t,"gutterClick",!0)}function Qt(e){var t=this;if(nn(t),!Ti(t,e)&&!Gt(t.display,e)){Ma(e),xo&&($o=+new Date);var n=Yt(t,e,!0),r=e.dataTransfer.files;if(n&&!t.isReadOnly())if(r&&r.length&&window.FileReader&&window.File)for(var i=r.length,o=Array(i),a=0,l=function(e,r){if(!t.options.allowDropFileTypes||-1!=Pi(t.options.allowDropFileTypes,e.type)){var l=new FileReader;l.onload=Et(t,function(){var e=l.result;if(/[\x00-\x08\x0e-\x1f]{2}/.test(e)&&(e=""),o[r]=e,++a==i){n=me(t.doc,n);var s={from:n,to:n,text:t.doc.splitLines(o.join(t.doc.lineSeparator())),origin:"paste"};Tn(t.doc,s),Le(t.doc,de(n,Qo(s)))}}),l.readAsText(e)}},s=0;i>s;++s)l(r[s],s);else{if(t.state.draggingText&&t.doc.sel.contains(n)>-1)return t.state.draggingText(e),void setTimeout(function(){t.display.input.focus()},20);try{var o=e.dataTransfer.getData("Text");if(o){if(t.state.draggingText&&!(Eo?e.altKey:e.ctrlKey))var c=t.listSelections();if(Me(t.doc,de(n,n)),c)for(var s=0;sa.clientWidth,s=a.scrollHeight>a.clientHeight;if(r&&l||i&&s){if(i&&Eo&&wo)e:for(var c=t.target,u=o.view;c!=a;c=c.parentNode)for(var f=0;fh?d=Math.max(0,d+h-50):p=Math.min(e.doc.height,p+h+50),A(e,{top:d,bottom:p})}20>Vo&&(null==o.wheelStartX?(o.wheelStartX=a.scrollLeft,o.wheelStartY=a.scrollTop,o.wheelDX=r,o.wheelDY=i,setTimeout(function(){if(null!=o.wheelStartX){var e=a.scrollLeft-o.wheelStartX,t=a.scrollTop-o.wheelStartY,n=t&&o.wheelDY&&t/o.wheelDY||e&&o.wheelDX&&e/o.wheelDX;o.wheelStartX=o.wheelStartY=null,n&&(Ko=(Ko*Vo+n)/(Vo+1),++Vo)}},200)):(o.wheelDX+=r,o.wheelDY+=i))}}function ln(e,t,n){if("string"==typeof t&&(t=ua[t],!t))return!1;e.display.input.ensurePolled();var r=e.display.shift,i=!1;try{e.isReadOnly()&&(e.state.suppressEdits=!0),n&&(e.display.shift=!1),i=t(e)!=Ha}finally{e.display.shift=r,e.state.suppressEdits=!1}return i}function sn(e,t,n){for(var r=0;rbo&&27==e.keyCode&&(e.returnValue=!1);var n=e.keyCode;t.display.shift=16==n||e.shiftKey;var r=un(t,e);Co&&(Jo=r?n:null,!r&&88==n&&!rl&&(Eo?e.metaKey:e.ctrlKey)&&t.replaceSelection("",null,"cut")),18!=n||/\bCodeMirror-crosshair\b/.test(t.display.lineDiv.className)||dn(t)}}function dn(e){function t(e){18!=e.keyCode&&e.altKey||(Za(n,"CodeMirror-crosshair"),Ia(document,"keyup",t),Ia(document,"mouseover",t))}var n=e.display.lineDiv;Ja(n,"CodeMirror-crosshair"),Ea(document,"keyup",t),Ea(document,"mouseover",t)}function pn(e){16==e.keyCode&&(this.doc.sel.shift=!1),Ti(this,e)}function mn(e){var t=this;if(!(Gt(t.display,e)||Ti(t,e)||e.ctrlKey&&!e.altKey||Eo&&e.metaKey)){var n=e.keyCode,r=e.charCode;if(Co&&n==Jo)return Jo=null,void Ma(e);if(!Co||e.which&&!(e.which<10)||!un(t,e)){var i=String.fromCharCode(null==r?n:r);fn(t,e,i)||t.display.input.onKeyPress(e)}}}function gn(e){e.state.delayingBlurEvent=!0,setTimeout(function(){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1,yn(e))},100)}function vn(e){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1),"nocursor"!=e.options.readOnly&&(e.state.focused||(Pa(e,"focus",e),e.state.focused=!0,Ja(e.display.wrapper,"CodeMirror-focused"),e.curOp||e.display.selForContextMenu==e.doc.sel||(e.display.input.reset(),wo&&setTimeout(function(){e.display.input.reset(!0)},20)),e.display.input.receivedFocus()),Be(e))}function yn(e){e.state.delayingBlurEvent||(e.state.focused&&(Pa(e,"blur",e),e.state.focused=!1,Za(e.display.wrapper,"CodeMirror-focused")),clearInterval(e.display.blinker),setTimeout(function(){e.state.focused||(e.display.shift=!1)},150))}function xn(e,t){Gt(e.display,t)||bn(e,t)||Ti(e,t,"contextmenu")||e.display.input.onContextMenu(t)}function bn(e,t){return Ni(e,"gutterContextMenu")?Zt(e,t,"gutterContextMenu",!1):!1}function wn(e,t){if(_o(e,t.from)<0)return e;if(_o(e,t.to)<=0)return Qo(t);var n=e.line+t.text.length-(t.to.line-t.from.line)-1,r=e.ch;return e.line==t.to.line&&(r+=Qo(t).ch-t.to.ch),Bo(n,r)}function kn(e,t){for(var n=[],r=0;r=0;--i)Mn(e,{from:r[i].from,to:r[i].to,text:i?[""]:t.text});else Mn(e,t)}}function Mn(e,t){if(1!=t.text.length||""!=t.text[0]||0!=_o(t.from,t.to)){var n=kn(e,t);ci(e,t,n,e.cm?e.cm.curOp.id:NaN),En(e,t,n,or(e,t));var r=[];Kr(e,function(e,n){n||-1!=Pi(r,e.history)||(xi(e.history,t),r.push(e.history)),En(e,t,null,or(e,t))})}}function Nn(e,t,n){if(!e.cm||!e.cm.state.suppressEdits){for(var r,i=e.history,o=e.sel,a="undo"==t?i.done:i.undone,l="undo"==t?i.undone:i.done,s=0;s=0;--s){var f=r.changes[s];if(f.origin=t,u&&!Ln(e,f,!1))return void(a.length=0);c.push(ai(e,f));var h=s?kn(e,f):Ii(a);En(e,f,h,lr(e,f)),!s&&e.cm&&e.cm.scrollIntoView({from:f.from,to:Qo(f)});var d=[];Kr(e,function(e,t){t||-1!=Pi(d,e.history)||(xi(e.history,f),d.push(e.history)),En(e,f,null,lr(e,f))})}}}}function An(e,t){if(0!=t&&(e.first+=t,e.sel=new ue(Ri(e.sel.ranges,function(e){return new fe(Bo(e.anchor.line+t,e.anchor.ch),Bo(e.head.line+t,e.head.ch))}),e.sel.primIndex),e.cm)){Dt(e.cm,e.first,e.first-t,t);for(var n=e.cm.display,r=n.viewFrom;re.lastLine())){if(t.from.lineo&&(t={from:t.from,to:Bo(o,Zr(e,o).text.length),text:[t.text[0]],origin:t.origin}),t.removed=Jr(e,t.from,t.to),n||(n=kn(e,t)),e.cm?On(e.cm,t,r):Yr(e,t,r),Me(e,n,Wa)}}function On(e,t,n){var r=e.doc,i=e.display,a=t.from,l=t.to,s=!1,c=a.line;e.options.lineWrapping||(c=ti(yr(Zr(r,a.line))),r.iter(c,l.line+1,function(e){return e==i.maxLine?(s=!0,!0):void 0})),r.sel.contains(t.from,t.to)>-1&&Mi(e),Yr(r,t,n,o(e)),e.options.lineWrapping||(r.iter(c,a.line+t.text.length,function(e){var t=f(e);t>i.maxLineLength&&(i.maxLine=e,i.maxLineLength=t,i.maxLineChanged=!0,s=!1)}),s&&(e.curOp.updateMaxLine=!0)),r.frontier=Math.min(r.frontier,a.line),_e(e,400);var u=t.text.length-(l.line-a.line)-1;t.full?Dt(e):a.line!=l.line||1!=t.text.length||Gr(e.doc,t)?Dt(e,a.line,l.line+1,u):Ht(e,a.line,"text");var h=Ni(e,"changes"),d=Ni(e,"change");if(d||h){var p={from:a,to:l,text:t.text,removed:t.removed,origin:t.origin};d&&Ci(e,"change",e,p),h&&(e.curOp.changeObjs||(e.curOp.changeObjs=[])).push(p)}e.display.selForContextMenu=null}function In(e,t,n,r,i){if(r||(r=n),_o(r,n)<0){var o=r;r=n,n=o}"string"==typeof t&&(t=e.splitLines(t)),Tn(e,{from:n,to:r,text:t,origin:i})}function Pn(e,t){if(!Ti(e,"scrollCursorIntoView")){var n=e.display,r=n.sizer.getBoundingClientRect(),i=null;if(t.top+r.top<0?i=!0:t.bottom+r.top>(window.innerHeight||document.documentElement.clientHeight)&&(i=!1),null!=i&&!Mo){var o=ji("div","​",null,"position: absolute; top: "+(t.top-n.viewOffset-Ue(e.display))+"px; height: "+(t.bottom-t.top+Ye(e)+n.barHeight)+"px; left: "+t.left+"px; width: 2px;");e.display.lineSpace.appendChild(o),o.scrollIntoView(i),e.display.lineSpace.removeChild(o)}}}function Rn(e,t,n,r){null==r&&(r=0);for(var i=0;5>i;i++){var o=!1,a=dt(e,t),l=n&&n!=t?dt(e,n):a,s=Hn(e,Math.min(a.left,l.left),Math.min(a.top,l.top)-r,Math.max(a.left,l.left),Math.max(a.bottom,l.bottom)+r),c=e.doc.scrollTop,u=e.doc.scrollLeft;if(null!=s.scrollTop&&(rn(e,s.scrollTop),Math.abs(e.doc.scrollTop-c)>1&&(o=!0)),null!=s.scrollLeft&&(on(e,s.scrollLeft),Math.abs(e.doc.scrollLeft-u)>1&&(o=!0)),!o)break}return a}function Dn(e,t,n,r,i){var o=Hn(e,t,n,r,i);null!=o.scrollTop&&rn(e,o.scrollTop),null!=o.scrollLeft&&on(e,o.scrollLeft)}function Hn(e,t,n,r,i){var o=e.display,a=yt(e.display);0>n&&(n=0);var l=e.curOp&&null!=e.curOp.scrollTop?e.curOp.scrollTop:o.scroller.scrollTop,s=Ve(e),c={};i-n>s&&(i=n+s);var u=e.doc.height+qe(o),f=a>n,h=i>u-a;if(l>n)c.scrollTop=f?0:n;else if(i>l+s){var d=Math.min(n,(h?u:i)-s);d!=l&&(c.scrollTop=d)}var p=e.curOp&&null!=e.curOp.scrollLeft?e.curOp.scrollLeft:o.scroller.scrollLeft,m=$e(e)-(e.options.fixedGutter?o.gutters.offsetWidth:0),g=r-t>m;return g&&(r=t+m),10>t?c.scrollLeft=0:p>t?c.scrollLeft=Math.max(0,t-(g?0:10)):r>m+p-3&&(c.scrollLeft=r+(g?0:10)-m),c}function Wn(e,t,n){null==t&&null==n||_n(e),null!=t&&(e.curOp.scrollLeft=(null==e.curOp.scrollLeft?e.doc.scrollLeft:e.curOp.scrollLeft)+t),null!=n&&(e.curOp.scrollTop=(null==e.curOp.scrollTop?e.doc.scrollTop:e.curOp.scrollTop)+n)}function Bn(e){_n(e);var t=e.getCursor(),n=t,r=t;e.options.lineWrapping||(n=t.ch?Bo(t.line,t.ch-1):t,r=Bo(t.line,t.ch+1)),e.curOp.scrollToPos={from:n,to:r,margin:e.options.cursorScrollMargin,isCursor:!0}}function _n(e){var t=e.curOp.scrollToPos;if(t){e.curOp.scrollToPos=null;var n=pt(e,t.from),r=pt(e,t.to),i=Hn(e,Math.min(n.left,r.left),Math.min(n.top,r.top)-t.margin,Math.max(n.right,r.right),Math.max(n.bottom,r.bottom)+t.margin);e.scrollTo(i.scrollLeft,i.scrollTop)}}function Fn(e,t,n,r){var i,o=e.doc;null==n&&(n="add"),"smart"==n&&(o.mode.indent?i=je(e,t):n="prev");var a=e.options.tabSize,l=Zr(o,t),s=Fa(l.text,null,a);l.stateAfter&&(l.stateAfter=null);var c,u=l.text.match(/^\s*/)[0];if(r||/\S/.test(l.text)){if("smart"==n&&(c=o.mode.indent(i,l.text.slice(u.length),l.text),c==Ha||c>150)){if(!r)return;n="prev"}}else c=0,n="not";"prev"==n?c=t>o.first?Fa(Zr(o,t-1).text,null,a):0:"add"==n?c=s+e.options.indentUnit:"subtract"==n?c=s-e.options.indentUnit:"number"==typeof n&&(c=s+n),c=Math.max(0,c);var f="",h=0;if(e.options.indentWithTabs)for(var d=Math.floor(c/a);d;--d)h+=a,f+=" ";if(c>h&&(f+=Oi(c-h)),f!=u)return In(o,f,Bo(t,0),Bo(t,u.length),"+input"),l.stateAfter=null,!0;for(var d=0;d=0;t--)In(e.doc,"",r[t].from,r[t].to,"+delete");Bn(e)})}function Un(e,t,n,r,i){function o(){var t=l+n;return t=e.first+e.size?!1:(l=t,u=Zr(e,t))}function a(e){var t=(i?fo:ho)(u,s,n,!0);if(null==t){if(e||!o())return!1;s=i?(0>n?io:ro)(u):0>n?u.text.length:0}else s=t;return!0}var l=t.line,s=t.ch,c=n,u=Zr(e,l);if("char"==r)a();else if("column"==r)a(!0);else if("word"==r||"group"==r)for(var f=null,h="group"==r,d=e.cm&&e.cm.getHelper(t,"wordChars"),p=!0;!(0>n)||a(!p);p=!1){var m=u.text.charAt(s)||"\n",g=_i(m,d)?"w":h&&"\n"==m?"n":!h||/\s/.test(m)?null:"p";if(!h||p||g||(g="s"),f&&f!=g){0>n&&(n=1,a());break}if(g&&(f=g),n>0&&!a(!p))break}var v=Ie(e,Bo(l,s),t,c,!0);return _o(t,v)||(v.hitSide=!0),v}function qn(e,t,n,r){var i,o=e.doc,a=t.left;if("page"==r){var l=Math.min(e.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight);i=t.top+n*(l-(0>n?1.5:.5)*yt(e.display))}else"line"==r&&(i=n>0?t.bottom+3:t.top-3);for(;;){var s=gt(e,a,i);if(!s.outside)break;if(0>n?0>=i:i>=o.height){s.hitSide=!0;break}i+=5*n}return s}function Gn(t,n,r,i){e.defaults[t]=n,r&&(ta[t]=i?function(e,t,n){n!=na&&r(e,t,n)}:r)}function Yn(e){for(var t,n,r,i,o=e.split(/-(?!$)/),e=o[o.length-1],a=0;a0||0==a&&o.clearWhenEmpty!==!1)return o;if(o.replacedWith&&(o.collapsed=!0,o.widgetNode=ji("span",[o.replacedWith],"CodeMirror-widget"),r.handleMouseEvents||o.widgetNode.setAttribute("cm-ignore-events","true"),r.insertLeft&&(o.widgetNode.insertLeft=!0)),o.collapsed){if(vr(e,t.line,t,n,o)||t.line!=n.line&&vr(e,n.line,t,n,o))throw new Error("Inserting collapsed marker partially overlapping an existing one");Wo=!0}o.addToHistory&&ci(e,{from:t,to:n,origin:"markText"},e.sel,NaN);var l,s=t.line,c=e.cm;if(e.iter(s,n.line+1,function(e){c&&o.collapsed&&!c.options.lineWrapping&&yr(e)==c.display.maxLine&&(l=!0),o.collapsed&&s!=t.line&&ei(e,0),nr(e,new Qn(o,s==t.line?t.ch:null,s==n.line?n.ch:null)),++s}),o.collapsed&&e.iter(t.line,n.line+1,function(t){kr(e,t)&&ei(t,0)}),o.clearOnEnter&&Ea(o,"beforeCursorEnter",function(){o.clear()}),o.readOnly&&(Ho=!0,(e.history.done.length||e.history.undone.length)&&e.clearHistory()),o.collapsed&&(o.id=++ga,o.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),o.collapsed)Dt(c,t.line,n.line+1);else if(o.className||o.title||o.startStyle||o.endStyle||o.css)for(var u=t.line;u<=n.line;u++)Ht(c,u,"text");o.atomic&&Ae(c.doc),Ci(c,"markerAdded",c,o)}return o}function Kn(e,t,n,r,i){r=Wi(r),r.shared=!1;var o=[Vn(e,t,n,r,i)],a=o[0],l=r.widgetNode;return Kr(e,function(e){l&&(r.widgetNode=l.cloneNode(!0)),o.push(Vn(e,me(e,t),me(e,n),r,i));for(var s=0;s=t:o.to>t);(r||(r=[])).push(new Qn(a,o.from,s?null:o.to))}}return r}function ir(e,t,n){if(e)for(var r,i=0;i=t:o.to>t);if(l||o.from==t&&"bookmark"==a.type&&(!n||o.marker.insertLeft)){var s=null==o.from||(a.inclusiveLeft?o.from<=t:o.from0&&l)for(var f=0;ff;++f)p.push(m);p.push(s)}return p}function ar(e){for(var t=0;t0)){var u=[s,1],f=_o(c.from,l.from),h=_o(c.to,l.to);(0>f||!a.inclusiveLeft&&!f)&&u.push({from:c.from,to:l.from}),(h>0||!a.inclusiveRight&&!h)&&u.push({from:l.to,to:c.to}),i.splice.apply(i,u),s+=u.length-1}}return i}function cr(e){var t=e.markedSpans;if(t){for(var n=0;n=0&&0>=f||0>=u&&f>=0)&&(0>=u&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.to,n)>=0:_o(c.to,n)>0)||u>=0&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.from,r)<=0:_o(c.from,r)<0)))return!0}}}function yr(e){for(var t;t=mr(e);)e=t.find(-1,!0).line;return e}function xr(e){for(var t,n;t=gr(e);)e=t.find(1,!0).line,(n||(n=[])).push(e);return n}function br(e,t){var n=Zr(e,t),r=yr(n);return n==r?t:ti(r)}function wr(e,t){if(t>e.lastLine())return t;var n,r=Zr(e,t);if(!kr(e,r))return t;for(;n=gr(r);)r=n.find(1,!0).line;return ti(r)+1}function kr(e,t){var n=Wo&&t.markedSpans;if(n)for(var r,i=0;io;o++){i&&(i[0]=e.innerMode(t,r).mode);var a=t.token(n,r);if(n.pos>n.start)return a}throw new Error("Mode "+t.name+" failed to advance stream.")}function Ir(e,t,n,r){function i(e){return{start:f.start,end:f.pos,string:f.current(),type:o||null,state:e?sa(a.mode,u):u}}var o,a=e.doc,l=a.mode;t=me(a,t);var s,c=Zr(a,t.line),u=je(e,t.line,n),f=new ma(c.text,e.options.tabSize);for(r&&(s=[]);(r||f.pose.options.maxHighlightLength?(l=!1,a&&Hr(e,t,r,f.pos),f.pos=t.length,s=null):s=Ar(Or(n,f,r,h),o),h){var d=h[0].name;d&&(s="m-"+(s?d+" "+s:d))}if(!l||u!=s){for(;cc;){var r=i[s];r>e&&i.splice(s,1,e,i[s+1],r),s+=2,c=Math.min(e,r)}if(t)if(l.opaque)i.splice(n,s-n,e,"cm-overlay "+t),s=n+2;else for(;s>n;n+=2){var o=i[n+1];i[n+1]=(o?o+" ":"")+"cm-overlay "+t}},o)}return{styles:i,classes:o.bgClass||o.textClass?o:null}}function Dr(e,t,n){if(!t.styles||t.styles[0]!=e.state.modeGen){var r=je(e,ti(t)),i=Rr(e,t,t.text.length>e.options.maxHighlightLength?sa(e.doc.mode,r):r);t.stateAfter=r,t.styles=i.styles,i.classes?t.styleClasses=i.classes:t.styleClasses&&(t.styleClasses=null),n===e.doc.frontier&&e.doc.frontier++}return t.styles}function Hr(e,t,n,r){var i=e.doc.mode,o=new ma(t,e.options.tabSize);for(o.start=o.pos=r||0,""==t&&Er(i,n);!o.eol();)Or(i,o,n),o.start=o.pos}function Wr(e,t){if(!e||/^\s*$/.test(e))return null;var n=t.addModeClass?ka:wa;return n[e]||(n[e]=e.replace(/\S+/g,"cm-$&"))}function Br(e,t){var n=ji("span",null,null,wo?"padding-right: .1px":null),r={pre:ji("pre",[n],"CodeMirror-line"),content:n,col:0,pos:0,cm:e,splitSpaces:(xo||wo)&&e.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var o,a=i?t.rest[i-1]:t.line;r.pos=0,r.addToken=Fr,Ji(e.display.measure)&&(o=ii(a))&&(r.addToken=jr(r.addToken,o)),r.map=[];var l=t!=e.display.externalMeasured&&ti(a);qr(a,r,Dr(e,a,l)),a.styleClasses&&(a.styleClasses.bgClass&&(r.bgClass=$i(a.styleClasses.bgClass,r.bgClass||"")),a.styleClasses.textClass&&(r.textClass=$i(a.styleClasses.textClass,r.textClass||""))),0==r.map.length&&r.map.push(0,0,r.content.appendChild(Zi(e.display.measure))),0==i?(t.measure.map=r.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(r.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(wo){var s=r.content.lastChild;(/\bcm-tab\b/.test(s.className)||s.querySelector&&s.querySelector(".cm-tab"))&&(r.content.className="cm-tab-wrap-hack")}return Pa(e,"renderLine",e,t.line,r.pre),r.pre.className&&(r.textClass=$i(r.pre.className,r.textClass||"")),r}function _r(e){var t=ji("span","•","cm-invalidchar");return t.title="\\u"+e.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function Fr(e,t,n,r,i,o,a){if(t){var l=e.splitSpaces?t.replace(/ {3,}/g,zr):t,s=e.cm.state.specialChars,c=!1;if(s.test(t))for(var u=document.createDocumentFragment(),f=0;;){s.lastIndex=f;var h=s.exec(t),d=h?h.index-f:t.length-f;if(d){var p=document.createTextNode(l.slice(f,f+d));xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.map.push(e.pos,e.pos+d,p),e.col+=d,e.pos+=d}if(!h)break;if(f+=d+1," "==h[0]){var m=e.cm.options.tabSize,g=m-e.col%m,p=u.appendChild(ji("span",Oi(g),"cm-tab"));p.setAttribute("role","presentation"),p.setAttribute("cm-text"," "),e.col+=g}else if("\r"==h[0]||"\n"==h[0]){var p=u.appendChild(ji("span","\r"==h[0]?"␍":"␤","cm-invalidchar"));p.setAttribute("cm-text",h[0]),e.col+=1}else{var p=e.cm.options.specialCharPlaceholder(h[0]);p.setAttribute("cm-text",h[0]),xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.col+=1}e.map.push(e.pos,e.pos+1,p),e.pos++}else{e.col+=t.length;var u=document.createTextNode(l);e.map.push(e.pos,e.pos+t.length,u),xo&&9>bo&&(c=!0),e.pos+=t.length}if(n||r||i||c||a){var v=n||"";r&&(v+=r),i&&(v+=i);var y=ji("span",[u],v,a);return o&&(y.title=o),e.content.appendChild(y)}e.content.appendChild(u)}}function zr(e){for(var t=" ",n=0;nc&&h.from<=c)break}if(h.to>=u)return e(n,r,i,o,a,l,s);e(n,r.slice(0,h.to-c),i,o,null,l,s),o=null,r=r.slice(h.to-c),c=h.to}}}function Ur(e,t,n,r){var i=!r&&n.widgetNode;i&&e.map.push(e.pos,e.pos+t,i),!r&&e.cm.display.input.needsContentAttribute&&(i||(i=e.content.appendChild(document.createElement("span"))),i.setAttribute("cm-marker",n.id)),i&&(e.cm.display.input.setUneditable(i),e.content.appendChild(i)),e.pos+=t}function qr(e,t,n){var r=e.markedSpans,i=e.text,o=0;if(r)for(var a,l,s,c,u,f,h,d=i.length,p=0,m=1,g="",v=0;;){if(v==p){s=c=u=f=l="",h=null,v=1/0;for(var y,x=[],b=0;bp||k.collapsed&&w.to==p&&w.from==p)?(null!=w.to&&w.to!=p&&v>w.to&&(v=w.to,c=""),k.className&&(s+=" "+k.className),k.css&&(l=(l?l+";":"")+k.css),k.startStyle&&w.from==p&&(u+=" "+k.startStyle),k.endStyle&&w.to==v&&(y||(y=[])).push(k.endStyle,w.to),k.title&&!f&&(f=k.title),k.collapsed&&(!h||dr(h.marker,k)<0)&&(h=w)):w.from>p&&v>w.from&&(v=w.from)}if(y)for(var b=0;b=d)break;for(var S=Math.min(d,v);;){if(g){var C=p+g.length;if(!h){var L=C>S?g.slice(0,S-p):g;t.addToken(t,L,a?a+s:s,u,p+L.length==v?c:"",f,l)}if(C>=S){g=g.slice(S-p),p=S;break}p=C,u=""}g=i.slice(o,o=n[m++]),a=Wr(n[m++],t.cm.options)}}else for(var m=1;mn;++n)o.push(new ba(c[n],i(n),r));return o}var l=t.from,s=t.to,c=t.text,u=Zr(e,l.line),f=Zr(e,s.line),h=Ii(c),d=i(c.length-1),p=s.line-l.line;if(t.full)e.insert(0,a(0,c.length)),e.remove(c.length,e.size-c.length);else if(Gr(e,t)){var m=a(0,c.length-1);o(f,f.text,d),p&&e.remove(l.line,p),m.length&&e.insert(l.line,m)}else if(u==f)if(1==c.length)o(u,u.text.slice(0,l.ch)+h+u.text.slice(s.ch),d);else{var m=a(1,c.length-1);m.push(new ba(h+u.text.slice(s.ch),d,r)),o(u,u.text.slice(0,l.ch)+c[0],i(0)),e.insert(l.line+1,m)}else if(1==c.length)o(u,u.text.slice(0,l.ch)+c[0]+f.text.slice(s.ch),i(0)),e.remove(l.line+1,p);else{o(u,u.text.slice(0,l.ch)+c[0],i(0)),o(f,h+f.text.slice(s.ch),d);var m=a(1,c.length-1);p>1&&e.remove(l.line+1,p-1),e.insert(l.line+1,m)}Ci(e,"change",e,t)}function $r(e){this.lines=e,this.parent=null;for(var t=0,n=0;tt||t>=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var n=e;!n.lines;)for(var r=0;;++r){var i=n.children[r],o=i.chunkSize();if(o>t){n=i;break}t-=o}return n.lines[t]}function Jr(e,t,n){var r=[],i=t.line;return e.iter(t.line,n.line+1,function(e){var o=e.text;i==n.line&&(o=o.slice(0,n.ch)),i==t.line&&(o=o.slice(t.ch)),r.push(o),++i}),r}function Qr(e,t,n){var r=[];return e.iter(t,n,function(e){r.push(e.text)}),r}function ei(e,t){var n=t-e.height;if(n)for(var r=e;r;r=r.parent)r.height+=n}function ti(e){if(null==e.parent)return null;for(var t=e.parent,n=Pi(t.lines,e),r=t.parent;r;t=r,r=r.parent)for(var i=0;r.children[i]!=t;++i)n+=r.children[i].chunkSize();return n+t.first}function ni(e,t){var n=e.first;e:do{for(var r=0;rt){e=i;continue e}t-=o,n+=i.chunkSize()}return n}while(!e.lines);for(var r=0;rt)break;t-=l}return n+r}function ri(e){e=yr(e);for(var t=0,n=e.parent,r=0;r1&&!e.done[e.done.length-2].ranges?(e.done.pop(),Ii(e.done)):void 0}function ci(e,t,n,r){var i=e.history;i.undone.length=0;var o,a=+new Date;if((i.lastOp==r||i.lastOrigin==t.origin&&t.origin&&("+"==t.origin.charAt(0)&&e.cm&&i.lastModTime>a-e.cm.options.historyEventDelay||"*"==t.origin.charAt(0)))&&(o=si(i,i.lastOp==r))){var l=Ii(o.changes);0==_o(t.from,t.to)&&0==_o(t.from,l.to)?l.to=Qo(t):o.changes.push(ai(e,t))}else{var s=Ii(i.done);for(s&&s.ranges||hi(e.sel,i.done),o={changes:[ai(e,t)],generation:i.generation},i.done.push(o);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(n),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=a,i.lastOp=i.lastSelOp=r,i.lastOrigin=i.lastSelOrigin=t.origin,l||Pa(e,"historyAdded")}function ui(e,t,n,r){var i=t.charAt(0);return"*"==i||"+"==i&&n.ranges.length==r.ranges.length&&n.somethingSelected()==r.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)}function fi(e,t,n,r){var i=e.history,o=r&&r.origin;n==i.lastSelOp||o&&i.lastSelOrigin==o&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==o||ui(e,o,Ii(i.done),t))?i.done[i.done.length-1]=t:hi(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=o,i.lastSelOp=n,r&&r.clearRedo!==!1&&li(i.undone)}function hi(e,t){var n=Ii(t);n&&n.ranges&&n.equals(e)||t.push(e)}function di(e,t,n,r){var i=t["spans_"+e.id],o=0;e.iter(Math.max(e.first,n),Math.min(e.first+e.size,r),function(n){n.markedSpans&&((i||(i=t["spans_"+e.id]={}))[o]=n.markedSpans),++o})}function pi(e){if(!e)return null;for(var t,n=0;n-1&&(Ii(l)[f]=u[f],delete u[f])}}}return i}function vi(e,t,n,r){n0?r.slice():Oa:r||Oa}function Ci(e,t){function n(e){return function(){e.apply(null,o)}}var r=Si(e,t,!1);if(r.length){var i,o=Array.prototype.slice.call(arguments,2);Go?i=Go.delayedCallbacks:Ra?i=Ra:(i=Ra=[],setTimeout(Li,0));for(var a=0;a0}function Ai(e){e.prototype.on=function(e,t){Ea(this,e,t)},e.prototype.off=function(e,t){Ia(this,e,t)}}function Ei(){this.id=null}function Oi(e){for(;ja.length<=e;)ja.push(Ii(ja)+" ");return ja[e]}function Ii(e){return e[e.length-1]}function Pi(e,t){for(var n=0;n-1&&Ya(e)?!0:t.test(e):Ya(e)}function Fi(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}function zi(e){return e.charCodeAt(0)>=768&&$a.test(e)}function ji(e,t,n,r){var i=document.createElement(e);if(n&&(i.className=n),r&&(i.style.cssText=r),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var o=0;o0;--t)e.removeChild(e.firstChild);return e}function qi(e,t){return Ui(e).appendChild(t)}function Gi(){for(var e=document.activeElement;e&&e.root&&e.root.activeElement;)e=e.root.activeElement;return e}function Yi(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}function $i(e,t){for(var n=e.split(" "),r=0;r2&&!(xo&&8>bo))}var n=Ka?ji("span","​"):ji("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return n.setAttribute("cm-text",""),n}function Ji(e){if(null!=Xa)return Xa;var t=qi(e,document.createTextNode("AخA")),n=qa(t,0,1).getBoundingClientRect();if(!n||n.left==n.right)return!1;var r=qa(t,1,2).getBoundingClientRect();return Xa=r.right-n.right<3}function Qi(e){if(null!=il)return il;var t=qi(e,ji("span","x")),n=t.getBoundingClientRect(),r=qa(t,0,1).getBoundingClientRect();return il=Math.abs(n.left-r.left)>1}function eo(e,t,n,r){if(!e)return r(t,n,"ltr");for(var i=!1,o=0;ot||t==n&&a.to==t)&&(r(Math.max(a.from,t),Math.min(a.to,n),1==a.level?"rtl":"ltr"),i=!0)}i||r(t,n,"ltr")}function to(e){return e.level%2?e.to:e.from}function no(e){return e.level%2?e.from:e.to}function ro(e){var t=ii(e);return t?to(t[0]):0}function io(e){var t=ii(e);return t?no(Ii(t)):e.text.length}function oo(e,t){var n=Zr(e.doc,t),r=yr(n);r!=n&&(t=ti(r));var i=ii(r),o=i?i[0].level%2?io(r):ro(r):0;return Bo(t,o)}function ao(e,t){for(var n,r=Zr(e.doc,t);n=gr(r);)r=n.find(1,!0).line,t=null;var i=ii(r),o=i?i[0].level%2?ro(r):io(r):r.text.length;return Bo(null==t?ti(r):t,o)}function lo(e,t){var n=oo(e,t.line),r=Zr(e.doc,n.line),i=ii(r);if(!i||0==i[0].level){var o=Math.max(0,r.text.search(/\S/)),a=t.line==n.line&&t.ch<=o&&t.ch;return Bo(n.line,a?0:o)}return n}function so(e,t,n){var r=e[0].level;return t==r?!0:n==r?!1:n>t}function co(e,t){al=null;for(var n,r=0;rt)return r;if(i.from==t||i.to==t){if(null!=n)return so(e,i.level,e[n].level)?(i.from!=i.to&&(al=n),r):(i.from!=i.to&&(al=r),n);n=r}}return n}function uo(e,t,n,r){if(!r)return t+n;do t+=n;while(t>0&&zi(e.text.charAt(t)));return t}function fo(e,t,n,r){var i=ii(e);if(!i)return ho(e,t,n,r);for(var o=co(i,t),a=i[o],l=uo(e,t,a.level%2?-n:n,r);;){if(l>a.from&&l0==a.level%2?a.to:a.from);if(a=i[o+=n],!a)return null;l=n>0==a.level%2?uo(e,a.to,-1,r):uo(e,a.from,1,r)}}function ho(e,t,n,r){var i=t+n;if(r)for(;i>0&&zi(e.text.charAt(i));)i+=n;return 0>i||i>e.text.length?null:i}var po=navigator.userAgent,mo=navigator.platform,go=/gecko\/\d/i.test(po),vo=/MSIE \d/.test(po),yo=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(po),xo=vo||yo,bo=xo&&(vo?document.documentMode||6:yo[1]),wo=/WebKit\//.test(po),ko=wo&&/Qt\/\d+\.\d+/.test(po),So=/Chrome\//.test(po),Co=/Opera\//.test(po),Lo=/Apple Computer/.test(navigator.vendor),To=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(po),Mo=/PhantomJS/.test(po),No=/AppleWebKit/.test(po)&&/Mobile\/\w+/.test(po),Ao=No||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(po),Eo=No||/Mac/.test(mo),Oo=/\bCrOS\b/.test(po),Io=/win/i.test(mo),Po=Co&&po.match(/Version\/(\d*\.\d*)/);Po&&(Po=Number(Po[1])),Po&&Po>=15&&(Co=!1,wo=!0);var Ro=Eo&&(ko||Co&&(null==Po||12.11>Po)),Do=go||xo&&bo>=9,Ho=!1,Wo=!1;m.prototype=Wi({update:function(e){var t=e.scrollWidth>e.clientWidth+1,n=e.scrollHeight>e.clientHeight+1,r=e.nativeBarWidth;if(n){this.vert.style.display="block",this.vert.style.bottom=t?r+"px":"0";var i=e.viewHeight-(t?r:0);this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+i)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=n?r+"px":"0",this.horiz.style.left=e.barLeft+"px";var o=e.viewWidth-e.barLeft-(n?r:0);this.horiz.firstChild.style.width=e.scrollWidth-e.clientWidth+o+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&e.clientHeight>0&&(0==r&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:n?r:0,bottom:t?r:0}},setScrollLeft:function(e){this.horiz.scrollLeft!=e&&(this.horiz.scrollLeft=e),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz)},setScrollTop:function(e){this.vert.scrollTop!=e&&(this.vert.scrollTop=e),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert)},zeroWidthHack:function(){var e=Eo&&!To?"12px":"18px";this.horiz.style.height=this.vert.style.width=e,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new Ei,this.disableVert=new Ei},enableZeroWidthBar:function(e,t){function n(){var r=e.getBoundingClientRect(),i=document.elementFromPoint(r.left+1,r.bottom-1);i!=e?e.style.pointerEvents="none":t.set(1e3,n)}e.style.pointerEvents="auto",t.set(1e3,n)},clear:function(){var e=this.horiz.parentNode;e.removeChild(this.horiz),e.removeChild(this.vert)}},m.prototype),g.prototype=Wi({update:function(){return{bottom:0,right:0}},setScrollLeft:function(){},setScrollTop:function(){},clear:function(){}},g.prototype),e.scrollbarModel={"native":m,"null":g},L.prototype.signal=function(e,t){Ni(e,t)&&this.events.push(arguments)},L.prototype.finish=function(){for(var e=0;e=9&&n.hasSelection&&(n.hasSelection=null),n.poll()}),Ea(o,"paste",function(e){Ti(r,e)||J(e,r)||(r.state.pasteIncoming=!0,n.fastPoll())}),Ea(o,"cut",t),Ea(o,"copy",t),Ea(e.scroller,"paste",function(t){Gt(e,t)||Ti(r,t)||(r.state.pasteIncoming=!0,n.focus())}),Ea(e.lineSpace,"selectstart",function(t){Gt(e,t)||Ma(t)}),Ea(o,"compositionstart",function(){var e=r.getCursor("from");n.composing&&n.composing.range.clear(),n.composing={start:e,range:r.markText(e,r.getCursor("to"),{className:"CodeMirror-composing"})}}),Ea(o,"compositionend",function(){n.composing&&(n.poll(),n.composing.range.clear(),n.composing=null)})},prepareSelection:function(){var e=this.cm,t=e.display,n=e.doc,r=De(e);if(e.options.moveInputWithCursor){var i=dt(e,n.sel.primary().head,"div"),o=t.wrapper.getBoundingClientRect(),a=t.lineDiv.getBoundingClientRect();r.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+a.top-o.top)),r.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+a.left-o.left))}return r},showSelection:function(e){var t=this.cm,n=t.display;qi(n.cursorDiv,e.cursors),qi(n.selectionDiv,e.selection),null!=e.teTop&&(this.wrapper.style.top=e.teTop+"px",this.wrapper.style.left=e.teLeft+"px")},reset:function(e){if(!this.contextMenuPending){var t,n,r=this.cm,i=r.doc;if(r.somethingSelected()){this.prevInput="";var o=i.sel.primary();t=rl&&(o.to().line-o.from().line>100||(n=r.getSelection()).length>1e3);var a=t?"-":n||r.getSelection();this.textarea.value=a,r.state.focused&&Ua(this.textarea),xo&&bo>=9&&(this.hasSelection=a)}else e||(this.prevInput=this.textarea.value="",xo&&bo>=9&&(this.hasSelection=null));this.inaccurateSelection=t}},getField:function(){return this.textarea},supportsTouch:function(){return!1},focus:function(){if("nocursor"!=this.cm.options.readOnly&&(!Ao||Gi()!=this.textarea))try{this.textarea.focus()}catch(e){}},blur:function(){this.textarea.blur()},resetPosition:function(){this.wrapper.style.top=this.wrapper.style.left=0; -},receivedFocus:function(){this.slowPoll()},slowPoll:function(){var e=this;e.pollingFast||e.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},fastPoll:function(){function e(){var r=n.poll();r||t?(n.pollingFast=!1,n.slowPoll()):(t=!0,n.polling.set(60,e))}var t=!1,n=this;n.pollingFast=!0,n.polling.set(20,e)},poll:function(){var e=this.cm,t=this.textarea,n=this.prevInput;if(this.contextMenuPending||!e.state.focused||nl(t)&&!n&&!this.composing||e.isReadOnly()||e.options.disableInput||e.state.keySeq)return!1;var r=t.value;if(r==n&&!e.somethingSelected())return!1;if(xo&&bo>=9&&this.hasSelection===r||Eo&&/[\uf700-\uf7ff]/.test(r))return e.display.input.reset(),!1;if(e.doc.sel==e.display.selForContextMenu){var i=r.charCodeAt(0);if(8203!=i||n||(n="​"),8666==i)return this.reset(),this.cm.execCommand("undo")}for(var o=0,a=Math.min(n.length,r.length);a>o&&n.charCodeAt(o)==r.charCodeAt(o);)++o;var l=this;return At(e,function(){Z(e,r.slice(o),n.length-o,null,l.composing?"*compose":null),r.length>1e3||r.indexOf("\n")>-1?t.value=l.prevInput="":l.prevInput=r,l.composing&&(l.composing.range.clear(),l.composing.range=e.markText(l.composing.start,e.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},ensurePolled:function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},onKeyPress:function(){xo&&bo>=9&&(this.hasSelection=null),this.fastPoll()},onContextMenu:function(e){function t(){if(null!=a.selectionStart){var e=i.somethingSelected(),t="​"+(e?a.value:"");a.value="⇚",a.value=t,r.prevInput=e?"":"​",a.selectionStart=1,a.selectionEnd=t.length,o.selForContextMenu=i.doc.sel}}function n(){if(r.contextMenuPending=!1,r.wrapper.style.cssText=f,a.style.cssText=u,xo&&9>bo&&o.scrollbars.setScrollTop(o.scroller.scrollTop=s),null!=a.selectionStart){(!xo||xo&&9>bo)&&t();var e=0,n=function(){o.selForContextMenu==i.doc.sel&&0==a.selectionStart&&a.selectionEnd>0&&"​"==r.prevInput?Et(i,ua.selectAll)(i):e++<10?o.detectingSelectAll=setTimeout(n,500):o.input.reset()};o.detectingSelectAll=setTimeout(n,200)}}var r=this,i=r.cm,o=i.display,a=r.textarea,l=Yt(i,e),s=o.scroller.scrollTop;if(l&&!Co){var c=i.options.resetSelectionOnContextMenu;c&&-1==i.doc.sel.contains(l)&&Et(i,Te)(i.doc,de(l),Wa);var u=a.style.cssText,f=r.wrapper.style.cssText;r.wrapper.style.cssText="position: absolute";var h=r.wrapper.getBoundingClientRect();if(a.style.cssText="position: absolute; width: 30px; height: 30px; top: "+(e.clientY-h.top-5)+"px; left: "+(e.clientX-h.left-5)+"px; z-index: 1000; background: "+(xo?"rgba(255, 255, 255, .05)":"transparent")+"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",wo)var d=window.scrollY;if(o.input.focus(),wo&&window.scrollTo(null,d),o.input.reset(),i.somethingSelected()||(a.value=r.prevInput=" "),r.contextMenuPending=!0,o.selForContextMenu=i.doc.sel,clearTimeout(o.detectingSelectAll),xo&&bo>=9&&t(),Do){Aa(e);var p=function(){Ia(window,"mouseup",p),setTimeout(n,20)};Ea(window,"mouseup",p)}else setTimeout(n,50)}},readOnlyChanged:function(e){e||this.reset()},setUneditable:Di,needsContentAttribute:!1},ne.prototype),ie.prototype=Wi({init:function(e){function t(e){if(!Ti(r,e)){if(r.somethingSelected())Fo={lineWise:!1,text:r.getSelections()},"cut"==e.type&&r.replaceSelection("",null,"cut");else{if(!r.options.lineWiseCopyCut)return;var t=ee(r);Fo={lineWise:!0,text:t.text},"cut"==e.type&&r.operation(function(){r.setSelections(t.ranges,0,Wa),r.replaceSelection("",null,"cut")})}if(e.clipboardData&&!No)e.preventDefault(),e.clipboardData.clearData(),e.clipboardData.setData("text/plain",Fo.text.join("\n"));else{var n=re(),i=n.firstChild;r.display.lineSpace.insertBefore(n,r.display.lineSpace.firstChild),i.value=Fo.text.join("\n");var o=document.activeElement;Ua(i),setTimeout(function(){r.display.lineSpace.removeChild(n),o.focus()},50)}}}var n=this,r=n.cm,i=n.div=e.lineDiv;te(i),Ea(i,"paste",function(e){Ti(r,e)||J(e,r)}),Ea(i,"compositionstart",function(e){var t=e.data;if(n.composing={sel:r.doc.sel,data:t,startData:t},t){var i=r.doc.sel.primary(),o=r.getLine(i.head.line),a=o.indexOf(t,Math.max(0,i.head.ch-t.length));a>-1&&a<=i.head.ch&&(n.composing.sel=de(Bo(i.head.line,a),Bo(i.head.line,a+t.length)))}}),Ea(i,"compositionupdate",function(e){n.composing.data=e.data}),Ea(i,"compositionend",function(e){var t=n.composing;t&&(e.data==t.startData||/\u200b/.test(e.data)||(t.data=e.data),setTimeout(function(){t.handled||n.applyComposition(t),n.composing==t&&(n.composing=null)},50))}),Ea(i,"touchstart",function(){n.forceCompositionEnd()}),Ea(i,"input",function(){n.composing||!r.isReadOnly()&&n.pollContent()||At(n.cm,function(){Dt(r)})}),Ea(i,"copy",t),Ea(i,"cut",t)},prepareSelection:function(){var e=De(this.cm,!1);return e.focus=this.cm.state.focused,e},showSelection:function(e,t){e&&this.cm.display.view.length&&((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},showPrimarySelection:function(){var e=window.getSelection(),t=this.cm.doc.sel.primary(),n=le(this.cm,e.anchorNode,e.anchorOffset),r=le(this.cm,e.focusNode,e.focusOffset);if(!n||n.bad||!r||r.bad||0!=_o(K(n,r),t.from())||0!=_o(V(n,r),t.to())){var i=oe(this.cm,t.from()),o=oe(this.cm,t.to());if(i||o){var a=this.cm.display.view,l=e.rangeCount&&e.getRangeAt(0);if(i){if(!o){var s=a[a.length-1].measure,c=s.maps?s.maps[s.maps.length-1]:s.map;o={node:c[c.length-1],offset:c[c.length-2]-c[c.length-3]}}}else i={node:a[0].measure.map[2],offset:0};try{var u=qa(i.node,i.offset,o.offset,o.node)}catch(f){}u&&(!go&&this.cm.state.focused?(e.collapse(i.node,i.offset),u.collapsed||e.addRange(u)):(e.removeAllRanges(),e.addRange(u)),l&&null==e.anchorNode?e.addRange(l):go&&this.startGracePeriod()),this.rememberSelection()}}},startGracePeriod:function(){var e=this;clearTimeout(this.gracePeriod),this.gracePeriod=setTimeout(function(){e.gracePeriod=!1,e.selectionChanged()&&e.cm.operation(function(){e.cm.curOp.selectionChanged=!0})},20)},showMultipleSelections:function(e){qi(this.cm.display.cursorDiv,e.cursors),qi(this.cm.display.selectionDiv,e.selection)},rememberSelection:function(){var e=window.getSelection();this.lastAnchorNode=e.anchorNode,this.lastAnchorOffset=e.anchorOffset,this.lastFocusNode=e.focusNode,this.lastFocusOffset=e.focusOffset},selectionInEditor:function(){var e=window.getSelection();if(!e.rangeCount)return!1;var t=e.getRangeAt(0).commonAncestorContainer;return Va(this.div,t)},focus:function(){"nocursor"!=this.cm.options.readOnly&&this.div.focus()},blur:function(){this.div.blur()},getField:function(){return this.div},supportsTouch:function(){return!0},receivedFocus:function(){function e(){t.cm.state.focused&&(t.pollSelection(),t.polling.set(t.cm.options.pollInterval,e))}var t=this;this.selectionInEditor()?this.pollSelection():At(this.cm,function(){t.cm.curOp.selectionChanged=!0}),this.polling.set(this.cm.options.pollInterval,e)},selectionChanged:function(){var e=window.getSelection();return e.anchorNode!=this.lastAnchorNode||e.anchorOffset!=this.lastAnchorOffset||e.focusNode!=this.lastFocusNode||e.focusOffset!=this.lastFocusOffset},pollSelection:function(){if(!this.composing&&!this.gracePeriod&&this.selectionChanged()){var e=window.getSelection(),t=this.cm;this.rememberSelection();var n=le(t,e.anchorNode,e.anchorOffset),r=le(t,e.focusNode,e.focusOffset);n&&r&&At(t,function(){Te(t.doc,de(n,r),Wa),(n.bad||r.bad)&&(t.curOp.selectionChanged=!0)})}},pollContent:function(){var e=this.cm,t=e.display,n=e.doc.sel.primary(),r=n.from(),i=n.to();if(r.linet.viewTo-1)return!1;var o;if(r.line==t.viewFrom||0==(o=Bt(e,r.line)))var a=ti(t.view[0].line),l=t.view[0].node;else var a=ti(t.view[o].line),l=t.view[o-1].node.nextSibling;var s=Bt(e,i.line);if(s==t.view.length-1)var c=t.viewTo-1,u=t.lineDiv.lastChild;else var c=ti(t.view[s+1].line)-1,u=t.view[s+1].node.previousSibling;for(var f=e.doc.splitLines(ce(e,l,u,a,c)),h=Jr(e.doc,Bo(a,0),Bo(c,Zr(e.doc,c).text.length));f.length>1&&h.length>1;)if(Ii(f)==Ii(h))f.pop(),h.pop(),c--;else{if(f[0]!=h[0])break;f.shift(),h.shift(),a++}for(var d=0,p=0,m=f[0],g=h[0],v=Math.min(m.length,g.length);v>d&&m.charCodeAt(d)==g.charCodeAt(d);)++d;for(var y=Ii(f),x=Ii(h),b=Math.min(y.length-(1==f.length?d:0),x.length-(1==h.length?d:0));b>p&&y.charCodeAt(y.length-p-1)==x.charCodeAt(x.length-p-1);)++p;f[f.length-1]=y.slice(0,y.length-p),f[0]=f[0].slice(d);var w=Bo(a,d),k=Bo(c,h.length?Ii(h).length-p:0);return f.length>1||f[0]||_o(w,k)?(In(e.doc,f,w,k,"+input"),!0):void 0},ensurePolled:function(){this.forceCompositionEnd()},reset:function(){this.forceCompositionEnd()},forceCompositionEnd:function(){this.composing&&!this.composing.handled&&(this.applyComposition(this.composing),this.composing.handled=!0,this.div.blur(),this.div.focus())},applyComposition:function(e){this.cm.isReadOnly()?Et(this.cm,Dt)(this.cm):e.data&&e.data!=e.startData&&Et(this.cm,Z)(this.cm,e.data,0,e.sel)},setUneditable:function(e){e.contentEditable="false"},onKeyPress:function(e){e.preventDefault(),this.cm.isReadOnly()||Et(this.cm,Z)(this.cm,String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),0)},readOnlyChanged:function(e){this.div.contentEditable=String("nocursor"!=e)},onContextMenu:Di,resetPosition:Di,needsContentAttribute:!0},ie.prototype),e.inputStyles={textarea:ne,contenteditable:ie},ue.prototype={primary:function(){return this.ranges[this.primIndex]},equals:function(e){if(e==this)return!0;if(e.primIndex!=this.primIndex||e.ranges.length!=this.ranges.length)return!1;for(var t=0;t=0&&_o(e,r.to())<=0)return n}return-1}},fe.prototype={from:function(){return K(this.anchor,this.head)},to:function(){return V(this.anchor,this.head)},empty:function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch}};var zo,jo,Uo,qo={left:0,right:0,top:0,bottom:0},Go=null,Yo=0,$o=0,Vo=0,Ko=null;xo?Ko=-.53:go?Ko=15:So?Ko=-.7:Lo&&(Ko=-1/3);var Xo=function(e){var t=e.wheelDeltaX,n=e.wheelDeltaY;return null==t&&e.detail&&e.axis==e.HORIZONTAL_AXIS&&(t=e.detail),null==n&&e.detail&&e.axis==e.VERTICAL_AXIS?n=e.detail:null==n&&(n=e.wheelDelta),{x:t,y:n}};e.wheelEventPixels=function(e){var t=Xo(e);return t.x*=Ko,t.y*=Ko,t};var Zo=new Ei,Jo=null,Qo=e.changeEnd=function(e){return e.text?Bo(e.from.line+e.text.length-1,Ii(e.text).length+(1==e.text.length?e.from.ch:0)):e.to};e.prototype={constructor:e,focus:function(){window.focus(),this.display.input.focus()},setOption:function(e,t){var n=this.options,r=n[e];n[e]==t&&"mode"!=e||(n[e]=t,ta.hasOwnProperty(e)&&Et(this,ta[e])(this,t,r))},getOption:function(e){return this.options[e]},getDoc:function(){return this.doc},addKeyMap:function(e,t){this.state.keyMaps[t?"push":"unshift"]($n(e))},removeKeyMap:function(e){for(var t=this.state.keyMaps,n=0;nn&&(Fn(this,i.head.line,e,!0),n=i.head.line,r==this.doc.sel.primIndex&&Bn(this));else{var o=i.from(),a=i.to(),l=Math.max(n,o.line);n=Math.min(this.lastLine(),a.line-(a.ch?0:1))+1;for(var s=l;n>s;++s)Fn(this,s,e);var c=this.doc.sel.ranges;0==o.ch&&t.length==c.length&&c[r].from().ch>0&&ke(this.doc,r,new fe(o,c[r].to()),Wa)}}}),getTokenAt:function(e,t){return Ir(this,e,t)},getLineTokens:function(e,t){return Ir(this,Bo(e),t,!0)},getTokenTypeAt:function(e){e=me(this.doc,e);var t,n=Dr(this,Zr(this.doc,e.line)),r=0,i=(n.length-1)/2,o=e.ch;if(0==o)t=n[2];else for(;;){var a=r+i>>1;if((a?n[2*a-1]:0)>=o)i=a;else{if(!(n[2*a+1]l?t:0==l?null:t.slice(0,l-1)},getModeAt:function(t){var n=this.doc.mode;return n.innerMode?e.innerMode(n,this.getTokenAt(t).state).mode:n},getHelper:function(e,t){return this.getHelpers(e,t)[0]},getHelpers:function(e,t){var n=[];if(!la.hasOwnProperty(t))return n;var r=la[t],i=this.getModeAt(e);if("string"==typeof i[t])r[i[t]]&&n.push(r[i[t]]);else if(i[t])for(var o=0;oi&&(e=i,r=!0),n=Zr(this.doc,e)}else n=e;return ut(this,n,{top:0,left:0},t||"page").top+(r?this.doc.height-ri(n):0)},defaultTextHeight:function(){return yt(this.display)},defaultCharWidth:function(){return xt(this.display)},setGutterMarker:Ot(function(e,t,n){return zn(this.doc,e,"gutter",function(e){var r=e.gutterMarkers||(e.gutterMarkers={});return r[t]=n,!n&&Fi(r)&&(e.gutterMarkers=null),!0})}),clearGutter:Ot(function(e){var t=this,n=t.doc,r=n.first;n.iter(function(n){n.gutterMarkers&&n.gutterMarkers[e]&&(n.gutterMarkers[e]=null,Ht(t,r,"gutter"),Fi(n.gutterMarkers)&&(n.gutterMarkers=null)),++r})}),lineInfo:function(e){if("number"==typeof e){if(!ve(this.doc,e))return null;var t=e;if(e=Zr(this.doc,e),!e)return null}else{var t=ti(e);if(null==t)return null}return{line:t,handle:e,text:e.text,gutterMarkers:e.gutterMarkers,textClass:e.textClass,bgClass:e.bgClass,wrapClass:e.wrapClass,widgets:e.widgets}},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(e,t,n,r,i){var o=this.display;e=dt(this,me(this.doc,e));var a=e.bottom,l=e.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),o.sizer.appendChild(t),"over"==r)a=e.top;else if("above"==r||"near"==r){var s=Math.max(o.wrapper.clientHeight,this.doc.height),c=Math.max(o.sizer.clientWidth,o.lineSpace.clientWidth);("above"==r||e.bottom+t.offsetHeight>s)&&e.top>t.offsetHeight?a=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=s&&(a=e.bottom),l+t.offsetWidth>c&&(l=c-t.offsetWidth)}t.style.top=a+"px",t.style.left=t.style.right="","right"==i?(l=o.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?l=0:"middle"==i&&(l=(o.sizer.clientWidth-t.offsetWidth)/2),t.style.left=l+"px"),n&&Dn(this,l,a,l+t.offsetWidth,a+t.offsetHeight)},triggerOnKeyDown:Ot(hn),triggerOnKeyPress:Ot(mn),triggerOnKeyUp:pn,execCommand:function(e){return ua.hasOwnProperty(e)?ua[e].call(null,this):void 0},triggerElectric:Ot(function(e){Q(this,e)}),findPosH:function(e,t,n,r){var i=1;0>t&&(i=-1,t=-t);for(var o=0,a=me(this.doc,e);t>o&&(a=Un(this.doc,a,i,n,r),!a.hitSide);++o);return a},moveH:Ot(function(e,t){var n=this;n.extendSelectionsBy(function(r){return n.display.shift||n.doc.extend||r.empty()?Un(n.doc,r.head,e,t,n.options.rtlMoveVisually):0>e?r.from():r.to()},_a)}),deleteH:Ot(function(e,t){var n=this.doc.sel,r=this.doc;n.somethingSelected()?r.replaceSelection("",null,"+delete"):jn(this,function(n){var i=Un(r,n.head,e,t,!1);return 0>e?{from:i,to:n.head}:{from:n.head,to:i}})}),findPosV:function(e,t,n,r){var i=1,o=r;0>t&&(i=-1,t=-t);for(var a=0,l=me(this.doc,e);t>a;++a){var s=dt(this,l,"div");if(null==o?o=s.left:s.left=o,l=qn(this,s,i,n),l.hitSide)break}return l},moveV:Ot(function(e,t){var n=this,r=this.doc,i=[],o=!n.display.shift&&!r.extend&&r.sel.somethingSelected();if(r.extendSelectionsBy(function(a){if(o)return 0>e?a.from():a.to();var l=dt(n,a.head,"div");null!=a.goalColumn&&(l.left=a.goalColumn),i.push(l.left);var s=qn(n,l,e,t);return"page"==t&&a==r.sel.primary()&&Wn(n,null,ht(n,s,"div").top-l.top),s},_a),i.length)for(var a=0;a0&&l(n.charAt(r-1));)--r;for(;i.5)&&a(this),Pa(this,"refresh",this)}),swapDoc:Ot(function(e){var t=this.doc;return t.cm=null,Xr(this,e),lt(this),this.display.input.reset(),this.scrollTo(e.scrollLeft,e.scrollTop),this.curOp.forceScroll=!0,Ci(this,"swapDoc",this,t),t}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ai(e);var ea=e.defaults={},ta=e.optionHandlers={},na=e.Init={toString:function(){return"CodeMirror.Init"}};Gn("value","",function(e,t){e.setValue(t)},!0),Gn("mode",null,function(e,t){e.doc.modeOption=t,n(e)},!0),Gn("indentUnit",2,n,!0),Gn("indentWithTabs",!1),Gn("smartIndent",!0),Gn("tabSize",4,function(e){r(e),lt(e),Dt(e)},!0),Gn("lineSeparator",null,function(e,t){if(e.doc.lineSep=t,t){var n=[],r=e.doc.first;e.doc.iter(function(e){for(var i=0;;){var o=e.text.indexOf(t,i);if(-1==o)break;i=o+t.length,n.push(Bo(r,o))}r++});for(var i=n.length-1;i>=0;i--)In(e.doc,t,n[i],Bo(n[i].line,n[i].ch+t.length))}}),Gn("specialChars",/[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g,function(t,n,r){t.state.specialChars=new RegExp(n.source+(n.test(" ")?"":"| "),"g"),r!=e.Init&&t.refresh()}),Gn("specialCharPlaceholder",_r,function(e){e.refresh()},!0),Gn("electricChars",!0),Gn("inputStyle",Ao?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),Gn("rtlMoveVisually",!Io),Gn("wholeLineUpdateBefore",!0),Gn("theme","default",function(e){l(e),s(e)},!0),Gn("keyMap","default",function(t,n,r){var i=$n(n),o=r!=e.Init&&$n(r);o&&o.detach&&o.detach(t,i),i.attach&&i.attach(t,o||null)}),Gn("extraKeys",null),Gn("lineWrapping",!1,i,!0),Gn("gutters",[],function(e){d(e.options),s(e)},!0),Gn("fixedGutter",!0,function(e,t){e.display.gutters.style.left=t?C(e.display)+"px":"0",e.refresh()},!0),Gn("coverGutterNextToScrollbar",!1,function(e){y(e)},!0),Gn("scrollbarStyle","native",function(e){v(e),y(e),e.display.scrollbars.setScrollTop(e.doc.scrollTop),e.display.scrollbars.setScrollLeft(e.doc.scrollLeft)},!0),Gn("lineNumbers",!1,function(e){d(e.options),s(e)},!0),Gn("firstLineNumber",1,s,!0),Gn("lineNumberFormatter",function(e){return e},s,!0),Gn("showCursorWhenSelecting",!1,Re,!0),Gn("resetSelectionOnContextMenu",!0),Gn("lineWiseCopyCut",!0),Gn("readOnly",!1,function(e,t){"nocursor"==t?(yn(e),e.display.input.blur(),e.display.disabled=!0):e.display.disabled=!1,e.display.input.readOnlyChanged(t)}),Gn("disableInput",!1,function(e,t){t||e.display.input.reset()},!0),Gn("dragDrop",!0,Ut),Gn("allowDropFileTypes",null),Gn("cursorBlinkRate",530),Gn("cursorScrollMargin",0),Gn("cursorHeight",1,Re,!0),Gn("singleCursorHeightPerLine",!0,Re,!0),Gn("workTime",100),Gn("workDelay",100),Gn("flattenSpans",!0,r,!0),Gn("addModeClass",!1,r,!0),Gn("pollInterval",100),Gn("undoDepth",200,function(e,t){e.doc.history.undoDepth=t}),Gn("historyEventDelay",1250),Gn("viewportMargin",10,function(e){e.refresh()},!0),Gn("maxHighlightLength",1e4,r,!0),Gn("moveInputWithCursor",!0,function(e,t){t||e.display.input.resetPosition()}),Gn("tabindex",null,function(e,t){e.display.input.getField().tabIndex=t||""}),Gn("autofocus",null);var ra=e.modes={},ia=e.mimeModes={};e.defineMode=function(t,n){e.defaults.mode||"null"==t||(e.defaults.mode=t),arguments.length>2&&(n.dependencies=Array.prototype.slice.call(arguments,2)),ra[t]=n},e.defineMIME=function(e,t){ia[e]=t},e.resolveMode=function(t){if("string"==typeof t&&ia.hasOwnProperty(t))t=ia[t];else if(t&&"string"==typeof t.name&&ia.hasOwnProperty(t.name)){var n=ia[t.name];"string"==typeof n&&(n={name:n}),t=Hi(n,t),t.name=n.name}else if("string"==typeof t&&/^[\w\-]+\/[\w\-]+\+xml$/.test(t))return e.resolveMode("application/xml");return"string"==typeof t?{name:t}:t||{name:"null"}},e.getMode=function(t,n){var n=e.resolveMode(n),r=ra[n.name];if(!r)return e.getMode(t,"text/plain");var i=r(t,n);if(oa.hasOwnProperty(n.name)){var o=oa[n.name];for(var a in o)o.hasOwnProperty(a)&&(i.hasOwnProperty(a)&&(i["_"+a]=i[a]),i[a]=o[a])}if(i.name=n.name,n.helperType&&(i.helperType=n.helperType),n.modeProps)for(var a in n.modeProps)i[a]=n.modeProps[a];return i},e.defineMode("null",function(){return{token:function(e){e.skipToEnd()}}}),e.defineMIME("text/plain","null");var oa=e.modeExtensions={};e.extendMode=function(e,t){var n=oa.hasOwnProperty(e)?oa[e]:oa[e]={};Wi(t,n)},e.defineExtension=function(t,n){e.prototype[t]=n},e.defineDocExtension=function(e,t){Ca.prototype[e]=t},e.defineOption=Gn;var aa=[];e.defineInitHook=function(e){aa.push(e)};var la=e.helpers={};e.registerHelper=function(t,n,r){la.hasOwnProperty(t)||(la[t]=e[t]={_global:[]}),la[t][n]=r},e.registerGlobalHelper=function(t,n,r,i){e.registerHelper(t,n,i),la[t]._global.push({pred:r,val:i})};var sa=e.copyState=function(e,t){if(t===!0)return t;if(e.copyState)return e.copyState(t);var n={};for(var r in t){var i=t[r];i instanceof Array&&(i=i.concat([])),n[r]=i}return n},ca=e.startState=function(e,t,n){return e.startState?e.startState(t,n):!0};e.innerMode=function(e,t){for(;e.innerMode;){var n=e.innerMode(t);if(!n||n.mode==e)break;t=n.state,e=n.mode}return n||{mode:e,state:t}};var ua=e.commands={selectAll:function(e){e.setSelection(Bo(e.firstLine(),0),Bo(e.lastLine()),Wa)},singleSelection:function(e){e.setSelection(e.getCursor("anchor"),e.getCursor("head"),Wa)},killLine:function(e){jn(e,function(t){if(t.empty()){var n=Zr(e.doc,t.head.line).text.length;return t.head.ch==n&&t.head.line0)i=new Bo(i.line,i.ch+1),e.replaceRange(o.charAt(i.ch-1)+o.charAt(i.ch-2),Bo(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var a=Zr(e.doc,i.line-1).text;a&&e.replaceRange(o.charAt(0)+e.doc.lineSeparator()+a.charAt(a.length-1),Bo(i.line-1,a.length-1),Bo(i.line,1),"+transpose")}n.push(new fe(i,i))}e.setSelections(n)})},newlineAndIndent:function(e){At(e,function(){for(var t=e.listSelections().length,n=0;t>n;n++){var r=e.listSelections()[n];e.replaceRange(e.doc.lineSeparator(),r.anchor,r.head,"+input"),e.indentLine(r.from().line+1,null,!0)}Bn(e)})},openLine:function(e){e.replaceSelection("\n","start")},toggleOverwrite:function(e){e.toggleOverwrite()}},fa=e.keyMap={};fa.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},fa.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},fa.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},fa.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},fa["default"]=Eo?fa.macDefault:fa.pcDefault,e.normalizeKeyMap=function(e){var t={};for(var n in e)if(e.hasOwnProperty(n)){var r=e[n];if(/^(name|fallthrough|(de|at)tach)$/.test(n))continue;if("..."==r){delete e[n];continue}for(var i=Ri(n.split(" "),Yn),o=0;o=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.post},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);return t>-1?(this.pos=t,!0):void 0},backUp:function(e){this.pos-=e},column:function(){return this.lastColumnPos0?null:(r&&t!==!1&&(this.pos+=r[0].length),r)}var i=function(e){return n?e.toLowerCase():e},o=this.string.substr(this.pos,e.length);return i(o)==i(e)?(t!==!1&&(this.pos+=e.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}}};var ga=0,va=e.TextMarker=function(e,t){this.lines=[],this.type=t,this.doc=e,this.id=++ga};Ai(va),va.prototype.clear=function(){if(!this.explicitlyCleared){var e=this.doc.cm,t=e&&!e.curOp;if(t&&bt(e),Ni(this,"clear")){var n=this.find();n&&Ci(this,"clear",n.from,n.to)}for(var r=null,i=null,o=0;oe.display.maxLineLength&&(e.display.maxLine=s,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=r&&e&&this.collapsed&&Dt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&Ae(e.doc)),e&&Ci(e,"markerCleared",e,this),t&&kt(e),this.parent&&this.parent.clear()}},va.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var n,r,i=0;in;++n){var i=this.lines[n];this.height-=i.height,Nr(i),Ci(i,"delete")}this.lines.splice(e,t)},collapse:function(e){e.push.apply(e,this.lines)},insertInner:function(e,t,n){this.height+=n,this.lines=this.lines.slice(0,e).concat(t).concat(this.lines.slice(e));for(var r=0;re;++e)if(n(this.lines[e]))return!0}},Vr.prototype={chunkSize:function(){return this.size},removeInner:function(e,t){this.size-=t;for(var n=0;ne){var o=Math.min(t,i-e),a=r.height;if(r.removeInner(e,o),this.height-=a-r.height,i==o&&(this.children.splice(n--,1),r.parent=null),0==(t-=o))break;e=0}else e-=i}if(this.size-t<25&&(this.children.length>1||!(this.children[0]instanceof $r))){var l=[];this.collapse(l),this.children=[new $r(l)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t=e){if(i.insertInner(e,t,n),i.lines&&i.lines.length>50){for(var a=i.lines.length%25+25,l=a;l10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;re){var a=Math.min(t,o-e);if(i.iterN(e,a,n))return!0;if(0==(t-=a))break;e=0}else e-=o}}};var Sa=0,Ca=e.Doc=function(e,t,n,r){if(!(this instanceof Ca))return new Ca(e,t,n,r);null==n&&(n=0),Vr.call(this,[new $r([new ba("",null)])]),this.first=n,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.cleanGeneration=1,this.frontier=n;var i=Bo(n,0);this.sel=de(i),this.history=new oi(null),this.id=++Sa,this.modeOption=t,this.lineSep=r,this.extend=!1,"string"==typeof e&&(e=this.splitLines(e)),Yr(this,{from:i,to:i,text:e}),Te(this,de(i),Wa)};Ca.prototype=Hi(Vr.prototype,{constructor:Ca,iter:function(e,t,n){n?this.iterN(e-this.first,t-e,n):this.iterN(this.first,this.first+this.size,e)},insert:function(e,t){for(var n=0,r=0;r=0;o--)Tn(this,r[o]);l?Le(this,l):this.cm&&Bn(this.cm)}),undo:It(function(){Nn(this,"undo")}),redo:It(function(){Nn(this,"redo")}),undoSelection:It(function(){Nn(this,"undo",!0)}),redoSelection:It(function(){Nn(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,n=0,r=0;r=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,n){e=me(this,e),t=me(this,t);var r=[],i=e.line;return this.iter(e.line,t.line+1,function(o){var a=o.markedSpans;if(a)for(var l=0;l=s.to||null==s.from&&i!=e.line||null!=s.from&&i==t.line&&s.from>=t.ch||n&&!n(s.marker)||r.push(s.marker.parent||s.marker)}++i}),r},getAllMarks:function(){var e=[];return this.iter(function(t){var n=t.markedSpans;if(n)for(var r=0;re?(t=e,!0):(e-=o,void++n)}),me(this,Bo(n,t))},indexFromPos:function(e){e=me(this,e);var t=e.ch;if(e.linet&&(t=e.from),null!=e.to&&e.tol||l>=t)return a+(t-o);a+=l-o,a+=n-a%n,o=l+1}},za=e.findColumn=function(e,t,n){for(var r=0,i=0;;){var o=e.indexOf(" ",r);-1==o&&(o=e.length);var a=o-r;if(o==e.length||i+a>=t)return r+Math.min(a,t-i);if(i+=o-r,i+=n-i%n,r=o+1,i>=t)return r}},ja=[""],Ua=function(e){e.select()};No?Ua=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:xo&&(Ua=function(e){try{e.select()}catch(t){}});var qa,Ga=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Ya=e.isWordChar=function(e){return/\w/.test(e)||e>"€"&&(e.toUpperCase()!=e.toLowerCase()||Ga.test(e))},$a=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;qa=document.createRange?function(e,t,n,r){var i=document.createRange();return i.setEnd(r||e,n),i.setStart(e,t),i}:function(e,t,n){var r=document.body.createTextRange();try{r.moveToElementText(e.parentNode)}catch(i){return r}return r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r};var Va=e.contains=function(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do if(11==t.nodeType&&(t=t.host),t==e)return!0;while(t=t.parentNode)};xo&&11>bo&&(Gi=function(){try{return document.activeElement}catch(e){return document.body}});var Ka,Xa,Za=e.rmClass=function(e,t){var n=e.className,r=Yi(t).exec(n);if(r){var i=n.slice(r.index+r[0].length);e.className=n.slice(0,r.index)+(i?r[1]+i:"")}},Ja=e.addClass=function(e,t){var n=e.className;Yi(t).test(n)||(e.className+=(n?" ":"")+t)},Qa=!1,el=function(){if(xo&&9>bo)return!1;var e=ji("div");return"draggable"in e||"dragDrop"in e}(),tl=e.splitLines=3!="\n\nb".split(/\n/).length?function(e){for(var t=0,n=[],r=e.length;r>=t;){var i=e.indexOf("\n",t);-1==i&&(i=e.length);var o=e.slice(t,"\r"==e.charAt(i-1)?i-1:i),a=o.indexOf("\r");-1!=a?(n.push(o.slice(0,a)),t+=a+1):(n.push(o),t=i+1)}return n}:function(e){return e.split(/\r\n?|\n/)},nl=window.getSelection?function(e){try{return e.selectionStart!=e.selectionEnd}catch(t){return!1}}:function(e){try{var t=e.ownerDocument.selection.createRange()}catch(n){}return t&&t.parentElement()==e?0!=t.compareEndPoints("StartToEnd",t):!1},rl=function(){var e=ji("div");return"oncopy"in e?!0:(e.setAttribute("oncopy","return;"),"function"==typeof e.oncopy)}(),il=null,ol=e.keyNames={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"=",91:"Mod",92:"Mod",93:"Mod",106:"*",107:"=",109:"-",110:".",111:"/",127:"Delete",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};!function(){for(var e=0;10>e;e++)ol[e+48]=ol[e+96]=String(e);for(var e=65;90>=e;e++)ol[e]=String.fromCharCode(e);for(var e=1;12>=e;e++)ol[e+111]=ol[e+63235]="F"+e}();var al,ll=function(){function e(e){return 247>=e?n.charAt(e):e>=1424&&1524>=e?"R":e>=1536&&1773>=e?r.charAt(e-1536):e>=1774&&2220>=e?"r":e>=8192&&8203>=e?"w":8204==e?"b":"L"}function t(e,t,n){this.level=e,this.from=t,this.to=n}var n="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",r="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm",i=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,o=/[stwN]/,a=/[LRr]/,l=/[Lb1n]/,s=/[1n]/,c="L";return function(n){if(!i.test(n))return!1;for(var r,u=n.length,f=[],h=0;u>h;++h)f.push(r=e(n.charCodeAt(h)));for(var h=0,d=c;u>h;++h){var r=f[h];"m"==r?f[h]=d:d=r}for(var h=0,p=c;u>h;++h){var r=f[h];"1"==r&&"r"==p?f[h]="n":a.test(r)&&(p=r,"r"==r&&(f[h]="R"))}for(var h=1,d=f[0];u-1>h;++h){var r=f[h];"+"==r&&"1"==d&&"1"==f[h+1]?f[h]="1":","!=r||d!=f[h+1]||"1"!=d&&"n"!=d||(f[h]=d),d=r}for(var h=0;u>h;++h){var r=f[h];if(","==r)f[h]="N";else if("%"==r){for(var m=h+1;u>m&&"%"==f[m];++m);for(var g=h&&"!"==f[h-1]||u>m&&"1"==f[m]?"1":"N",v=h;m>v;++v)f[v]=g;h=m-1}}for(var h=0,p=c;u>h;++h){var r=f[h];"L"==p&&"1"==r?f[h]="L":a.test(r)&&(p=r)}for(var h=0;u>h;++h)if(o.test(f[h])){for(var m=h+1;u>m&&o.test(f[m]);++m);for(var y="L"==(h?f[h-1]:c),x="L"==(u>m?f[m]:c),g=y||x?"L":"R",v=h;m>v;++v)f[v]=g;h=m-1}for(var b,w=[],h=0;u>h;)if(l.test(f[h])){var k=h;for(++h;u>h&&l.test(f[h]);++h);w.push(new t(0,k,h))}else{var S=h,C=w.length;for(++h;u>h&&"L"!=f[h];++h);for(var v=S;h>v;)if(s.test(f[v])){v>S&&w.splice(C,0,new t(1,S,v));var L=v;for(++v;h>v&&s.test(f[v]);++v);w.splice(C,0,new t(2,L,v)),S=v}else++v;h>S&&w.splice(C,0,new t(1,S,h))}return 1==w[0].level&&(b=n.match(/^\s+/))&&(w[0].from=b[0].length,w.unshift(new t(0,0,b[0].length))),1==Ii(w).level&&(b=n.match(/\s+$/))&&(Ii(w).to-=b[0].length,w.push(new t(0,u-b[0].length,u))),2==w[0].level&&w.unshift(new t(1,w[0].to,w[0].to)),w[0].level!=Ii(w).level&&w.push(new t(w[0].level,u,u)),w}}();return e.version="5.15.2",e})},{}],11:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../markdown/markdown"),t("../../addon/mode/overlay")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],i):i(CodeMirror)}(function(e){"use strict";var t=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i;e.defineMode("gfm",function(n,r){function i(e){return e.code=!1,null}var o=0,a={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(e){return{code:e.code,codeBlock:e.codeBlock,ateSpace:e.ateSpace}},token:function(e,n){if(n.combineTokens=null,n.codeBlock)return e.match(/^```+/)?(n.codeBlock=!1,null):(e.skipToEnd(),null);if(e.sol()&&(n.code=!1),e.sol()&&e.match(/^```+/))return e.skipToEnd(),n.codeBlock=!0,null;if("`"===e.peek()){e.next();var i=e.pos;e.eatWhile("`");var a=1+e.pos-i;return n.code?a===o&&(n.code=!1):(o=a,n.code=!0),null}if(n.code)return e.next(),null;if(e.eatSpace())return n.ateSpace=!0,null;if((e.sol()||n.ateSpace)&&(n.ateSpace=!1,r.gitHubSpice!==!1)){if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/))return n.combineTokens=!0,"link";if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return n.combineTokens=!0,"link"}return e.match(t)&&"]("!=e.string.slice(e.start-2,e.start)&&(0==e.start||/\W/.test(e.string.charAt(e.start-1)))?(n.combineTokens=!0,"link"):(e.next(),null)},blankLine:i},l={underscoresBreakWords:!1,taskLists:!0,fencedCodeBlocks:"```",strikethrough:!0};for(var s in r)l[s]=r[s];return l.name="markdown",e.overlayMode(e.getMode(n,l),a)},"markdown"),e.defineMIME("text/x-gfm","gfm")})},{"../../addon/mode/overlay":8,"../../lib/codemirror":10,"../markdown/markdown":12}],12:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../xml/xml"),t("../meta")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../xml/xml","../meta"],i):i(CodeMirror)}(function(e){"use strict";e.defineMode("markdown",function(t,n){function r(n){if(e.findModeByName){var r=e.findModeByName(n);r&&(n=r.mime||r.mimes[0])}var i=e.getMode(t,n);return"null"==i.name?null:i}function i(e,t,n){return t.f=t.inline=n,n(e,t)}function o(e,t,n){return t.f=t.block=n,n(e,t)}function a(e){return!e||!/\S/.test(e.string)}function l(e){return e.linkTitle=!1,e.em=!1,e.strong=!1,e.strikethrough=!1,e.quote=0,e.indentedCode=!1,k&&e.f==c&&(e.f=p,e.block=s),e.trailingSpace=0,e.trailingSpaceNewLine=!1,e.prevLine=e.thisLine,e.thisLine=null,null}function s(t,o){var l=t.sol(),s=o.list!==!1,c=o.indentedCode;o.indentedCode=!1,s&&(o.indentationDiff>=0?(o.indentationDiff<4&&(o.indentation-=o.indentationDiff),o.list=null):o.indentation>0?o.list=null:o.list=!1);var f=null;if(o.indentationDiff>=4)return t.skipToEnd(),c||a(o.prevLine)?(o.indentation-=4,o.indentedCode=!0,S.code):null;if(t.eatSpace())return null;if((f=t.match(A))&&f[1].length<=6)return o.header=f[1].length,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(!(a(o.prevLine)||o.quote||s||c)&&(f=t.match(E)))return o.header="="==f[0].charAt(0)?1:2,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(t.eat(">"))return o.quote=l?1:o.quote+1,n.highlightFormatting&&(o.formatting="quote"),t.eatSpace(),h(o);if("["===t.peek())return i(t,o,y);if(t.match(L,!0))return o.hr=!0,S.hr;if((a(o.prevLine)||s)&&(t.match(T,!1)||t.match(M,!1))){var d=null;for(t.match(T,!0)?d="ul":(t.match(M,!0),d="ol"),o.indentation=t.column()+t.current().length,o.list=!0;o.listStack&&t.column()")>-1)&&(n.f=p,n.block=s,n.htmlState=null)}return r}function u(e,t){return t.fencedChars&&e.match(t.fencedChars,!1)?(t.localMode=t.localState=null,t.f=t.block=f,null):t.localMode?t.localMode.token(e,t.localState):(e.skipToEnd(),S.code)}function f(e,t){e.match(t.fencedChars),t.block=s,t.f=p,t.fencedChars=null,n.highlightFormatting&&(t.formatting="code-block"),t.code=1;var r=h(t);return t.code=0,r}function h(e){var t=[];if(e.formatting){t.push(S.formatting),"string"==typeof e.formatting&&(e.formatting=[e.formatting]);for(var r=0;r=e.quote?t.push(S.formatting+"-"+e.formatting[r]+"-"+e.quote):t.push("error"))}if(e.taskOpen)return t.push("meta"),t.length?t.join(" "):null;if(e.taskClosed)return t.push("property"),t.length?t.join(" "):null;if(e.linkHref?t.push(S.linkHref,"url"):(e.strong&&t.push(S.strong),e.em&&t.push(S.em),e.strikethrough&&t.push(S.strikethrough),e.linkText&&t.push(S.linkText),e.code&&t.push(S.code)),e.header&&t.push(S.header,S.header+"-"+e.header),e.quote&&(t.push(S.quote),!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.quote+"-"+e.quote):t.push(S.quote+"-"+n.maxBlockquoteDepth)),e.list!==!1){var i=(e.listStack.length-1)%3;i?1===i?t.push(S.list2):t.push(S.list3):t.push(S.list1)}return e.trailingSpaceNewLine?t.push("trailing-space-new-line"):e.trailingSpace&&t.push("trailing-space-"+(e.trailingSpace%2?"a":"b")),t.length?t.join(" "):null}function d(e,t){return e.match(O,!0)?h(t):void 0}function p(t,r){var i=r.text(t,r);if("undefined"!=typeof i)return i;if(r.list)return r.list=null,h(r);if(r.taskList){var a="x"!==t.match(N,!0)[1];return a?r.taskOpen=!0:r.taskClosed=!0,n.highlightFormatting&&(r.formatting="task"),r.taskList=!1,h(r)}if(r.taskOpen=!1,r.taskClosed=!1,r.header&&t.match(/^#+$/,!0))return n.highlightFormatting&&(r.formatting="header"), -h(r);var l=t.sol(),s=t.next();if(r.linkTitle){r.linkTitle=!1;var u=s;"("===s&&(u=")"),u=(u+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");var f="^\\s*(?:[^"+u+"\\\\]+|\\\\\\\\|\\\\.)"+u;if(t.match(new RegExp(f),!0))return S.linkHref}if("`"===s){var d=r.formatting;n.highlightFormatting&&(r.formatting="code"),t.eatWhile("`");var p=t.current().length;if(0==r.code)return r.code=p,h(r);if(p==r.code){var v=h(r);return r.code=0,v}return r.formatting=d,h(r)}if(r.code)return h(r);if("\\"===s&&(t.next(),n.highlightFormatting)){var y=h(r),x=S.formatting+"-escape";return y?y+" "+x:x}if("!"===s&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return t.match(/\[[^\]]*\]/),r.inline=r.f=g,S.image;if("["===s&&t.match(/[^\]]*\](\(.*\)| ?\[.*?\])/,!1))return r.linkText=!0,n.highlightFormatting&&(r.formatting="link"),h(r);if("]"===s&&r.linkText&&t.match(/\(.*?\)| ?\[.*?\]/,!1)){n.highlightFormatting&&(r.formatting="link");var y=h(r);return r.linkText=!1,r.inline=r.f=g,y}if("<"===s&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkInline}if("<"===s&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkEmail}if("<"===s&&t.match(/^(!--|\w)/,!1)){var b=t.string.indexOf(">",t.pos);if(-1!=b){var k=t.string.substring(t.start,b);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(k)&&(r.md_inside=!0)}return t.backUp(1),r.htmlState=e.startState(w),o(t,r,c)}if("<"===s&&t.match(/^\/\w*?>/))return r.md_inside=!1,"tag";var C=!1;if(!n.underscoresBreakWords&&"_"===s&&"_"!==t.peek()&&t.match(/(\w)/,!1)){var L=t.pos-2;if(L>=0){var T=t.string.charAt(L);"_"!==T&&T.match(/(\w)/,!1)&&(C=!0)}}if("*"===s||"_"===s&&!C)if(l&&" "===t.peek());else{if(r.strong===s&&t.eat(s)){n.highlightFormatting&&(r.formatting="strong");var v=h(r);return r.strong=!1,v}if(!r.strong&&t.eat(s))return r.strong=s,n.highlightFormatting&&(r.formatting="strong"),h(r);if(r.em===s){n.highlightFormatting&&(r.formatting="em");var v=h(r);return r.em=!1,v}if(!r.em)return r.em=s,n.highlightFormatting&&(r.formatting="em"),h(r)}else if(" "===s&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return h(r);t.backUp(1)}if(n.strikethrough)if("~"===s&&t.eatWhile(s)){if(r.strikethrough){n.highlightFormatting&&(r.formatting="strikethrough");var v=h(r);return r.strikethrough=!1,v}if(t.match(/^[^\s]/,!1))return r.strikethrough=!0,n.highlightFormatting&&(r.formatting="strikethrough"),h(r)}else if(" "===s&&t.match(/^~~/,!0)){if(" "===t.peek())return h(r);t.backUp(2)}return" "===s&&(t.match(/ +$/,!1)?r.trailingSpace++:r.trailingSpace&&(r.trailingSpaceNewLine=!0)),h(r)}function m(e,t){var r=e.next();if(">"===r){t.f=t.inline=p,n.highlightFormatting&&(t.formatting="link");var i=h(t);return i?i+=" ":i="",i+S.linkInline}return e.match(/^[^>]+/,!0),S.linkInline}function g(e,t){if(e.eatSpace())return null;var r=e.next();return"("===r||"["===r?(t.f=t.inline=v("("===r?")":"]",0),n.highlightFormatting&&(t.formatting="link-string"),t.linkHref=!0,h(t)):"error"}function v(e){return function(t,r){var i=t.next();if(i===e){r.f=r.inline=p,n.highlightFormatting&&(r.formatting="link-string");var o=h(r);return r.linkHref=!1,o}return t.match(P[e]),r.linkHref=!0,h(r)}}function y(e,t){return e.match(/^([^\]\\]|\\.)*\]:/,!1)?(t.f=x,e.next(),n.highlightFormatting&&(t.formatting="link"),t.linkText=!0,h(t)):i(e,t,p)}function x(e,t){if(e.match(/^\]:/,!0)){t.f=t.inline=b,n.highlightFormatting&&(t.formatting="link");var r=h(t);return t.linkText=!1,r}return e.match(/^([^\]\\]|\\.)+/,!0),S.linkText}function b(e,t){return e.eatSpace()?null:(e.match(/^[^\s]+/,!0),void 0===e.peek()?t.linkTitle=!0:e.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),t.f=t.inline=p,S.linkHref+" url")}var w=e.getMode(t,"text/html"),k="null"==w.name;void 0===n.highlightFormatting&&(n.highlightFormatting=!1),void 0===n.maxBlockquoteDepth&&(n.maxBlockquoteDepth=0),void 0===n.underscoresBreakWords&&(n.underscoresBreakWords=!0),void 0===n.taskLists&&(n.taskLists=!1),void 0===n.strikethrough&&(n.strikethrough=!1),void 0===n.tokenTypeOverrides&&(n.tokenTypeOverrides={});var S={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"tag",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough"};for(var C in S)S.hasOwnProperty(C)&&n.tokenTypeOverrides[C]&&(S[C]=n.tokenTypeOverrides[C]);var L=/^([*\-_])(?:\s*\1){2,}\s*$/,T=/^[*\-+]\s+/,M=/^[0-9]+([.)])\s+/,N=/^\[(x| )\](?=\s)/,A=n.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,E=/^ *(?:\={1,}|-{1,})\s*$/,O=/^[^#!\[\]*_\\<>` "'(~]+/,I=new RegExp("^("+(n.fencedCodeBlocks===!0?"~~~+|```+":n.fencedCodeBlocks)+")[ \\t]*([\\w+#-]*)"),P={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\\]]|\\.)*\])*?(?=\])/},R={startState:function(){return{f:s,prevLine:null,thisLine:null,block:s,htmlState:null,indentation:0,inline:p,text:d,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,fencedChars:null}},copyState:function(t){return{f:t.f,prevLine:t.prevLine,thisLine:t.thisLine,block:t.block,htmlState:t.htmlState&&e.copyState(w,t.htmlState),indentation:t.indentation,localMode:t.localMode,localState:t.localMode?e.copyState(t.localMode,t.localState):null,inline:t.inline,text:t.text,formatting:!1,linkTitle:t.linkTitle,code:t.code,em:t.em,strong:t.strong,strikethrough:t.strikethrough,header:t.header,hr:t.hr,taskList:t.taskList,list:t.list,listStack:t.listStack.slice(0),quote:t.quote,indentedCode:t.indentedCode,trailingSpace:t.trailingSpace,trailingSpaceNewLine:t.trailingSpaceNewLine,md_inside:t.md_inside,fencedChars:t.fencedChars}},token:function(e,t){if(t.formatting=!1,e!=t.thisLine){var n=t.header||t.hr;if(t.header=0,t.hr=!1,e.match(/^\s*$/,!0)||n){if(l(t),!n)return null;t.prevLine=null}t.prevLine=t.thisLine,t.thisLine=e,t.taskList=!1,t.trailingSpace=0,t.trailingSpaceNewLine=!1,t.f=t.block;var r=e.match(/^\s*/,!0)[0].replace(/\t/g," ").length;if(t.indentationDiff=Math.min(r-t.indentation,4),t.indentation=t.indentation+t.indentationDiff,r>0)return null}return t.f(e,t)},innerMode:function(e){return e.block==c?{state:e.htmlState,mode:w}:e.localState?{state:e.localState,mode:e.localMode}:{state:e,mode:R}},blankLine:l,getType:h,fold:"markdown"};return R},"xml"),e.defineMIME("text/x-markdown","markdown")})},{"../../lib/codemirror":10,"../meta":13,"../xml/xml":14}],13:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../lib/codemirror")):"function"==typeof e&&e.amd?e(["../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["pgp"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists.txt$/},{name:"CoffeeScript",mime:"text/x-coffeescript",mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded Javascript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history).md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"]},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Jade",mime:"text/x-jade",mode:"jade",ext:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"null",mode:"jinja2"},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"mbox",mime:"application/mbox",mode:"mbox",ext:["mbox"]},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mime:"text/n-triples",mode:"ntriples",ext:["nt"]},{name:"Objective C",mime:"text/x-objectivec",mode:"clike",ext:["m","mm"],alias:["objective-c","objc"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mime:"application/x-httpd-php",mode:"php",ext:["php","php3","php4","php5","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"PowerShell",mime:"application/x-powershell",mode:"powershell",ext:["ps1","psd1","psm1"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["BUILD","bzl","py","pyw"],file:/^(BUCK|BUILD)$/},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"SAS",mime:"text/x-sas",mode:"sas",ext:["sas"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mime:"text/x-sh",mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki ",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"Web IDL",mime:"text/x-webidl",mode:"webidl",ext:["webidl"]},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"Yacas",mime:"text/x-yacas",mode:"yacas",ext:["ys"]},{name:"YAML",mime:"text/x-yaml",mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]}];for(var t=0;t-1&&t.substring(i+1,t.length);return o?e.findModeByExtension(o):void 0},e.findModeByName=function(t){t=t.toLowerCase();for(var n=0;n")):null:e.match("--")?n(s("comment","-->")):e.match("DOCTYPE",!0,!0)?(e.eatWhile(/[\w\._\-]/),n(c(1))):null:e.eat("?")?(e.eatWhile(/[\w\._\-]/),t.tokenize=s("meta","?>"),"meta"):(T=e.eat("/")?"closeTag":"openTag",t.tokenize=a,"tag bracket");if("&"==r){var i;return i=e.eat("#")?e.eat("x")?e.eatWhile(/[a-fA-F\d]/)&&e.eat(";"):e.eatWhile(/[\d]/)&&e.eat(";"):e.eatWhile(/[\w\.\-:]/)&&e.eat(";"),i?"atom":"error"}return e.eatWhile(/[^&<]/),null}function a(e,t){var n=e.next();if(">"==n||"/"==n&&e.eat(">"))return t.tokenize=o,T=">"==n?"endTag":"selfcloseTag","tag bracket";if("="==n)return T="equals",null;if("<"==n){t.tokenize=o,t.state=d,t.tagName=t.tagStart=null;var r=t.tokenize(e,t);return r?r+" tag error":"tag error"}return/[\'\"]/.test(n)?(t.tokenize=l(n),t.stringStartCol=e.column(),t.tokenize(e,t)):(e.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function l(e){var t=function(t,n){for(;!t.eol();)if(t.next()==e){n.tokenize=a;break}return"string"};return t.isInAttribute=!0,t}function s(e,t){return function(n,r){for(;!n.eol();){if(n.match(t)){r.tokenize=o;break}n.next()}return e}}function c(e){return function(t,n){for(var r;null!=(r=t.next());){if("<"==r)return n.tokenize=c(e+1),n.tokenize(t,n);if(">"==r){if(1==e){n.tokenize=o;break}return n.tokenize=c(e-1),n.tokenize(t,n)}}return"meta"}}function u(e,t,n){this.prev=e.context,this.tagName=t,this.indent=e.indented,this.startOfLine=n,(S.doNotIndent.hasOwnProperty(t)||e.context&&e.context.noIndent)&&(this.noIndent=!0)}function f(e){e.context&&(e.context=e.context.prev)}function h(e,t){for(var n;;){if(!e.context)return;if(n=e.context.tagName,!S.contextGrabbers.hasOwnProperty(n)||!S.contextGrabbers[n].hasOwnProperty(t))return;f(e)}}function d(e,t,n){return"openTag"==e?(n.tagStart=t.column(),p):"closeTag"==e?m:d}function p(e,t,n){return"word"==e?(n.tagName=t.current(),M="tag",y):(M="error",p)}function m(e,t,n){if("word"==e){var r=t.current();return n.context&&n.context.tagName!=r&&S.implicitlyClosed.hasOwnProperty(n.context.tagName)&&f(n),n.context&&n.context.tagName==r||S.matchClosing===!1?(M="tag",g):(M="tag error",v)}return M="error",v}function g(e,t,n){return"endTag"!=e?(M="error",g):(f(n),d)}function v(e,t,n){return M="error",g(e,t,n)}function y(e,t,n){if("word"==e)return M="attribute",x;if("endTag"==e||"selfcloseTag"==e){var r=n.tagName,i=n.tagStart;return n.tagName=n.tagStart=null,"selfcloseTag"==e||S.autoSelfClosers.hasOwnProperty(r)?h(n,r):(h(n,r),n.context=new u(n,r,i==n.indented)),d}return M="error",y}function x(e,t,n){return"equals"==e?b:(S.allowMissing||(M="error"),y(e,t,n))}function b(e,t,n){return"string"==e?w:"word"==e&&S.allowUnquoted?(M="string",y):(M="error",y(e,t,n))}function w(e,t,n){return"string"==e?w:y(e,t,n)}var k=r.indentUnit,S={},C=i.htmlMode?t:n;for(var L in C)S[L]=C[L];for(var L in i)S[L]=i[L];var T,M;return o.isInText=!0,{startState:function(e){var t={tokenize:o,state:d,indented:e||0,tagName:null,tagStart:null,context:null};return null!=e&&(t.baseIndent=e),t},token:function(e,t){if(!t.tagName&&e.sol()&&(t.indented=e.indentation()),e.eatSpace())return null;T=null;var n=t.tokenize(e,t);return(n||T)&&"comment"!=n&&(M=null,t.state=t.state(T||n,e,t),M&&(n="error"==M?n+" error":M)),n},indent:function(t,n,r){var i=t.context;if(t.tokenize.isInAttribute)return t.tagStart==t.indented?t.stringStartCol+1:t.indented+k;if(i&&i.noIndent)return e.Pass;if(t.tokenize!=a&&t.tokenize!=o)return r?r.match(/^(\s*)/)[0].length:0;if(t.tagName)return S.multilineTagIndentPastTag!==!1?t.tagStart+t.tagName.length+2:t.tagStart+k*(S.multilineTagIndentFactor||1);if(S.alignCDATA&&/$/,blockCommentStart:"",configuration:S.htmlMode?"html":"xml",helperType:S.htmlMode?"html":"xml",skipAttribute:function(e){e.state==b&&(e.state=y)}}}),e.defineMIME("text/xml","xml"),e.defineMIME("application/xml","xml"),e.mimeModes.hasOwnProperty("text/html")||e.defineMIME("text/html",{name:"xml",htmlMode:!0})})},{"../../lib/codemirror":10}],15:[function(e,t,n){n.read=function(e,t,n,r,i){var o,a,l=8*i-r-1,s=(1<>1,u=-7,f=n?i-1:0,h=n?-1:1,d=e[t+f];for(f+=h,o=d&(1<<-u)-1,d>>=-u,u+=l;u>0;o=256*o+e[t+f],f+=h,u-=8);for(a=o&(1<<-u)-1,o>>=-u,u+=r;u>0;a=256*a+e[t+f],f+=h,u-=8);if(0===o)o=1-c;else{if(o===s)return a?NaN:(d?-1:1)*(1/0);a+=Math.pow(2,r),o-=c}return(d?-1:1)*a*Math.pow(2,o-r)},n.write=function(e,t,n,r,i,o){var a,l,s,c=8*o-i-1,u=(1<>1,h=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,d=r?0:o-1,p=r?1:-1,m=0>t||0===t&&0>1/t?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(l=isNaN(t)?1:0,a=u):(a=Math.floor(Math.log(t)/Math.LN2),t*(s=Math.pow(2,-a))<1&&(a--,s*=2),t+=a+f>=1?h/s:h*Math.pow(2,1-f),t*s>=2&&(a++,s/=2),a+f>=u?(l=0,a=u):a+f>=1?(l=(t*s-1)*Math.pow(2,i),a+=f):(l=t*Math.pow(2,f-1)*Math.pow(2,i),a=0));i>=8;e[n+d]=255&l,d+=p,l/=256,i-=8);for(a=a<0;e[n+d]=255&a,d+=p,a/=256,c-=8);e[n+d-p]|=128*m}},{}],16:[function(e,t,n){var r={}.toString;t.exports=Array.isArray||function(e){return"[object Array]"==r.call(e)}},{}],17:[function(t,n,r){(function(t){(function(){function t(e){this.tokens=[],this.tokens.links={},this.options=e||h.defaults,this.rules=d.normal,this.options.gfm&&(this.options.tables?this.rules=d.tables:this.rules=d.gfm)}function i(e,t){if(this.options=t||h.defaults,this.links=e,this.rules=p.normal,this.renderer=this.options.renderer||new o,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=p.breaks:this.rules=p.gfm:this.options.pedantic&&(this.rules=p.pedantic)}function o(e){this.options=e||{}}function a(e){this.tokens=[],this.token=null,this.options=e||h.defaults,this.options.renderer=this.options.renderer||new o,this.renderer=this.options.renderer,this.renderer.options=this.options}function l(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function s(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function c(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function f(e){for(var t,n,r=1;rAn error occured:

    "+l(u.message+"",!0)+"
    ";throw u}}var d={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:u,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:u,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:u,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};d.bullet=/(?:[*+-]|\d+\.)/,d.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,d.item=c(d.item,"gm")(/bull/g,d.bullet)(),d.list=c(d.list)(/bull/g,d.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+d.def.source+")")(),d.blockquote=c(d.blockquote)("def",d.def)(),d._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",d.html=c(d.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,d._tag)(),d.paragraph=c(d.paragraph)("hr",d.hr)("heading",d.heading)("lheading",d.lheading)("blockquote",d.blockquote)("tag","<"+d._tag)("def",d.def)(),d.normal=f({},d),d.gfm=f({},d.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),d.gfm.paragraph=c(d.paragraph)("(?!","(?!"+d.gfm.fences.source.replace("\\1","\\2")+"|"+d.list.source.replace("\\1","\\3")+"|")(),d.tables=f({},d.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=d,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t,n){for(var r,i,o,a,l,s,c,u,f,e=e.replace(/^ +$/gm,"");e;)if((o=this.rules.newline.exec(e))&&(e=e.substring(o[0].length),o[0].length>1&&this.tokens.push({type:"space"})),o=this.rules.code.exec(e))e=e.substring(o[0].length),o=o[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?o:o.replace(/\n+$/,"")});else if(o=this.rules.fences.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"code",lang:o[2],text:o[3]||""});else if(o=this.rules.heading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:o[1].length,text:o[2]});else if(t&&(o=this.rules.nptable.exec(e))){for(e=e.substring(o[0].length),s={type:"table",header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/\n$/,"").split("\n")},u=0;u ?/gm,""),this.token(o,t,!0),this.tokens.push({type:"blockquote_end"});else if(o=this.rules.list.exec(e)){for(e=e.substring(o[0].length),a=o[2],this.tokens.push({type:"list_start",ordered:a.length>1}),o=o[0].match(this.rules.item),r=!1,f=o.length,u=0;f>u;u++)s=o[u],c=s.length,s=s.replace(/^ *([*+-]|\d+\.) +/,""),~s.indexOf("\n ")&&(c-=s.length,s=this.options.pedantic?s.replace(/^ {1,4}/gm,""):s.replace(new RegExp("^ {1,"+c+"}","gm"),"")),this.options.smartLists&&u!==f-1&&(l=d.bullet.exec(o[u+1])[0],a===l||a.length>1&&l.length>1||(e=o.slice(u+1).join("\n")+e,u=f-1)),i=r||/\n\n(?!\s*$)/.test(s),u!==f-1&&(r="\n"===s.charAt(s.length-1),i||(i=r)),this.tokens.push({type:i?"loose_item_start":"list_item_start"}),this.token(s,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(o=this.rules.html.exec(e))e=e.substring(o[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===o[1]||"script"===o[1]||"style"===o[1]),text:o[0]});else if(!n&&t&&(o=this.rules.def.exec(e)))e=e.substring(o[0].length),this.tokens.links[o[1].toLowerCase()]={href:o[2],title:o[3]};else if(t&&(o=this.rules.table.exec(e))){for(e=e.substring(o[0].length),s={type:"table", -header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=f({},p),p.pedantic=f({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=f({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=f({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=l(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^
    /i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):l(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(l(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(l(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=l(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=l(t.href),r=t.title?l(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,l(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'
    '+(n?e:l(e,!0))+"\n
    \n":"
    "+(n?e:l(e,!0))+"\n
    "},o.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"'+e+"\n"},o.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},o.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"\n"},o.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},o.prototype.paragraph=function(e){return"

    "+e+"

    \n"},o.prototype.table=function(e,t){return"\n\n"+e+"\n\n"+t+"\n
    \n"},o.prototype.tablerow=function(e){return"
    - + {% for p in programs %} From b9becbf21f4ce78a080dab4be6cf2ea6e54fc362 Mon Sep 17 00:00:00 2001 From: Darks Date: Tue, 6 Jun 2023 19:52:46 +0200 Subject: [PATCH 81/92] moderation: fixed moving a post to another topic --- app/routes/posts/edit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index ea988a3..93ac5a3 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -166,7 +166,9 @@ def move_post(postid): move_form = MovePost(prefix="move_") search_form = SearchThread(prefix="thread_") - keyword = search_form.name.data if search_form.validate_on_submit() else "" + + # There is a bug with validate_on_submit + keyword = search_form.name.data if search_form.search.data else "" # Get 10 last corresponding threads # TODO: add support for every MainPost From be0d531b00b100d6c255cd754f4bc84db864568e Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 6 Jun 2023 20:14:07 +0200 Subject: [PATCH 82/92] style: Add blockquote style --- app/static/css/global.css | 6 ++++++ app/static/css/themes/FK_dark_theme.css | 5 +++++ app/static/css/themes/Tituya_v43_theme.css | 5 +++++ app/static/css/themes/default_theme.css | 5 +++++ app/static/less/global.less | 7 +++++++ 5 files changed, 28 insertions(+) diff --git a/app/static/css/global.css b/app/static/css/global.css index a956df7..0a84f86 100644 --- a/app/static/css/global.css +++ b/app/static/css/global.css @@ -78,6 +78,12 @@ section h2 { color: var(--text-light); padding-bottom: 2px; } +section blockquote { + margin: 0 0 10px 0; + border-left: 3px solid var(--border); + background: var(--background); + padding-left: 15px; +} button, .button, input[type="button"], diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css index acd62c4..bba8d16 100644 --- a/app/static/css/themes/FK_dark_theme.css +++ b/app/static/css/themes/FK_dark_theme.css @@ -130,6 +130,11 @@ table.codehilitetable { --background: #263238; } +blockquote { + --border: rgba(255, 255, 255, .3); + --background: transparent; +} + div.editor-toolbar, div.CodeMirror { --border: #404040; --background-light: #404040; diff --git a/app/static/css/themes/Tituya_v43_theme.css b/app/static/css/themes/Tituya_v43_theme.css index 0d75a56..7dda869 100644 --- a/app/static/css/themes/Tituya_v43_theme.css +++ b/app/static/css/themes/Tituya_v43_theme.css @@ -145,6 +145,11 @@ table.thread.topcomment { border: 1px solid #c0c0c0; } +blockquote { + --border: rgba(236, 36, 36, .7); + --background: transparent; +} + div.editor-toolbar { --border: #aaa2a2; --background-light: #c0c0c0; diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index 0b2d169..d885174 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -37,6 +37,11 @@ table th { --border: #ddd; } +blockquote { + --border: rgba(0, 0, 0, .3); + --background: transparent; +} + .form { --background: #fff; --text: #000; diff --git a/app/static/less/global.less b/app/static/less/global.less index 3036468..b175aa0 100644 --- a/app/static/less/global.less +++ b/app/static/less/global.less @@ -77,6 +77,13 @@ section { color: var(--text-light); padding-bottom: 2px; } + + blockquote { + margin: 0 0 10px 0; + border-left: 3px solid var(--border); + background: var(--background); + padding-left: 15px; + } } /* Buttons */ From 57644c4378293c9e67092606d121be521936c1f2 Mon Sep 17 00:00:00 2001 From: Eragon Date: Tue, 6 Jun 2023 20:57:05 +0200 Subject: [PATCH 83/92] editor: fix modal not closing when openning an other one --- app/static/css/editor.css | 10 ++++++++-- app/static/less/editor.less | 11 +++++++++-- app/static/scripts/editor.js | 7 ++++--- app/templates/widgets/editor.html | 5 +++-- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/static/css/editor.css b/app/static/css/editor.css index aa9e410..efb5ab9 100644 --- a/app/static/css/editor.css +++ b/app/static/css/editor.css @@ -51,14 +51,14 @@ position: absolute; left: 0px; width: auto; - min-width: 30vw; + min-width: min-content; text-align: left; right: inherit; background: var(--background-hover); border: var(--border); color: var(--text); padding: .2rem; - top: 3.2rem; + top: 2.3rem; z-index: 100; list-style-position: initial; list-style-type: none; @@ -67,10 +67,16 @@ margin: 0.8rem; margin-top: 0.4rem; margin-bottom: 1rem; + min-width: 30vw; } .editor .modal > div label { margin-top: 0.4rem; } +.editor .modal a.editor-emoji-close-btn { + display: inline-block; + margin: 0.3rem; + margin-top: 0.5rem; +} @media screen and (max-width:849px) { .editor .modal { width: 80vw; diff --git a/app/static/less/editor.less b/app/static/less/editor.less index 46d0a3b..3f3a5c4 100644 --- a/app/static/less/editor.less +++ b/app/static/less/editor.less @@ -63,7 +63,7 @@ position: absolute; left: 0px; width: auto; - min-width: 30vw; + min-width: min-content; text-align: left; right: inherit; @@ -79,7 +79,7 @@ border: var(--border); color: var(--text); padding: .2rem; - top: 3.2rem; + top: 2.3rem; z-index: 100; list-style-position: initial; list-style-type: none; @@ -88,11 +88,18 @@ margin: 0.8rem; margin-top: 0.4rem; margin-bottom: 1rem; + min-width: 30vw; label { margin-top: 0.4rem; } } + + a.editor-emoji-close-btn { + display: inline-block; + margin: 0.3rem; + margin-top: 0.5rem; + } } } diff --git a/app/static/scripts/editor.js b/app/static/scripts/editor.js index 930b223..436543c 100644 --- a/app/static/scripts/editor.js +++ b/app/static/scripts/editor.js @@ -114,7 +114,7 @@ function editor_clear_modals(event, close = true) document.getElementById('media-alt-input').value = ''; document.getElementById('media-link-input').value = ''; document.getElementById('link-desc-input').value = ''; - document.getElementById('link-desc-input').value = ''; + document.getElementById('link-link-input').value = ''; const media_type = document.getElementsByName("media-type"); for(i = 0; i < media_type.length; i++) { media_type[i].checked = false; @@ -162,7 +162,7 @@ function editor_display_link_modal(event) { event.currentTarget.querySelector("#link-desc-input").value = selection; } - event.currentTarget.children[1].style = {'display': 'block'}; + editor_display_child_modal(event); } function editor_insert_link(event, link_id, text_id, media = false) @@ -284,7 +284,8 @@ function editor_separator(event) { editor_insert_around(event, "", "\n---\n"); } -function editor_display_emoji_modal(event) { +function editor_display_child_modal(event) { + editor_clear_modals(event); event.currentTarget.children[1].style = {'display': 'block'}; } diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 2d30dc7..3f51e33 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -92,13 +92,14 @@ - @@ -120,7 +121,7 @@ -
    IDNomAuteurLabelProgrankTags
    IDNomAuteurPublicationLabelProgrankTags
    {{ p.id }} {{ p.name }}