Skip to content

Commit cafb784

Browse files
bepmoorereason
authored andcommitted
Add emoji support
This uses the Emoji map from https://github.com/kyokomi/emoji -- but with a custom replacement implementation. The built-in are fine for most use cases, but in Hugo we do care about pure speed. The benchmarks below are skewed in Hugo's direction as the source and result is a byte slice, Kyokomi's implementation works best with strings. Curious: The easy-to-use `strings.Replacer` is also plenty fast. ``` BenchmarkEmojiKyokomiFprint-4 20000 86038 ns/op 33960 B/op 117 allocs/op BenchmarkEmojiKyokomiSprint-4 20000 83252 ns/op 38232 B/op 122 allocs/op BenchmarkEmojiStringsReplacer-4 100000 21092 ns/op 17248 B/op 25 allocs/op BenchmarkHugoEmoji-4 500000 5728 ns/op 624 B/op 13 allocs/op ``` Fixes #1891
1 parent 5926c6c commit cafb784

File tree

7 files changed

+256
-3
lines changed

7 files changed

+256
-3
lines changed

‎commands/hugo.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 The Hugo Authors. All rights reserved.
1+
// Copyright 2016 The Hugo Authors. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -301,6 +301,7 @@ func LoadDefaultSettings() {
301301
viper.SetDefault("SectionPagesMenu", "")
302302
viper.SetDefault("DisablePathToLower", false)
303303
viper.SetDefault("HasCJKLanguage", false)
304+
viper.SetDefault("EnableEmoji", false)
304305
}
305306

306307
// InitializeConfig initializes a config file with sensible default configuration flags.

‎docs/content/overview/configuration.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ Following is a list of Hugo-defined variables that you can configure and their c
9999
disableRobotsTXT: false
100100
# edit new content with this editor, if provided
101101
editor: ""
102+
# Enable Emoji emoticons support for page content.
103+
# See www.emoji-cheat-sheet.com
104+
enableEmoji: false
102105
footnoteAnchorPrefix: ""
103106
footnoteReturnLinkContents: ""
104107
# google analytics tracking id

‎docs/content/templates/functions.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,14 @@ These are formatted with the layout string.
413413
e.g. `{{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }}` → "Wednesday, Jan 21, 2015"
414414

415415

