Skip to content

Commit

Permalink
Support unicode emojis and remove emojify.js (go-gitea#11032)
Browse files Browse the repository at this point in the history
* Support unicode emojis and remove emojify.js

This PR replaces all use of emojify.js and adds unicode emoji support to various areas of gitea.

This works in a few ways:

First it adds emoji parsing support into gitea itself. This allows us to

 * Render emojis from valid alias (:smile:)
 * Detect unicode emojis and let us put them in their own class with proper aria-labels and styling
 * Easily allow for custom "emoji"
 * Support all emoji rendering and features without javascript
 * Uses plain unicode and lets the system render in appropriate emoji font
 * Doesn't leave us relying on external sources for updates/fixes/features

That same list of emoji is also used to create a json file which replaces the part of emojify.js that populates the emoji search tribute. This file is about 35KB with GZIP turned on and I've set it to load after the page renders to not hinder page load time (and this removes loading emojify.js also)

For custom "emoji" it uses a pretty simple scheme of just looking for /emojis/img/name.png where name is something a user has put in the "allowed reactions" setting we already have. The gitea reaction that was previously hard coded into a forked copy of emojify.js is included and works as a custom reaction under this method.

The emoji data sourced here is from https://github.com/github/gemoji which is the gem library Github uses for their emoji rendering (and a data source for other sites). So we should be able to easily render any emoji and :alias: that Github can, removing any errors from migrated content. They also update it as well, so we can sync when there are new unicode emoji lists released.

I've included a slimmed down and slightly modified forked copy of https://github.com/knq/emoji to make up our own emoji module. The code is pretty straight forward and again allows us to have a lot of flexibility in what happens.

I had seen a few comments about performance in some of the other threads if we render this ourselves, but there doesn't seem to be any issue here. In a test it can parse, convert, and render 1,000 emojis inside of a large markdown table in about 100ms on my laptop (which is many more emojis than will ever be in any normal issue). This also prevents any flickering and other weirdness from using javascript to render some things while using go for others.

Not included here are image fall back URLS. I don't really think they are necessary for anything new being written in 2020. However, managing the emoji ourselves would allow us to add these as a feature later on if it seems necessary.

Fixes: go-gitea#9182
Fixes: go-gitea#8974
Fixes: go-gitea#8953
Fixes: go-gitea#6628
Fixes: go-gitea#5130

* add new shared function emojiHTML

* don't increase emoji size in issue title

* Update templates/repo/issue/view_content/add_reaction.tmpl

Co-Authored-By: 6543 <[email protected]>

* Support for emoji rendering in various templates

* Render code and review comments as they should be

* Better way to handle mail subjects

* insert unicode from tribute selection

* Add template helper for plain text when needed

* Use existing replace function I forgot about

* Don't include emoji greater than Unicode Version 12

Only include emoji and aliases in JSON

* Update build/generate-emoji.go

* Tweak regex slightly to really match everything including random invisible characters. Run tests for every emoji we have

* final updates

* code review

* code review

* hard code gitea custom emoji to match previous behavior

* Update .eslintrc

Co-Authored-By: silverwind <[email protected]>

* disable preempt

Co-authored-by: silverwind <[email protected]>
Co-authored-by: 6543 <[email protected]>
Co-authored-by: Lauris BH <[email protected]>
Co-authored-by: guillep2k <[email protected]>
  • Loading branch information
5 people committed Apr 28, 2020
1 parent 922a239 commit 4563eb8
Show file tree
Hide file tree
Showing 928 changed files with 2,713 additions and 167 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ globals:
__webpack_public_path__: true
CodeMirror: false
Dropzone: false
emojify: false
SimpleMDE: false
u2fApi: false
Tribute: false

overrides:
- files: ["web_src/**/*.worker.js"]
Expand Down
1 change: 1 addition & 0 deletions assets/emoji.json

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions build/generate-emoji.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2015 Kenneth Shaw
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// +build ignore

package main

import (
"encoding/json"
"flag"
"fmt"
"go/format"
"io/ioutil"
"log"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
)

const (
gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
maxUnicodeVersion = 12
)

var (
flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
)

// Gemoji is a set of emoji data.
type Gemoji []Emoji

// Emoji represents a single emoji and associated data.
type Emoji struct {
Emoji string `json:"emoji"`
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases"`
UnicodeVersion string `json:"unicode_version,omitempty"`
}

// Don't include some fields in JSON
func (e Emoji) MarshalJSON() ([]byte, error) {
type emoji Emoji
x := emoji(e)
x.UnicodeVersion = ""
x.Description = ""
return json.Marshal(x)
}

func main() {
var err error

flag.Parse()

// generate data
buf, err := generate()
if err != nil {
log.Fatal(err)
}

// write
err = ioutil.WriteFile(*flagOut, buf, 0644)
if err != nil {
log.Fatal(err)
}
}

var replacer = strings.NewReplacer(
"main.Gemoji", "Gemoji",
"main.Emoji", "\n",
"}}", "},\n}",
", Description:", ", ",
", Aliases:", ", ",
", UnicodeVersion:", ", ",
)

var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)

