gotosocial/internal/text/emojify.go
tobi 027a93facc
[feature/frontend] Respect prefers-reduced-motion for avatars, headers, and emojis (#3118)
* [feature/frontend] Respect `prefers-reduced-motion` for avatars, headers, and emojis

* go fmt

* fix tests

* use static version of instance thumbnail when appropriate

* use prefers-reduced-motion

* simplify account conversion a bit

* fix c&p error
2024-07-21 14:22:08 +02:00

143 lines
4.3 KiB
Go

// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package text
import (
"bytes"
"html"
"html/template"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/regexes"
)
// EmojifyWeb replaces emoji shortcodes like `:example:` in the given HTML
// fragment with `<img>` tags suitable for rendering on the web frontend.
func EmojifyWeb(emojis []apimodel.Emoji, html template.HTML) template.HTML {
out := emojify(
emojis,
string(html),
func(url, staticURL, code string, buf *bytes.Buffer) {
// Open a picture tag so we
// can present multiple options.
buf.WriteString(`<picture>`)
// Static version.
buf.WriteString(`<source `)
{
buf.WriteString(`class="emoji" `)
buf.WriteString(`srcset="` + staticURL + `" `)
buf.WriteString(`type="image/png" `)
// Show this version when user
// doesn't want an animated emoji.
buf.WriteString(`media="(prefers-reduced-motion: reduce)" `)
// Limit size to avoid showing
// huge emojis when unstyled.
buf.WriteString(`width="25" height="25" `)
}
buf.WriteString(`/>`)
// Original image source.
buf.WriteString(`<img `)
{
buf.WriteString(`class="emoji" `)
buf.WriteString(`src="` + url + `" `)
buf.WriteString(`title=":` + code + `:" `)
buf.WriteString(`alt=":` + code + `:" `)
// Lazy load emojis when
// they scroll into view.
buf.WriteString(`loading="lazy" `)
// Limit size to avoid showing
// huge emojis when unstyled.
buf.WriteString(`width="25" height="25" `)
}
buf.WriteString(`/>`)
// Close the picture tag.
buf.WriteString(`</picture>`)
},
)
// If input was safe,
// we can trust output.
return template.HTML(out) // #nosec G203
}
// EmojifyRSS replaces emoji shortcodes like `:example:` in the given text
// fragment with `<img>` tags suitable for rendering as RSS content.
func EmojifyRSS(emojis []apimodel.Emoji, text string) string {
return emojify(
emojis,
text,
func(url, staticURL, code string, buf *bytes.Buffer) {
// Original image source.
buf.WriteString(`<img `)
{
buf.WriteString(`src="` + url + `" `)
buf.WriteString(`title=":` + code + `:" `)
buf.WriteString(`alt=":` + code + `:" `)
// Limit size to avoid showing
// huge emojis in RSS readers.
buf.WriteString(`width="25" height="25" `)
}
buf.WriteString(`/>`)
},
)
}
// Demojify replaces emoji shortcodes like `:example:` in the given text
// fragment with empty strings, essentially stripping them from the text.
// This is useful for text used in OG Meta headers.
func Demojify(text string) string {
return regexes.EmojiFinder.ReplaceAllString(text, "")
}
func emojify(
emojis []apimodel.Emoji,
input string,
write func(url, staticURL, code string, buf *bytes.Buffer),
) string {
// Build map of shortcodes. Normalize each
// shortcode by readding closing colons.
emojisMap := make(map[string]apimodel.Emoji, len(emojis))
for _, emoji := range emojis {
shortcode := ":" + emoji.Shortcode + ":"
emojisMap[shortcode] = emoji
}
return regexes.ReplaceAllStringFunc(
regexes.EmojiFinder,
input,
func(shortcode string, buf *bytes.Buffer) string {
// Look for emoji with this shortcode.
emoji, ok := emojisMap[shortcode]
if !ok {
return shortcode
}
// Escape raw emoji content.
url := html.EscapeString(emoji.URL)
staticURL := html.EscapeString(emoji.StaticURL)
code := html.EscapeString(emoji.Shortcode)
// Write emoji repr to buffer.
write(url, staticURL, code, buf)
return buf.String()
},
)
}