mirror of
https://gitea.com/gitea/tea.git
synced 2024-12-26 02:49:45 +01:00
361 lines
8.8 KiB
Go
361 lines
8.8 KiB
Go
|
// package emoji is a extension for the goldmark(http://github.com/yuin/goldmark).
|
||
|
package emoji
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/yuin/goldmark"
|
||
|
east "github.com/yuin/goldmark-emoji/ast"
|
||
|
"github.com/yuin/goldmark-emoji/definition"
|
||
|
"github.com/yuin/goldmark/ast"
|
||
|
"github.com/yuin/goldmark/parser"
|
||
|
"github.com/yuin/goldmark/renderer"
|
||
|
"github.com/yuin/goldmark/renderer/html"
|
||
|
"github.com/yuin/goldmark/text"
|
||
|
"github.com/yuin/goldmark/util"
|
||
|
)
|
||
|
|
||
|
// Option interface sets options for this extension.
|
||
|
type Option interface {
|
||
|
emojiOption()
|
||
|
}
|
||
|
|
||
|
// ParserConfig struct is a data structure that holds configuration of
|
||
|
// the Emoji extension.
|
||
|
type ParserConfig struct {
|
||
|
Emojis definition.Emojis
|
||
|
}
|
||
|
|
||
|
const optEmojis parser.OptionName = "EmojiEmojis"
|
||
|
|
||
|
// SetOption implements parser.SetOptioner
|
||
|
func (c *ParserConfig) SetOption(name parser.OptionName, value interface{}) {
|
||
|
switch name {
|
||
|
case optEmojis:
|
||
|
c.Emojis = value.(definition.Emojis)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// A ParserOption interface sets options for the emoji parser.
|
||
|
type ParserOption interface {
|
||
|
Option
|
||
|
parser.Option
|
||
|
|
||
|
SetEmojiOption(*ParserConfig)
|
||
|
}
|
||
|
|
||
|
var _ ParserOption = &withEmojis{}
|
||
|
|
||
|
type withEmojis struct {
|
||
|
value definition.Emojis
|
||
|
}
|
||
|
|
||
|
func (o *withEmojis) emojiOption() {}
|
||
|
|
||
|
func (o *withEmojis) SetParserOption(c *parser.Config) {
|
||
|
c.Options[optEmojis] = o.value
|
||
|
}
|
||
|
|
||
|
func (o *withEmojis) SetEmojiOption(c *ParserConfig) {
|
||
|
c.Emojis = o.value
|
||
|
}
|
||
|
|
||
|
// WithMaping is a functional option that defines links names to unicode emojis.
|
||
|
func WithEmojis(value definition.Emojis) Option {
|
||
|
return &withEmojis{
|
||
|
value: value,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RenderingMethod indicates how emojis are rendered.
|
||
|
type RenderingMethod int
|
||
|
|
||
|
// RendererFunc will be used for rendering emojis.
|
||
|
type RendererFunc func(w util.BufWriter, source []byte, n *east.Emoji, config *RendererConfig)
|
||
|
|
||
|
const (
|
||
|
// Entity renders an emoji as an html entity.
|
||
|
Entity RenderingMethod = iota
|
||
|
|
||
|
// Unicode renders an emoji as unicode character.
|
||
|
Unicode
|
||
|
|
||
|
// Twemoji renders an emoji as an img tag with [twemoji](https://github.com/twitter/twemoji).
|
||
|
Twemoji
|
||
|
|
||
|
// Func renders an emoji using RendererFunc.
|
||
|
Func
|
||
|
)
|
||
|
|
||
|
// RendererConfig struct holds options for the emoji renderer.
|
||
|
type RendererConfig struct {
|
||
|
html.Config
|
||
|
|
||
|
// Method indicates how emojis are rendered.
|
||
|
Method RenderingMethod
|
||
|
|
||
|
// TwemojiTemplate is a printf template for twemoji. This value is valid only when Method is set to Twemoji.
|
||
|
// `printf` arguments are:
|
||
|
//
|
||
|
// 1: name (e.g. "face with tears of joy")
|
||
|
// 2: file name without an extension (e.g. 1f646-2642)
|
||
|
// 3: '/' if XHTML, otherwise ''
|
||
|
//
|
||
|
TwemojiTemplate string
|
||
|
|
||
|
// RendererFunc is a RendererFunc that renders emojis. This value is valid only when Method is set to Func.
|
||
|
RendererFunc RendererFunc
|
||
|
}
|
||
|
|
||
|
// DefaultTwemojiTemplate is a default value for RendererConfig.TwemojiTemplate.
|
||
|
const DefaultTwemojiTemplate = `<img class="emoji" draggable="false" alt="%[1]s" src="https://twemoji.maxcdn.com/v/latest/72x72/%[2]s.png"%[3]s>`
|
||
|
|
||
|
// SetOption implements renderer.SetOptioner.
|
||
|
func (c *RendererConfig) SetOption(name renderer.OptionName, value interface{}) {
|
||
|
switch name {
|
||
|
case optRenderingMethod:
|
||
|
c.Method = value.(RenderingMethod)
|
||
|
case optTwemojiTemplate:
|
||
|
c.TwemojiTemplate = value.(string)
|
||
|
case optRendererFunc:
|
||
|
c.RendererFunc = value.(RendererFunc)
|
||
|
default:
|
||
|
c.Config.SetOption(name, value)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// A RendererOption interface sets options for the emoji renderer.
|
||
|
type RendererOption interface {
|
||
|
Option
|
||
|
renderer.Option
|
||
|
|
||
|
SetEmojiOption(*RendererConfig)
|
||
|
}
|
||
|
|
||
|
var _ RendererOption = &withRenderingMethod{}
|
||
|
|
||
|
type withRenderingMethod struct {
|
||
|
value RenderingMethod
|
||
|
}
|
||
|
|
||
|
func (o *withRenderingMethod) emojiOption() {
|
||
|
}
|
||
|
|
||
|
// SetConfig implements renderer.Option#SetConfig.
|
||
|
func (o *withRenderingMethod) SetConfig(c *renderer.Config) {
|
||
|
c.Options[optRenderingMethod] = o.value
|
||
|
}
|
||
|
|
||
|
// SetEmojiOption implements RendererOption#SetEmojiOption
|
||
|
func (o *withRenderingMethod) SetEmojiOption(c *RendererConfig) {
|
||
|
c.Method = o.value
|
||
|
}
|
||
|
|
||
|
const optRenderingMethod renderer.OptionName = "EmojiRenderingMethod"
|
||
|
|
||
|
// WithRenderingMethod is a functional option that indicates how emojis are rendered.
|
||
|
func WithRenderingMethod(a RenderingMethod) Option {
|
||
|
return &withRenderingMethod{a}
|
||
|
}
|
||
|
|
||
|
type withTwemojiTemplate struct {
|
||
|
value string
|
||
|
}
|
||
|
|
||
|
func (o *withTwemojiTemplate) emojiOption() {
|
||
|
}
|
||
|
|
||
|
// SetConfig implements renderer.Option#SetConfig.
|
||
|
func (o *withTwemojiTemplate) SetConfig(c *renderer.Config) {
|
||
|
c.Options[optTwemojiTemplate] = o.value
|
||
|
}
|
||
|
|
||
|
// SetEmojiOption implements RendererOption#SetEmojiOption
|
||
|
func (o *withTwemojiTemplate) SetEmojiOption(c *RendererConfig) {
|
||
|
c.TwemojiTemplate = o.value
|
||
|
}
|
||
|
|
||
|
const optTwemojiTemplate renderer.OptionName = "EmojiTwemojiTemplate"
|
||
|
|
||
|
// WithTwemojiTemplate is a functional option that changes a twemoji img tag.
|
||
|
func WithTwemojiTemplate(s string) Option {
|
||
|
return &withTwemojiTemplate{s}
|
||
|
}
|
||
|
|
||
|
var _ RendererOption = &withRendererFunc{}
|
||
|
|
||
|
type withRendererFunc struct {
|
||
|
value RendererFunc
|
||
|
}
|
||
|
|
||
|
func (o *withRendererFunc) emojiOption() {
|
||
|
}
|
||
|
|
||
|
// SetConfig implements renderer.Option#SetConfig.
|
||
|
func (o *withRendererFunc) SetConfig(c *renderer.Config) {
|
||
|
c.Options[optRendererFunc] = o.value
|
||
|
}
|
||
|
|
||
|
// SetEmojiOption implements RendererOption#SetEmojiOption
|
||
|
func (o *withRendererFunc) SetEmojiOption(c *RendererConfig) {
|
||
|
c.RendererFunc = o.value
|
||
|
}
|
||
|
|
||
|
const optRendererFunc renderer.OptionName = "EmojiRendererFunc"
|
||
|
|
||
|
// WithRendererFunc is a functional option that changes a renderer func.
|
||
|
func WithRendererFunc(f RendererFunc) Option {
|
||
|
return &withRendererFunc{f}
|
||
|
}
|
||
|
|
||
|
type emojiParser struct {
|
||
|
ParserConfig
|
||
|
}
|
||
|
|
||
|
// NewParser returns a new parser.InlineParser that can parse emoji expressions.
|
||
|
func NewParser(opts ...ParserOption) parser.InlineParser {
|
||
|
p := &emojiParser{
|
||
|
ParserConfig: ParserConfig{
|
||
|
Emojis: definition.Github(),
|
||
|
},
|
||
|
}
|
||
|
for _, o := range opts {
|
||
|
o.SetEmojiOption(&p.ParserConfig)
|
||
|
}
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
func (s *emojiParser) Trigger() []byte {
|
||
|
return []byte{':'}
|
||
|
}
|
||
|
|
||
|
func (s *emojiParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||
|
line, _ := block.PeekLine()
|
||
|
if len(line) < 1 {
|
||
|
return nil
|
||
|
}
|
||
|
i := 1
|
||
|
for ; i < len(line); i++ {
|
||
|
c := line[i]
|
||
|
if !(util.IsAlphaNumeric(c) || c == '_' || c == '-' || c == '+') {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if i >= len(line) || line[i] != ':' {
|
||
|
return nil
|
||
|
}
|
||
|
block.Advance(i + 1)
|
||
|
shortName := line[1:i]
|
||
|
emoji, ok := s.Emojis.Get(util.BytesToReadOnlyString(shortName))
|
||
|
if !ok {
|
||
|
return nil
|
||
|
}
|
||
|
return east.NewEmoji(shortName, emoji)
|
||
|
}
|
||
|
|
||
|
type emojiHTMLRenderer struct {
|
||
|
RendererConfig
|
||
|
}
|
||
|
|
||
|
// NewHTMLRenderer returns a new HTMLRenderer.
|
||
|
func NewHTMLRenderer(opts ...RendererOption) renderer.NodeRenderer {
|
||
|
r := &emojiHTMLRenderer{
|
||
|
RendererConfig: RendererConfig{
|
||
|
Config: html.NewConfig(),
|
||
|
Method: Entity,
|
||
|
TwemojiTemplate: DefaultTwemojiTemplate,
|
||
|
RendererFunc: nil,
|
||
|
},
|
||
|
}
|
||
|
for _, opt := range opts {
|
||
|
opt.SetEmojiOption(&r.RendererConfig)
|
||
|
}
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||
|
func (r *emojiHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||
|
reg.Register(east.KindEmoji, r.renderEmoji)
|
||
|
}
|
||
|
|
||
|
const slash = " /"
|
||
|
const empty = ""
|
||
|
|
||
|
func (r *emojiHTMLRenderer) renderEmoji(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||
|
if !entering {
|
||
|
return ast.WalkContinue, nil
|
||
|
}
|
||
|
node := n.(*east.Emoji)
|
||
|
if !node.Value.IsUnicode() && r.Method != Func {
|
||
|
fmt.Fprintf(w, `<span title="%s">:%s:</span>`, util.EscapeHTML(util.StringToReadOnlyBytes(node.Value.Name)), node.ShortName)
|
||
|
return ast.WalkContinue, nil
|
||
|
}
|
||
|
|
||
|
switch r.Method {
|
||
|
case Entity:
|
||
|
for _, r := range node.Value.Unicode {
|
||
|
if r == 0x200D {
|
||
|
_, _ = w.WriteString("‍")
|
||
|
continue
|
||
|
}
|
||
|
fmt.Fprintf(w, "&#x%x;", r)
|
||
|
}
|
||
|
case Unicode:
|
||
|
fmt.Fprintf(w, "%s", string(node.Value.Unicode))
|
||
|
case Twemoji:
|
||
|
s := slash
|
||
|
if !r.XHTML {
|
||
|
s = empty
|
||
|
}
|
||
|
values := []string{}
|
||
|
for _, r := range node.Value.Unicode {
|
||
|
values = append(values, fmt.Sprintf("%x", r))
|
||
|
}
|
||
|
fmt.Fprintf(w, r.TwemojiTemplate, util.EscapeHTML(util.StringToReadOnlyBytes(node.Value.Name)), strings.Join(values, "-"), s)
|
||
|
case Func:
|
||
|
r.RendererFunc(w, source, node, &r.RendererConfig)
|
||
|
}
|
||
|
return ast.WalkContinue, nil
|
||
|
}
|
||
|
|
||
|
type emoji struct {
|
||
|
options []Option
|
||
|
}
|
||
|
|
||
|
// Emoji is a goldmark.Extender implementation.
|
||
|
var Emoji = &emoji{
|
||
|
options: []Option{},
|
||
|
}
|
||
|
|
||
|
// New returns a new extension with given options.
|
||
|
func New(opts ...Option) goldmark.Extender {
|
||
|
return &emoji{
|
||
|
options: opts,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Extend implements goldmark.Extender.
|
||
|
func (e *emoji) Extend(m goldmark.Markdown) {
|
||
|
pOpts := []ParserOption{}
|
||
|
rOpts := []RendererOption{}
|
||
|
for _, o := range e.options {
|
||
|
if po, ok := o.(ParserOption); ok {
|
||
|
pOpts = append(pOpts, po)
|
||
|
continue
|
||
|
}
|
||
|
if ro, ok := o.(RendererOption); ok {
|
||
|
rOpts = append(rOpts, ro)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||
|
util.Prioritized(NewHTMLRenderer(rOpts...), 200),
|
||
|
))
|
||
|
|
||
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
||
|
util.Prioritized(NewParser(pOpts...), 999),
|
||
|
))
|
||
|
|
||
|
}
|