emoji.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. """
  2. Emoji.
  3. pymdownx.emoji
  4. Emoji extension for EmojiOne's, GitHub's, or Twemoji's gemoji.
  5. MIT license.
  6. Copyright (c) 2016 - 2017 Isaac Muse <isaacmuse@gmail.com>
  7. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  8. documentation files (the "Software"), to deal in the Software without restriction, including without limitation
  9. the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
  10. and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  11. The above copyright notice and this permission notice shall be included in all copies or substantial portions
  12. of the Software.
  13. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
  14. TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  15. THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  16. CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. DEALINGS IN THE SOFTWARE.
  18. """
  19. from markdown import Extension
  20. from markdown.inlinepatterns import InlineProcessor
  21. from markdown import util as md_util
  22. import xml.etree.ElementTree as etree
  23. import inspect
  24. import copy
  25. import warnings
  26. from . import util
  27. RE_EMOJI = r'(:[+\-\w]+:)'
  28. SUPPORTED_INDEXES = ('emojione', 'gemoji', 'twemoji')
  29. UNICODE_VARIATION_SELECTOR_16 = 'fe0f'
  30. EMOJIONE_SVG_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/assets/svg/'
  31. EMOJIONE_PNG_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.7/assets/png/'
  32. TWEMOJI_SVG_CDN = 'https://twemoji.maxcdn.com/v/latest/svg/'
  33. TWEMOJI_PNG_CDN = 'https://twemoji.maxcdn.com/v/latest/72x72/'
  34. GITHUB_UNICODE_CDN = 'https://github.githubassets.com/images/icons/emoji/unicode/'
  35. GITHUB_CDN = 'https://github.githubassets.com/images/icons/emoji/'
  36. NO_TITLE = 'none'
  37. LONG_TITLE = 'long'
  38. SHORT_TITLE = 'short'
  39. VALID_TITLE = (LONG_TITLE, SHORT_TITLE, NO_TITLE)
  40. UNICODE_ENTITY = 'html_entity'
  41. UNICODE_ALT = ('unicode', UNICODE_ENTITY)
  42. LEGACY_ARG_COUNT = 8
  43. MSG_INDEX_WARN = """Using emoji indexes with no arguments is now deprecated.
  44. Emoji indexes now take 2 arguments: 'options' and 'md'.
  45. Please update your custom index accordingly.
  46. """
  47. def add_attriubtes(options, attributes):
  48. """Add additional attributes from options."""
  49. attr = options.get('attributes', {})
  50. if attr:
  51. for k, v in attr.items():
  52. attributes[k] = v
  53. def emojione(options, md):
  54. """The EmojiOne index."""
  55. from . import emoji1_db as emoji_map
  56. return {
  57. "name": emoji_map.name,
  58. "emoji": copy.deepcopy(emoji_map.emoji),
  59. "aliases": copy.deepcopy(emoji_map.aliases)
  60. }
  61. def gemoji(options, md):
  62. """The Gemoji index."""
  63. from . import gemoji_db as emoji_map
  64. return {
  65. "name": emoji_map.name,
  66. "emoji": copy.deepcopy(emoji_map.emoji),
  67. "aliases": copy.deepcopy(emoji_map.aliases)
  68. }
  69. def twemoji(options, md):
  70. """The Twemoji index."""
  71. from . import twemoji_db as emoji_map
  72. return {
  73. "name": emoji_map.name,
  74. "emoji": copy.deepcopy(emoji_map.emoji),
  75. "aliases": copy.deepcopy(emoji_map.aliases)
  76. }
  77. ###################
  78. # Converters
  79. ###################
  80. def to_png(index, shortname, alias, uc, alt, title, category, options, md):
  81. """Return PNG element."""
  82. if index == 'gemoji':
  83. def_image_path = GITHUB_UNICODE_CDN
  84. def_non_std_image_path = GITHUB_CDN
  85. elif index == 'twemoji':
  86. def_image_path = TWEMOJI_PNG_CDN
  87. def_image_path = TWEMOJI_PNG_CDN
  88. else:
  89. def_image_path = EMOJIONE_PNG_CDN
  90. def_non_std_image_path = EMOJIONE_PNG_CDN
  91. is_unicode = uc is not None
  92. classes = options.get('classes', index)
  93. # In general we can use the alias, but github specific images don't have one for each alias.
  94. # We can tell we have a github specific if there is no Unicode value.
  95. if is_unicode:
  96. image_path = options.get('image_path', def_image_path)
  97. else:
  98. image_path = options.get('non_standard_image_path', def_non_std_image_path)
  99. src = "%s%s.png" % (
  100. image_path,
  101. uc if is_unicode else shortname[1:-1]
  102. )
  103. attributes = {
  104. "class": classes,
  105. "alt": alt,
  106. "src": src
  107. }
  108. if title:
  109. attributes['title'] = title
  110. add_attriubtes(options, attributes)
  111. return etree.Element("img", attributes)
  112. def to_svg(index, shortname, alias, uc, alt, title, category, options, md):
  113. """Return SVG element."""
  114. if index == 'twemoji':
  115. svg_path = TWEMOJI_SVG_CDN
  116. else:
  117. svg_path = EMOJIONE_SVG_CDN
  118. attributes = {
  119. "class": options.get('classes', index),
  120. "alt": alt,
  121. "src": "%s%s.svg" % (
  122. options.get('image_path', svg_path),
  123. uc
  124. )
  125. }
  126. if title:
  127. attributes['title'] = title
  128. add_attriubtes(options, attributes)
  129. return etree.Element("img", attributes)
  130. def to_png_sprite(index, shortname, alias, uc, alt, title, category, options, md):
  131. """Return PNG sprite element."""
  132. attributes = {
  133. "class": '%(class)s-%(size)s-%(category)s _%(unicode)s' % {
  134. "class": options.get('classes', index),
  135. "size": options.get('size', '64'),
  136. "category": (category if category else ''),
  137. "unicode": uc
  138. }
  139. }
  140. if title:
  141. attributes['title'] = title
  142. add_attriubtes(options, attributes)
  143. el = etree.Element("span", attributes)
  144. el.text = md_util.AtomicString(alt)
  145. return el
  146. def to_svg_sprite(index, shortname, alias, uc, alt, title, category, options, md):
  147. """
  148. Return SVG sprite element.
  149. ```
  150. <svg class="%(classes)s"><description>%(alt)s</description>
  151. <use xlink:href="%(sprite)s#emoji-%(unicode)s"></use></svg>
  152. ```
  153. """
  154. xlink_href = '%s#emoji-%s' % (
  155. options.get('image_path', './../assets/sprites/emojione.sprites.svg'), uc
  156. )
  157. svg = etree.Element("svg", {"class": options.get('classes', index)})
  158. desc = etree.SubElement(svg, 'description')
  159. desc.text = md_util.AtomicString(alt)
  160. etree.SubElement(svg, 'use', {'xlink:href': xlink_href})
  161. return svg
  162. def to_alt(index, shortname, alias, uc, alt, title, category, options, md):
  163. """Return html entities."""
  164. return md.htmlStash.store(alt)
  165. ###################
  166. # Classes
  167. ###################
  168. class EmojiPattern(InlineProcessor):
  169. """Return element of type `tag` with a text attribute of group(2) of an `InlineProcessor`."""
  170. def __init__(self, pattern, config, md):
  171. """Initialize."""
  172. InlineProcessor.__init__(self, pattern, md)
  173. title = config['title']
  174. alt = config['alt']
  175. self.options = config['options']
  176. self._set_index(config["emoji_index"])
  177. self.unicode_alt = alt in UNICODE_ALT
  178. self.encoded_alt = alt == UNICODE_ENTITY
  179. self.remove_var_sel = config['remove_variation_selector']
  180. self.title = title if title in VALID_TITLE else NO_TITLE
  181. self.generator = config['emoji_generator']
  182. def _set_index(self, index):
  183. """Set the index."""
  184. if len(inspect.getfullargspec(index).args):
  185. self.emoji_index = index(self.options, self.md)
  186. else:
  187. warnings.warn(MSG_INDEX_WARN, util.PymdownxDeprecationWarning)
  188. self.emoji_index = index()
  189. def _remove_variation_selector(self, value):
  190. """Remove variation selectors."""
  191. return value.replace('-' + UNICODE_VARIATION_SELECTOR_16, '')
  192. def _get_unicode_char(self, value):
  193. """Get the Unicode char."""
  194. return ''.join([util.get_char(int(c, 16)) for c in value.split('-')])
  195. def _get_unicode(self, emoji):
  196. """
  197. Get Unicode and Unicode alt.
  198. Unicode: This is the stripped down form of the Unicode, no joining chars and no variation chars.
  199. Unicode code points are not always valid. If this is present and there is no 'unicode_alt',
  200. Unicode code points can be counted on as valid. For the most part, the returned `uc` should
  201. be used to reference image files, or create classes, but for inserting actual Unicode, 'uc_alt'
  202. should be used.
  203. Unicode Alt: When present, this will always be valid Unicode points. This contains not just the
  204. needed characters to identify the Unicode emoji, but the formatting as well. Joining characters
  205. and variation characters will be present. If you don't want variation chars, enable the global
  206. 'remove_variation_selector' option.
  207. If using gemoji, it is possible you will get no Unicode and no Unicode alt. This occurs with emoji
  208. like `:octocat:`. `:octocat:` is not a real emoji and has no Unicode code points, but it is provided by
  209. gemoji as an emoji anyways.
  210. """
  211. uc = emoji.get('unicode')
  212. uc_alt = emoji.get('unicode_alt', uc)
  213. if uc_alt and self.remove_var_sel:
  214. uc_alt = self._remove_variation_selector(uc_alt)
  215. return uc, uc_alt
  216. def _get_title(self, shortname, emoji):
  217. """Get the title."""
  218. if self.title == LONG_TITLE:
  219. title = emoji['name']
  220. elif self.title == SHORT_TITLE:
  221. title = shortname
  222. else:
  223. title = None
  224. return title
  225. def _get_alt(self, shortname, uc_alt):
  226. """Get alt form."""
  227. if uc_alt is None or not self.unicode_alt:
  228. alt = shortname
  229. else:
  230. alt = self._get_unicode_char(uc_alt)
  231. if self.encoded_alt:
  232. alt = ''.join(
  233. [md_util.AMP_SUBSTITUTE + ('#x%04x;' % util.get_ord(point)) for point in util.get_code_points(alt)]
  234. )
  235. return alt
  236. def _get_category(self, emoji):
  237. """Get the category."""
  238. return emoji.get('category')
  239. def handleMatch(self, m, data):
  240. """Handle emoji pattern matches."""
  241. el = m.group(1)
  242. shortname = self.emoji_index['aliases'].get(el, el)
  243. alias = None if shortname == el else el
  244. emoji = self.emoji_index['emoji'].get(shortname, None)
  245. if emoji:
  246. uc, uc_alt = self._get_unicode(emoji)
  247. title = self._get_title(el, emoji)
  248. alt = self._get_alt(el, uc_alt)
  249. category = self._get_category(emoji)
  250. el = self.generator(
  251. self.emoji_index['name'],
  252. shortname,
  253. alias,
  254. uc,
  255. alt,
  256. title,
  257. category,
  258. self.options,
  259. self.md
  260. )
  261. return el, m.start(0), m.end(0)
  262. class EmojiExtension(Extension):
  263. """Add emoji extension to Markdown class."""
  264. def __init__(self, *args, **kwargs):
  265. """Initialize."""
  266. self.config = {
  267. 'emoji_index': [
  268. emojione,
  269. "Function that returns the desired emoji index. - Default: 'pymdownx.emoji.emojione'"
  270. ],
  271. 'emoji_generator': [
  272. to_png,
  273. "Emoji generator method. - Default: pymdownx.emoji.to_png"
  274. ],
  275. 'title': [
  276. 'short',
  277. "What title to use on images. You can use 'long' which shows the long name, "
  278. "'short' which shows the shortname (:short:), or 'none' which shows no title. "
  279. "- Default: 'short'"
  280. ],
  281. 'alt': [
  282. 'unicode',
  283. "Control alt form. 'short' sets alt to the shortname (:short:), 'uniocde' sets "
  284. "alt to the raw Unicode value, and 'html_entity' sets alt to the HTML entity. "
  285. "- Default: 'unicode'"
  286. ],
  287. 'remove_variation_selector': [
  288. False,
  289. "Remove variation selector 16 from unicode. - Default: False"
  290. ],
  291. 'options': [
  292. {},
  293. "Emoji options see documentation for options for github and emojione."
  294. ]
  295. }
  296. super(EmojiExtension, self).__init__(*args, **kwargs)
  297. def extendMarkdown(self, md):
  298. """Add support for emoji."""
  299. config = self.getConfigs()
  300. util.escape_chars(md, [':'])
  301. md.inlinePatterns.register(EmojiPattern(RE_EMOJI, config, md), "emoji", 75)
  302. ###################
  303. # Make Available
  304. ###################
  305. def makeExtension(*args, **kwargs):
  306. """Return extension."""
  307. return EmojiExtension(*args, **kwargs)