func generate() ([]byte, error) {
var err error

// load gemoji data
res, err := http.Get(gemojiURL)
if err != nil {
return nil, err
}
defer res.Body.Close()

// read all
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}

// unmarshal
var data Gemoji
err = json.Unmarshal(body, &data)
if err != nil {
return nil, err
}

var re = regexp.MustCompile(`keycap|registered|copyright`)
tmp := data[:0]

// filter out emoji that require greater than max unicode version
for i := range data {
val, _ := strconv.ParseFloat(data[i].UnicodeVersion, 64)
if int(val) <= maxUnicodeVersion {
// remove these keycaps for now they really complicate matching since
// they include normal letters in them
if re.MatchString(data[i].Description) {
continue
}
tmp = append(tmp, data[i])
}
}
data = tmp

sort.Slice(data, func(i, j int) bool {
return data[i].Aliases[0] < data[j].Aliases[0]
})

aliasPairs := make([]string, 0)
aliasMap := make(map[string]int, len(data))

for i, e := range data {
if e.Emoji == "" || len(e.Aliases) == 0 {
continue
}
for _, a := range e.Aliases {
if a == "" {
continue
}
aliasMap[a] = i
aliasPairs = append(aliasPairs, ":"+a+":", e.Emoji)
}
}

// gitea customizations
i, ok := aliasMap["tada"]
if ok {
data[i].Aliases = append(data[i].Aliases, "hooray")
}
i, ok = aliasMap["laughing"]
if ok {
data[i].Aliases = append(data[i].Aliases, "laugh")
}

// add header
str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data))

// change the format of the unicode string
str = emojiRE.ReplaceAllStringFunc(str, func(s string) string {
var err error
s, err = strconv.Unquote(s[len("{Emoji:"):])
if err != nil {
panic(err)
}
return "{" + strconv.QuoteToASCII(s)
})

// write a JSON file to use with tribute
file, _ := json.Marshal(data)
_ = ioutil.WriteFile("assets/emoji.json", file, 0644)

// format
return format.Source([]byte(str))
}