416+
### emojify
417+
418+
Runs the string through the Emoji emoticons processor. The result will be declared as "safe" so Go templates will not filter it.
419+
420+
See the [Emoji cheat sheet](http://www.emoji-cheat-sheet.com/) for available emoticons.
421+
422+
e.g. `{{ "I :heart: Hugo" | emojify }}`
423+
416424
### highlight
417425
Takes a string of code and a language, uses Pygments to return the syntax highlighted code in HTML.
418426
Used in the [highlight shortcode](/extras/highlighting/).

‎helpers/emoji.go‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2016 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package helpers
14+
15+
import (
16+
"bytes"
17+
"github.com/kyokomi/emoji"
18+
"sync"
19+
)
20+
21+
var (
22+
emojiInit sync.Once
23+
24+
emojis = make(map[string][]byte)
25+
26+
emojiDelim = []byte(":")
27+
emojiWordDelim = []byte(" ")
28+
emojiMaxSize int
29+
)
30+
31+
// Emojify "emojifies" the input source.
32+
// Note that the input byte slice will be modified if needed.
33+
// See http://www.emoji-cheat-sheet.com/
34+
func Emojify(source []byte) []byte {
35+
36+
emojiInit.Do(initEmoji)
37+
38+
start := 0
39+
k := bytes.Index(source[start:], emojiDelim)
40+
41+
for k != -1 {
42+
43+
j := start + k
44+
45+
upper := j + emojiMaxSize
46+
47+
if upper > len(source) {
48+
upper = len(source)
49+
}
50+
51+
endEmoji := bytes.Index(source[j+1:upper], emojiDelim)
52+
53+
if endEmoji < 0 {
54+
break
55+
}
56+
57+
nextWordDelim := bytes.Index(source[j:upper], emojiWordDelim)
58+
59+
if endEmoji == 0 || (nextWordDelim != -1 && nextWordDelim < endEmoji) {
60+
start += endEmoji + 1
61+
} else {
62+
endKey := endEmoji + j + 2
63+
emojiKey := source[j:endKey]
64+
65+
if emoji, ok := emojis[string(emojiKey)]; ok {
66+
source = append(source[:j], append(emoji, source[endKey:]...)...)
67+
}
68+
69+
start += endEmoji
70+
}
71+
72+
if start >= len(source) {
73+
break
74+
}
75+
76+
k = bytes.Index(source[start:], emojiDelim)
77+
}
78+
79+
return source
80+
81+
}
82+
83+
func initEmoji() {
84+
emojiMap := emoji.CodeMap()
85+
86+
for k, v := range emojiMap {
87+
emojis[k] = []byte(v + emoji.ReplacePadding)
88+
89+
if len(k) > emojiMaxSize {
90+
emojiMaxSize = len(k)
91+
}
92+
}
93+
94+
}

‎helpers/emoji_test.go‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2016 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package helpers
14+
15+
import (
16+
"github.com/kyokomi/emoji"
17+
"github.com/spf13/hugo/bufferpool"
18+
"reflect"
19+
"strings"
20+
"testing"
21+
)
22+
23+
func TestEmojiCustom(t *testing.T) {
24+
for i, this := range []struct {
25+
input string
26+
expect []byte
27+
}{
28+
{"A :smile: a day", []byte(emoji.Sprint("A :smile: a day"))},
29+
{"A few :smile:s a day", []byte(emoji.Sprint("A few :smile:s a day"))},
30+
{"A :smile: and a :beer: makes the day for sure.", []byte(emoji.Sprint("A :smile: and a :beer: makes the day for sure."))},
31+
{"A :smile: and: a :beer:", []byte(emoji.Sprint("A :smile: and: a :beer:"))},
32+
{"A :diamond_shape_with_a_dot_inside: and then some.", []byte(emoji.Sprint("A :diamond_shape_with_a_dot_inside: and then some."))},
33+
{":smile:", []byte(emoji.Sprint(":smile:"))},
34+
{":smi", []byte(":smi")},
35+
{"A :smile:", []byte(emoji.Sprint("A :smile:"))},
36+
{":beer:!", []byte(emoji.Sprint(":beer:!"))},
37+
{"::smile:", []byte(emoji.Sprint("::smile:"))},
38+
{":beer::", []byte(emoji.Sprint(":beer::"))},
39+
{" :beer: :", []byte(emoji.Sprint(" :beer: :"))},
40+
{":beer: and :smile: and another :beer:!", []byte(emoji.Sprint(":beer: and :smile: and another :beer:!"))},
41+
{" :beer: : ", []byte(emoji.Sprint(" :beer: : "))},
42+
{"No smilies for you!", []byte("No smilies for you!")},
43+
{" The motto: no smiles! ", []byte(" The motto: no smiles! ")},
44+
{":hugo_is_the_best_static_gen:", []byte(":hugo_is_the_best_static_gen:")},
45+
{"은행 :smile: 은행", []byte(emoji.Sprint("은행 :smile: 은행"))},
46+
} {
47+
result := Emojify([]byte(this.input))
48+
49+
if !reflect.DeepEqual(result, this.expect) {
50+
t.Errorf("[%d] got '%q' but expected %q", i, result, this.expect)
51+
}
52+
53+
}
54+
}
55+
56+
// The Emoji benchmarks below are heavily skewed in Hugo's direction:
57+
//
58+
// Hugo have a byte slice, wants a byte slice and doesn't mind if the original is modified.
59+
60+
func BenchmarkEmojiKyokomiFprint(b *testing.B) {
61+
62+
f := func(in []byte) []byte {
63+
buff := bufferpool.GetBuffer()
64+
defer bufferpool.PutBuffer(buff)
65+
emoji.Fprint(buff, string(in))
66+
67+
bc := make([]byte, buff.Len(), buff.Len())
68+
copy(bc, buff.Bytes())
69+
return bc
70+
}
71+
72+
doBenchmarkEmoji(b, f)
73+
}
74+
75+
func BenchmarkEmojiKyokomiSprint(b *testing.B) {
76+
77+
f := func(in []byte) []byte {
78+
return []byte(emoji.Sprint(string(in)))
79+
}
80+
81+
doBenchmarkEmoji(b, f)
82+
}
83+
84+
func BenchmarkHugoEmoji(b *testing.B) {
85+
doBenchmarkEmoji(b, Emojify)
86+
}
87+
88+
func doBenchmarkEmoji(b *testing.B, f func(in []byte) []byte) {
89+
90+
type input struct {
91+
in []byte
92+
expect []byte
93+
}
94+
95+
data := []struct {
96+
input string
97+
expect string
98+
}{
99+
{"A :smile: a day", emoji.Sprint("A :smile: a day")},
100+
{"A :smile: and a :beer: day keeps the doctor away", emoji.Sprint("A :smile: and a :beer: day keeps the doctor away")},
101+
{"A :smile: a day and 10 " + strings.Repeat(":beer: ", 10), emoji.Sprint("A :smile: a day and 10 " + strings.Repeat(":beer: ", 10))},
102+
{"No smiles today.", "No smiles today."},
103+
{"No smiles for you or " + strings.Repeat("you ", 1000), "No smiles for you or " + strings.Repeat("you ", 1000)},
104+
}
105+
106+
var in []input = make([]input, b.N*len(data))
107+
var cnt = 0
108+
for i := 0; i < b.N; i++ {
109+
for _, this := range data {
110+
in[cnt] = input{[]byte(this.input), []byte(this.expect)}
111+
cnt++
112+
}
113+
}
114+
115+
b.ResetTimer()
116+
cnt = 0
117+
for i := 0; i < b.N; i++ {
118+
for j := range data {
119+
currIn := in[cnt]
120+
cnt++
121+
result := f(currIn.in)
122+
if len(result) != len(currIn.expect) {
123+
b.Fatalf("[%d] emoji std, got \n%q but expected \n%q", j, result, currIn.expect)
124+
}
125+
}
126+
127+
}
128+
}

‎hugolib/handler_page.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 The Hugo Authors. All rights reserved.
1+
// Copyright 2016 The Hugo Authors. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ import (
1818
"github.com/spf13/hugo/source"
1919
"github.com/spf13/hugo/tpl"
2020
jww "github.com/spf13/jwalterweatherman"
21+
"github.com/spf13/viper"
2122
)
2223

