diff options
Diffstat (limited to 'helpers')
-rw-r--r-- | helpers/content.go | 301 | ||||
-rw-r--r-- | helpers/content_test.go | 244 | ||||
-rw-r--r-- | helpers/docshelper.go | 37 | ||||
-rw-r--r-- | helpers/emoji.go | 96 | ||||
-rw-r--r-- | helpers/emoji_test.go | 143 | ||||
-rw-r--r-- | helpers/general.go | 522 | ||||
-rw-r--r-- | helpers/general_test.go | 460 | ||||
-rw-r--r-- | helpers/path.go | 488 | ||||
-rw-r--r-- | helpers/path_test.go | 561 | ||||
-rw-r--r-- | helpers/pathspec.go | 87 | ||||
-rw-r--r-- | helpers/pathspec_test.go | 62 | ||||
-rw-r--r-- | helpers/processing_stats.go | 120 | ||||
-rw-r--r-- | helpers/testhelpers_test.go | 49 | ||||
-rw-r--r-- | helpers/url.go | 241 | ||||
-rw-r--r-- | helpers/url_test.go | 260 |
15 files changed, 3671 insertions, 0 deletions
diff --git a/helpers/content.go b/helpers/content.go new file mode 100644 index 000000000..d04e34a07 --- /dev/null +++ b/helpers/content.go @@ -0,0 +1,301 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package helpers implements general utility functions that work with +// and on content. The helper functions defined here lay down the +// foundation of how Hugo works with files and filepaths, and perform +// string operations on content. +package helpers + +import ( + "bytes" + "html/template" + "strings" + "unicode" + "unicode/utf8" + + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/loggers" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/converter/hooks" + + "github.com/gohugoio/hugo/markup" + + "github.com/gohugoio/hugo/config" +) + +var ( + openingPTag = []byte("<p>") + closingPTag = []byte("</p>") + paragraphIndicator = []byte("<p") + closingIndicator = []byte("</") +) + +// ContentSpec provides functionality to render markdown content. +type ContentSpec struct { + Converters markup.ConverterProvider + anchorNameSanitizer converter.AnchorNameSanitizer + getRenderer func(t hooks.RendererType, id any) any + + // SummaryLength is the length of the summary that Hugo extracts from a content. + summaryLength int + + BuildFuture bool + BuildExpired bool + BuildDrafts bool + + Cfg config.Provider +} + +// NewContentSpec returns a ContentSpec initialized +// with the appropriate fields from the given config.Provider. +func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero.Fs, ex *hexec.Exec) (*ContentSpec, error) { + spec := &ContentSpec{ + summaryLength: cfg.GetInt("summaryLength"), + BuildFuture: cfg.GetBool("buildFuture"), + BuildExpired: cfg.GetBool("buildExpired"), + BuildDrafts: cfg.GetBool("buildDrafts"), + + Cfg: cfg, + } + + converterProvider, err := markup.NewConverterProvider(converter.ProviderConfig{ + Cfg: cfg, + ContentFs: contentFs, + Logger: logger, + Exec: ex, + }) + if err != nil { + return nil, err + } + + spec.Converters = converterProvider + p := converterProvider.Get("markdown") + conv, err := p.New(converter.DocumentContext{}) + if err != nil { + return nil, err + } + if as, ok := conv.(converter.AnchorNameSanitizer); ok { + spec.anchorNameSanitizer = as + } else { + // Use Goldmark's sanitizer + p := converterProvider.Get("goldmark") + conv, err := p.New(converter.DocumentContext{}) + if err != nil { + return nil, err + } + spec.anchorNameSanitizer = conv.(converter.AnchorNameSanitizer) + } + + return spec, nil +} + +// stripEmptyNav strips out empty <nav> tags from content. +func stripEmptyNav(in []byte) []byte { + return bytes.Replace(in, []byte("<nav>\n</nav>\n\n"), []byte(``), -1) +} + +// BytesToHTML converts bytes to type template.HTML. +func BytesToHTML(b []byte) template.HTML { + return template.HTML(string(b)) +} + +// ExtractTOC extracts Table of Contents from content. +func ExtractTOC(content []byte) (newcontent []byte, toc []byte) { + if !bytes.Contains(content, []byte("<nav>")) { + return content, nil + } + origContent := make([]byte, len(content)) + copy(origContent, content) + first := []byte(`<nav> +<ul>`) + + last := []byte(`</ul> +</nav>`) + + replacement := []byte(`<nav id="TableOfContents"> +<ul>`) + + startOfTOC := bytes.Index(content, first) + + peekEnd := len(content) + if peekEnd > 70+startOfTOC { + peekEnd = 70 + startOfTOC + } + + if startOfTOC < 0 { + return stripEmptyNav(content), toc + } + // Need to peek ahead to see if this nav element is actually the right one. + correctNav := bytes.Index(content[startOfTOC:peekEnd], []byte(`<li><a href="#`)) + if correctNav < 0 { // no match found + return content, toc + } + lengthOfTOC := bytes.Index(content[startOfTOC:], last) + len(last) + endOfTOC := startOfTOC + lengthOfTOC + + newcontent = append(content[:startOfTOC], content[endOfTOC:]...) + toc = append(replacement, origContent[startOfTOC+len(first):endOfTOC]...) + return +} + +func (c *ContentSpec) SanitizeAnchorName(s string) string { + return c.anchorNameSanitizer.SanitizeAnchorName(s) +} + +func (c *ContentSpec) ResolveMarkup(in string) string { + in = strings.ToLower(in) + switch in { + case "md", "markdown", "mdown": + return "markdown" + case "html", "htm": + return "html" + default: + if conv := c.Converters.Get(in); conv != nil { + return conv.Name() + } + } + return "" +} + +// TotalWords counts instance of one or more consecutive white space +// characters, as defined by unicode.IsSpace, in s. +// This is a cheaper way of word counting than the obvious len(strings.Fields(s)). +func TotalWords(s string) int { + n := 0 + inWord := false + for _, r := range s { + wasInWord := inWord + inWord = !unicode.IsSpace(r) + if inWord && !wasInWord { + n++ + } + } + return n +} + +// TruncateWordsByRune truncates words by runes. +func (c *ContentSpec) TruncateWordsByRune(in []string) (string, bool) { + words := make([]string, len(in)) + copy(words, in) + + count := 0 + for index, word := range words { + if count >= c.summaryLength { + return strings.Join(words[:index], " "), true + } + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + count++ + } else if count+runeCount < c.summaryLength { + count += runeCount + } else { + for ri := range word { + if count >= c.summaryLength { + truncatedWords := append(words[:index], word[:ri]) + return strings.Join(truncatedWords, " "), true + } + count++ + } + } + } + + return strings.Join(words, " "), false +} + +// TruncateWordsToWholeSentence takes content and truncates to whole sentence +// limited by max number of words. It also returns whether it is truncated. +func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) { + var ( + wordCount = 0 + lastWordIndex = -1 + ) + + for i, r := range s { + if unicode.IsSpace(r) { + wordCount++ + lastWordIndex = i + + if wordCount >= c.summaryLength { + break + } + + } + } + + if lastWordIndex == -1 { + return s, false + } + + endIndex := -1 + + for j, r := range s[lastWordIndex:] { + if isEndOfSentence(r) { + endIndex = j + lastWordIndex + utf8.RuneLen(r) + break + } + } + + if endIndex == -1 { + return s, false + } + + return strings.TrimSpace(s[:endIndex]), endIndex < len(s) +} + +// TrimShortHTML removes the <p>/</p> tags from HTML input in the situation +// where said tags are the only <p> tags in the input and enclose the content +// of the input (whitespace excluded). +func (c *ContentSpec) TrimShortHTML(input []byte) []byte { + firstOpeningP := bytes.Index(input, paragraphIndicator) + lastOpeningP := bytes.LastIndex(input, paragraphIndicator) + + lastClosingP := bytes.LastIndex(input, closingPTag) + lastClosing := bytes.LastIndex(input, closingIndicator) + + if firstOpeningP == lastOpeningP && lastClosingP == lastClosing { + input = bytes.TrimSpace(input) + input = bytes.TrimPrefix(input, openingPTag) + input = bytes.TrimSuffix(input, closingPTag) + input = bytes.TrimSpace(input) + } + return input +} + +func isEndOfSentence(r rune) bool { + return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n' +} + +// Kept only for benchmark. +func (c *ContentSpec) truncateWordsToWholeSentenceOld(content string) (string, bool) { + words := strings.Fields(content) + + if c.summaryLength >= len(words) { + return strings.Join(words, " "), false + } + + for counter, word := range words[c.summaryLength:] { + if strings.HasSuffix(word, ".") || + strings.HasSuffix(word, "?") || + strings.HasSuffix(word, ".\"") || + strings.HasSuffix(word, "!") { + upper := c.summaryLength + counter + 1 + return strings.Join(words[:upper], " "), (upper < len(words)) + } + } + + return strings.Join(words[:c.summaryLength], " "), true +} diff --git a/helpers/content_test.go b/helpers/content_test.go new file mode 100644 index 000000000..54b7ef3f9 --- /dev/null +++ b/helpers/content_test.go @@ -0,0 +1,244 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "html/template" + "strings" + "testing" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" + + qt "github.com/frankban/quicktest" +) + +const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>" + +func TestTrimShortHTML(t *testing.T) { + tests := []struct { + input, output []byte + }{ + {[]byte(""), []byte("")}, + {[]byte("Plain text"), []byte("Plain text")}, + {[]byte(" \t\n Whitespace text\n\n"), []byte("Whitespace text")}, + {[]byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")}, + {[]byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")}, + {[]byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")}, + {[]byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")}, + {[]byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>"), []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>")}, + } + + c := newTestContentSpec() + for i, test := range tests { + output := c.TrimShortHTML(test.input) + if !bytes.Equal(test.output, output) { + t.Errorf("Test %d failed. Expected %q got %q", i, test.output, output) + } + } +} + +func TestStripEmptyNav(t *testing.T) { + c := qt.New(t) + cleaned := stripEmptyNav([]byte("do<nav>\n</nav>\n\nbedobedo")) + c.Assert(cleaned, qt.DeepEquals, []byte("dobedobedo")) +} + +func TestBytesToHTML(t *testing.T) { + c := qt.New(t) + c.Assert(BytesToHTML([]byte("dobedobedo")), qt.Equals, template.HTML("dobedobedo")) +} + +func TestNewContentSpec(t *testing.T) { + cfg := config.NewWithTestDefaults() + c := qt.New(t) + + cfg.Set("summaryLength", 32) + cfg.Set("buildFuture", true) + cfg.Set("buildExpired", true) + cfg.Set("buildDrafts", true) + + spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil) + + c.Assert(err, qt.IsNil) + c.Assert(spec.summaryLength, qt.Equals, 32) + c.Assert(spec.BuildFuture, qt.Equals, true) + c.Assert(spec.BuildExpired, qt.Equals, true) + c.Assert(spec.BuildDrafts, qt.Equals, true) +} + +var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20) + +func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) { + c := newTestContentSpec() + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.TruncateWordsToWholeSentence(benchmarkTruncateString) + } +} + +func BenchmarkTestTruncateWordsToWholeSentenceOld(b *testing.B) { + c := newTestContentSpec() + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.truncateWordsToWholeSentenceOld(benchmarkTruncateString) + } +} + +func TestTruncateWordsToWholeSentence(t *testing.T) { + c := newTestContentSpec() + type test struct { + input, expected string + max int + truncated bool + } + data := []test{ + {"a b c", "a b c", 12, false}, + {"a b c", "a b c", 3, false}, + {"a", "a", 1, false}, + {"This is a sentence.", "This is a sentence.", 5, false}, + {"This is also a sentence!", "This is also a sentence!", 1, false}, + {"To be. Or not to be. That's the question.", "To be.", 1, true}, + {" \nThis is not a sentence\nAnd this is another", "This is not a sentence", 4, true}, + {"", "", 10, false}, + {"This... is a more difficult test?", "This... is a more difficult test?", 1, false}, + } + for i, d := range data { + c.summaryLength = d.max + output, truncated := c.TruncateWordsToWholeSentence(d.input) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + + if d.truncated != truncated { + t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated) + } + } +} + +func TestTruncateWordsByRune(t *testing.T) { + c := newTestContentSpec() + type test struct { + input, expected string + max int + truncated bool + } + data := []test{ + {"", "", 1, false}, + {"a b c", "a b c", 12, false}, + {"a b c", "a b c", 3, false}, + {"a", "a", 1, false}, + {"Hello 中国", "", 0, true}, + {"这是中文,全中文。", "这是中文,", 5, true}, + {"Hello 中国", "Hello 中", 2, true}, + {"Hello 中国", "Hello 中国", 3, false}, + {"Hello中国 Good 好的", "Hello中国 Good 好", 9, true}, + {"This is a sentence.", "This is", 2, true}, + {"This is also a sentence!", "This", 1, true}, + {"To be. Or not to be. That's the question.", "To be. Or not", 4, true}, + {" \nThis is not a sentence\n ", "This is not", 3, true}, + } + for i, d := range data { + c.summaryLength = d.max + output, truncated := c.TruncateWordsByRune(strings.Fields(d.input)) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + + if d.truncated != truncated { + t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated) + } + } +} + +func TestExtractTOCNormalContent(t *testing.T) { + content := []byte("<nav>\n<ul>\nTOC<li><a href=\"#") + + actualTocLessContent, actualToc := ExtractTOC(content) + expectedTocLess := []byte("TOC<li><a href=\"#") + expectedToc := []byte("<nav id=\"TableOfContents\">\n<ul>\n") + + if !bytes.Equal(actualTocLessContent, expectedTocLess) { + t.Errorf("Actual tocless (%s) did not equal expected (%s) tocless content", actualTocLessContent, expectedTocLess) + } + + if !bytes.Equal(actualToc, expectedToc) { + t.Errorf("Actual toc (%s) did not equal expected (%s) toc content", actualToc, expectedToc) + } +} + +func TestExtractTOCGreaterThanSeventy(t *testing.T) { + content := []byte("<nav>\n<ul>\nTOC This is a very long content which will definitely be greater than seventy, I promise you that.<li><a href=\"#") + + actualTocLessContent, actualToc := ExtractTOC(content) + // Because the start of Toc is greater than 70+startpoint of <li> content and empty TOC will be returned + expectedToc := []byte("") + + if !bytes.Equal(actualTocLessContent, content) { + t.Errorf("Actual tocless (%s) did not equal expected (%s) tocless content", actualTocLessContent, content) + } + + if !bytes.Equal(actualToc, expectedToc) { + t.Errorf("Actual toc (%s) did not equal expected (%s) toc content", actualToc, expectedToc) + } +} + +func TestExtractNoTOC(t *testing.T) { + content := []byte("TOC") + + actualTocLessContent, actualToc := ExtractTOC(content) + expectedToc := []byte("") + + if !bytes.Equal(actualTocLessContent, content) { + t.Errorf("Actual tocless (%s) did not equal expected (%s) tocless content", actualTocLessContent, content) + } + + if !bytes.Equal(actualToc, expectedToc) { + t.Errorf("Actual toc (%s) did not equal expected (%s) toc content", actualToc, expectedToc) + } +} + +var totalWordsBenchmarkString = strings.Repeat("Hugo Rocks ", 200) + +func TestTotalWords(t *testing.T) { + for i, this := range []struct { + s string + words int + }{ + {"Two, Words!", 2}, + {"Word", 1}, + {"", 0}, + {"One, Two, Three", 3}, + {totalWordsBenchmarkString, 400}, + } { + actualWordCount := TotalWords(this.s) + + if actualWordCount != this.words { + t.Errorf("[%d] Actual word count (%d) for test string (%s) did not match %d", i, actualWordCount, this.s, this.words) + } + } +} + +func BenchmarkTotalWords(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + wordCount := TotalWords(totalWordsBenchmarkString) + if wordCount != 400 { + b.Fatal("Wordcount error") + } + } +} diff --git a/helpers/docshelper.go b/helpers/docshelper.go new file mode 100644 index 000000000..35d07d366 --- /dev/null +++ b/helpers/docshelper.go @@ -0,0 +1,37 @@ +package helpers + +import ( + "sort" + + "github.com/alecthomas/chroma/v2/lexers" + "github.com/gohugoio/hugo/docshelper" +) + +// This is is just some helpers used to create some JSON used in the Hugo docs. +func init() { + docsProvider := func() docshelper.DocProvider { + var chromaLexers []any + + sort.Sort(lexers.GlobalLexerRegistry.Lexers) + + for _, l := range lexers.GlobalLexerRegistry.Lexers { + + config := l.Config() + + lexerEntry := struct { + Name string + Aliases []string + }{ + config.Name, + config.Aliases, + } + + chromaLexers = append(chromaLexers, lexerEntry) + + } + + return docshelper.DocProvider{"chroma": map[string]any{"lexers": chromaLexers}} + } + + docshelper.AddDocProviderFunc(docsProvider) +} diff --git a/helpers/emoji.go b/helpers/emoji.go new file mode 100644 index 000000000..eb47ff448 --- /dev/null +++ b/helpers/emoji.go @@ -0,0 +1,96 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helpers + +import ( + "bytes" + "sync" + + "github.com/kyokomi/emoji/v2" +) + +var ( + emojiInit sync.Once + + emojis = make(map[string][]byte) + + emojiDelim = []byte(":") + emojiWordDelim = []byte(" ") + emojiMaxSize int +) + +// Emoji returns the emojy given a key, e.g. ":smile:", nil if not found. +func Emoji(key string) []byte { + emojiInit.Do(initEmoji) + return emojis[key] +} + +// Emojify "emojifies" the input source. +// Note that the input byte slice will be modified if needed. +// See http://www.emoji-cheat-sheet.com/ +func Emojify(source []byte) []byte { + emojiInit.Do(initEmoji) + + start := 0 + k := bytes.Index(source[start:], emojiDelim) + + for k != -1 { + + j := start + k + + upper := j + emojiMaxSize + + if upper > len(source) { + upper = len(source) + } + + endEmoji := bytes.Index(source[j+1:upper], emojiDelim) + nextWordDelim := bytes.Index(source[j:upper], emojiWordDelim) + + if endEmoji < 0 { + start++ + } else if endEmoji == 0 || (nextWordDelim != -1 && nextWordDelim < endEmoji) { + start += endEmoji + 1 + } else { + endKey := endEmoji + j + 2 + emojiKey := source[j:endKey] + + if emoji, ok := emojis[string(emojiKey)]; ok { + source = append(source[:j], append(emoji, source[endKey:]...)...) + } + + start += endEmoji + } + + if start >= len(source) { + break + } + + k = bytes.Index(source[start:], emojiDelim) + } + + return source +} + +func initEmoji() { + emojiMap := emoji.CodeMap() + + for k, v := range emojiMap { + emojis[k] = []byte(v) + + if len(k) > emojiMaxSize { + emojiMaxSize = len(k) + } + } +} diff --git a/helpers/emoji_test.go b/helpers/emoji_test.go new file mode 100644 index 000000000..6485bb5fe --- /dev/null +++ b/helpers/emoji_test.go @@ -0,0 +1,143 @@ +// Copyright 2016 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package helpers + +import ( + "math" + "reflect" + "strings" + "testing" + + "github.com/gohugoio/hugo/bufferpool" + "github.com/kyokomi/emoji/v2" +) + +func TestEmojiCustom(t *testing.T) { + for i, this := range []struct { + input string + expect []byte + }{ + {"A :smile: a day", []byte("A 😄 a day")}, + {"A few :smile:s a day", []byte("A few 😄s a day")}, + {"A :smile: and a :beer: makes the day for sure.", []byte("A 😄 and a 🍺 makes the day for sure.")}, + {"A :smile: and: a :beer:", []byte("A 😄 and: a 🍺")}, + {"A :diamond_shape_with_a_dot_inside: and then some.", []byte("A 💠 and then some.")}, + {":smile:", []byte("😄")}, + {":smi", []byte(":smi")}, + {"A :smile:", []byte("A 😄")}, + {":beer:!", []byte("🍺!")}, + {"::smile:", []byte(":😄")}, + {":beer::", []byte("🍺:")}, + {" :beer: :", []byte(" 🍺 :")}, + {":beer: and :smile: and another :beer:!", []byte("🍺 and 😄 and another 🍺!")}, + {" :beer: : ", []byte(" 🍺 : ")}, + {"No smilies for you!", []byte("No smilies for you!")}, + {" The motto: no smiles! ", []byte(" The motto: no smiles! ")}, + {":hugo_is_the_best_static_gen:", []byte(":hugo_is_the_best_static_gen:")}, + {"은행 :smile: 은행", []byte("은행 😄 은행")}, + // #2198 + {"See: A :beer:!", []byte("See: A 🍺!")}, + {`Aaaaaaaaaa: aaaaaaaaaa aaaaaaaaaa aaaaaaaaaa. + +:beer:`, []byte(`Aaaaaaaaaa: aaaaaaaaaa aaaaaaaaaa aaaaaaaaaa. + +🍺`)}, + {"test :\n```bash\nthis is a test\n```\n\ntest\n\n:cool::blush:::pizza:\\:blush : : blush: :pizza:", []byte("test :\n```bash\nthis is a test\n```\n\ntest\n\n🆒😊:🍕\\:blush : : blush: 🍕")}, + { + // 2391 + "[a](http://gohugo.io) :smile: [r](http://gohugo.io/introduction/overview/) :beer:", + []byte(`[a](http://gohugo.io) 😄 [r](http://gohugo.io/introduction/overview/) 🍺`), + }, + } { + + result := Emojify([]byte(this.input)) + + if !reflect.DeepEqual(result, this.expect) { + t.Errorf("[%d] got %q but expected %q", i, result, this.expect) + } + + } +} + +// The Emoji benchmarks below are heavily skewed in Hugo's direction: +// +// Hugo have a byte slice, wants a byte slice and doesn't mind if the original is modified. + +func BenchmarkEmojiKyokomiFprint(b *testing.B) { + f := func(in []byte) []byte { + buff := bufferpool.GetBuffer() + defer bufferpool.PutBuffer(buff) + emoji.Fprint(buff, string(in)) + + bc := make([]byte, buff.Len()) + copy(bc, buff.Bytes()) + return bc + } + + doBenchmarkEmoji(b, f) +} + +func BenchmarkEmojiKyokomiSprint(b *testing.B) { + f := func(in []byte) []byte { + return []byte(emoji.Sprint(string(in))) + } + + doBenchmarkEmoji(b, f) +} + +func BenchmarkHugoEmoji(b *testing.B) { + doBenchmarkEmoji(b, Emojify) +} + +func doBenchmarkEmoji(b *testing.B, f func(in []byte) []byte) { + type input struct { + in []byte + expect []byte + } + + data := []struct { + input string + expect string + }{ + {"A :smile: a day", emoji.Sprint("A :smile: a day")}, + {"A :smile: and a :beer: day keeps the doctor away", emoji.Sprint("A :smile: and a :beer: day keeps the doctor away")}, + {"A :smile: a day and 10 " + strings.Repeat(":beer: ", 10), emoji.Sprint("A :smile: a day and 10 " + strings.Repeat(":beer: ", 10))}, + {"No smiles today.", "No smiles today."}, + {"No smiles for you or " + strings.Repeat("you ", 1000), "No smiles for you or " + strings.Repeat("you ", 1000)}, + } + + in := make([]input, b.N |