const hdr = `
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package emoji
// Code generated by gen.go. DO NOT EDIT.
// Sourced from %s
//
var GemojiData = %#v
`
5 changes: 3 additions & 2 deletions custom/conf/app.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ SHOW_USER_EMAIL = true
DEFAULT_THEME = gitea
; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`.
THEMES = gitea,arc-green
; All available reactions. Allow users react with different emoji's
; For the whole list look at https://gitea.com/gitea/gitea.com/issues/8
;All available reactions users can choose on issues/prs and comments.
;Values can be emoji alias (:smile:) or a unicode emoji.
;For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png
REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes
; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
DEFAULT_SHOW_FULL_NAME = false
Expand Down
4 changes: 3 additions & 1 deletion docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `DEFAULT_THEME`: **gitea**: \[gitea, arc-green\]: Set the default theme for the Gitea install.
- `THEMES`: **gitea,arc-green**: All available themes. Allow users select personalized themes
regardless of the value of `DEFAULT_THEME`.
- `REACTIONS`: All available reactions. Allow users react with different emoji's.
- `REACTIONS`: All available reactions users can choose on issues/prs and comments
Values can be emoji alias (:smile:) or a unicode emoji.
For custom reactions, add a tightly cropped square image to public/emoji/img/reaction_name.png
- `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
- `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page.
- `USE_SERVICE_WORKER`: **true**: Whether to enable a Service Worker to cache frontend assets.
Expand Down
1 change: 0 additions & 1 deletion docs/content/page/index.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ Windows, on architectures like amd64, i386, ARM, PowerPC, and others.
* [DropzoneJS](http:https://www.dropzonejs.com/)
* [Highlight](https://highlightjs.org/)
* [Clipboard](https://zenorocha.github.io/clipboard.js/)
* [Emojify](https://github.com/Ranks/emojify.js)
* [CodeMirror](https://codemirror.net/)
* [jQuery Date Time Picker](https://github.com/xdan/datetimepicker)
* [jQuery MiniColors](https://github.com/claviska/jquery-minicolors)
Expand Down
1 change: 0 additions & 1 deletion docs/content/page/index.fr-fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,6 @@ Le but de ce projet est de fournir de la manière la plus simple, la plus rapide
* [DropzoneJS](http:https://www.dropzonejs.com/)
* [Highlight](https://highlightjs.org/)
* [Clipboard](https://zenorocha.github.io/clipboard.js/)
* [Emojify](https://github.com/Ranks/emojify.js)
* [CodeMirror](https://codemirror.net/)
* [jQuery Date Time Picker](https://github.com/xdan/datetimepicker)
* [jQuery MiniColors](https://github.com/claviska/jquery-minicolors)
Expand Down
1 change: 0 additions & 1 deletion docs/content/page/index.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ Gitea的首要目标是创建一个极易安装,运行非常快速,安装和
* [DropzoneJS](http:https://www.dropzonejs.com/)
* [Highlight](https://highlightjs.org/)
* [Clipboard](https://zenorocha.github.io/clipboard.js/)
* [Emojify](https://github.com/Ranks/emojify.js)
* [CodeMirror](https://codemirror.net/)
* [jQuery Date Time Picker](https://github.com/xdan/datetimepicker)
* [jQuery MiniColors](https://github.com/claviska/jquery-minicolors)
Expand Down
1 change: 0 additions & 1 deletion docs/content/page/index.zh-tw.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ Gitea 的首要目標是建立一個容易安裝,運行快速,安装和使
* [DropzoneJS](http:https://www.dropzonejs.com/)
* [Highlight](https://highlightjs.org/)
* [Clipboard](https://zenorocha.github.io/clipboard.js/)
* [Emojify](https://github.com/Ranks/emojify.js)
* [CodeMirror](https://codemirror.net/)
* [jQuery Date Time Picker](https://github.com/xdan/datetimepicker)
* [jQuery MiniColors](https://github.com/claviska/jquery-minicolors)
Expand Down
119 changes: 119 additions & 0 deletions modules/emoji/emoji.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2015 Kenneth Shaw
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package emoji

import (
"strings"
"sync"
)

// Gemoji is a set of emoji data.
type Gemoji []Emoji

// Emoji represents a single emoji and associated data.
type Emoji struct {
Emoji string
Description string
Aliases []string
UnicodeVersion string
}

var (
// codeMap provides a map of the emoji unicode code to its emoji data.
codeMap map[string]int

// aliasMap provides a map of the alias to its emoji data.
aliasMap map[string]int

// codeReplacer is the string replacer for emoji codes.
codeReplacer *strings.Replacer

// aliasReplacer is the string replacer for emoji aliases.
aliasReplacer *strings.Replacer

once sync.Once
)

func loadMap() {

once.Do(func() {

// initialize
codeMap = make(map[string]int, len(GemojiData))
aliasMap = make(map[string]int, len(GemojiData))

// process emoji codes and aliases
codePairs := make([]string, 0)
aliasPairs := make([]string, 0)
for i, e := range GemojiData {
if e.Emoji == "" || len(e.Aliases) == 0 {
continue
}

// setup codes
codeMap[e.Emoji] = i
codePairs = append(codePairs, e.Emoji, ":"+e.Aliases[0]+":")

// setup aliases
for _, a := range e.Aliases {
if a == "" {
continue
}

aliasMap[a] = i
aliasPairs = append(aliasPairs, ":"+a+":", e.Emoji)
}
}

// create replacers
codeReplacer = strings.NewReplacer(codePairs...)
aliasReplacer = strings.NewReplacer(aliasPairs...)
})
}

// FromCode retrieves the emoji data based on the provided unicode code (ie,
// "\u2618" will return the Gemoji data for "shamrock").
func FromCode(code string) *Emoji {
loadMap()
i, ok := codeMap[code]
if !ok {
return nil
}

return &GemojiData[i]
}

// FromAlias retrieves the emoji data based on the provided alias in the form
// "alias" or ":alias:" (ie, "shamrock" or ":shamrock:" will return the Gemoji
// data for "shamrock").
func FromAlias(alias string) *Emoji {
loadMap()
if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") {
alias = alias[1 : len(alias)-1]
}

i, ok := aliasMap[alias]
if !ok {
return nil
}

return &GemojiData[i]
}

// ReplaceCodes replaces all emoji codes with the first corresponding emoji
// alias (in the form of ":alias:") (ie, "\u2618" will be converted to
// ":shamrock:").
func ReplaceCodes(s string) string {
loadMap()
return codeReplacer.Replace(s)
}

// ReplaceAliases replaces all aliases of the form ":alias:" with its
// corresponding unicode value.
func ReplaceAliases(s string) string {
loadMap()
return aliasReplacer.Replace(s)
}
Loading

0 comments on commit 4563eb8

Please sign in to comment.