2324
func init() {
@@ -114,6 +115,10 @@ func commonConvert(p *Page, t tpl.Template) HandledResult {
114115

115116
var err error
116117

118+
if viper.GetBool("EnableEmoji") {
119+
p.rawContent = helpers.Emojify(p.rawContent)
120+
}
121+
117122
renderedContent := p.renderContent(helpers.RemoveSummaryDivider(p.rawContent))
118123

119124
if len(p.contentShortCodes) > 0 {

���tpl/template_funcs.go‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 The Hugo Authors. All rights reserved.
1+
// Copyright 2016 The Hugo Authors. All rights reserved.
22
//
33
// Portions Copyright The Go Authors.
44

@@ -1156,6 +1156,19 @@ func jsonify(v interface{}) (template.HTML, error) {
11561156
return "", err
11571157
}
11581158
return template.HTML(b), nil
1159+
1160+
}
1161+
1162+
// emojify "emojifies" the given string.
1163+
//
1164+
// See http://www.emoji-cheat-sheet.com/
1165+
func emojify(in interface{}) (template.HTML, error) {
1166+
str, err := cast.ToStringE(in)
1167+
1168+
if err != nil {
1169+
return "", err
1170+
}
1171+
return template.HTML(helpers.Emojify([]byte(str))), nil
11591172
}
11601173

11611174
func refPage(page interface{}, ref, methodName string) template.HTML {
@@ -1715,6 +1728,7 @@ func init() {
17151728
"dict": dictionary,
17161729
"div": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '/') },
17171730
"echoParam": returnWhenSet,
1731+
"emojify": emojify,
17181732
"eq": eq,
17191733
"first": first,
17201734
"ge": ge,

0 commit comments

Comments
 (0)