56 行
2.0 KiB
Python
56 行
2.0 KiB
Python
import json
|
|
import re
|
|
|
|
import aiofiles
|
|
from quart import escape
|
|
|
|
class BadEmote(Exception):
|
|
pass
|
|
|
|
class BadEmoteName(BadEmote):
|
|
pass
|
|
|
|
def _load_emote_schema(emotes):
|
|
for key in ('name', 'file', 'width', 'height'):
|
|
for emote in emotes:
|
|
if key not in emote:
|
|
raise BadEmote(f'emotes must have a `{key}`: {emote}')
|
|
precompute_emote_regex(emotes)
|
|
return emotes
|
|
|
|
def load_emote_schema(filepath):
|
|
with open(filepath) as fp:
|
|
emotes = json.load(fp)
|
|
return _load_emote_schema(emotes)
|
|
|
|
async def load_emote_schema_async(filepath):
|
|
async with aiofiles.open(filepath) as fp:
|
|
data = await fp.read(8192)
|
|
return _load_emote_schema(json.loads(data))
|
|
|
|
def precompute_emote_regex(schema):
|
|
for emote in schema:
|
|
if not emote['name']:
|
|
raise BadEmoteName(f'emote names cannot be empty: {emote}')
|
|
if re.search(r'\s', emote['name']):
|
|
raise BadEmoteName(
|
|
f'whitespace is not allowed in emote names: {emote["name"]!r}'
|
|
)
|
|
for length in (emote['width'], emote['height']):
|
|
if length is not None and (not isinstance(length, int) or length < 0):
|
|
raise BadEmoteName(
|
|
f'emote dimensions must be null or non-negative integers: '
|
|
f'{emote}'
|
|
)
|
|
# If the emote name begins with a word character [a-zA-Z0-9_],
|
|
# match only if preceded by a non-word character or the empty
|
|
# string. Similarly for the end of the emote name.
|
|
# Examples:
|
|
# * ":joy:" matches "abc :joy:~xyz" and "abc:joy:xyz"
|
|
# * "JoySi" matches "abc JoySi~xyz" but NOT "abcJoySiabc"
|
|
onset = r'(?:^|(?<=\W))' if re.fullmatch(r'\w', emote['name'][0]) else r''
|
|
finish = r'(?:$|(?=\W))' if re.fullmatch(r'\w', emote['name'][-1]) else r''
|
|
emote['regex'] = re.compile(''.join(
|
|
(onset, re.escape(escape(emote['name'])), finish)
|
|
))
|