Rewrote segments to adapt to later cchat API
This commit is contained in:
parent
0f1cdafec6
commit
e9796170f8
4
go.mod
4
go.mod
|
@ -4,10 +4,10 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/diamondburned/arikawa v1.3.0
|
||||
github.com/diamondburned/cchat v0.1.3
|
||||
github.com/diamondburned/cchat v0.2.11
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200820222640-35796f938a58
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-test/deep v1.0.6
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/yuin/goldmark v1.1.30
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -22,6 +22,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
|
|||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI=
|
||||
github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
|
||||
|
@ -79,6 +80,8 @@ github.com/diamondburned/cchat v0.1.2 h1:/9/xtHeifirMHiHsf/acL23UPZuS2YdzqWMMR5+
|
|||
github.com/diamondburned/cchat v0.1.2/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.1.3 h1:4xq8Tc+U0OUf2Vr6s8Igb5iADmeJ9oM1Db+M6zF/PDQ=
|
||||
github.com/diamondburned/cchat v0.1.3/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.2.11 h1:w4c/6t02htGtVj6yIjznecOGMlkcj0TmmLy+K48gHeM=
|
||||
github.com/diamondburned/cchat v0.2.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
||||
|
@ -137,6 +140,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
|||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
|
|
|
@ -15,6 +15,8 @@ type Author struct {
|
|||
avatar string
|
||||
}
|
||||
|
||||
var _ cchat.Author = (*Author)(nil)
|
||||
|
||||
func NewUser(u discord.User, s *state.Instance) Author {
|
||||
var name = text.Rich{Content: u.Username}
|
||||
if u.Bot {
|
||||
|
|
|
@ -62,6 +62,13 @@ type Message struct {
|
|||
mentioned bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.MessageCreate = (*Message)(nil)
|
||||
_ cchat.MessageUpdate = (*Message)(nil)
|
||||
_ cchat.MessageDelete = (*Message)(nil)
|
||||
_ cchat.Noncer = (*Message)(nil)
|
||||
)
|
||||
|
||||
func NewMessageUpdateContent(msg discord.Message, s *state.Instance) Message {
|
||||
// Check if content is empty.
|
||||
if msg.Content == "" {
|
||||
|
|
|
@ -1,59 +1,77 @@
|
|||
package segments
|
||||
package blockquote
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
type BlockquoteSegment struct {
|
||||
start, end int
|
||||
func init() {
|
||||
renderer.Register(ast.KindBlockquote, blockquote)
|
||||
}
|
||||
|
||||
var _ text.Quoteblocker = (*BlockquoteSegment)(nil)
|
||||
func blockquote(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*ast.Blockquote)
|
||||
|
||||
func (r *TextRenderer) blockquote(n *ast.Blockquote, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
// Block formatting.
|
||||
r.ensureBreak()
|
||||
defer r.ensureBreak()
|
||||
r.EnsureBreak()
|
||||
defer r.EnsureBreak()
|
||||
|
||||
// Create a segment.
|
||||
var seg = BlockquoteSegment{start: r.buf.Len()}
|
||||
var seg = Segment{Start: r.Buffer.Len()}
|
||||
|
||||
// A blockquote contains a paragraph each line. Because Discord.
|
||||
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
r.buf.WriteString("> ")
|
||||
r.Buffer.WriteString("> ")
|
||||
|
||||
ast.Walk(child, func(node ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
// We only call when entering, since we don't want to trigger a
|
||||
// hard new line after each paragraph.
|
||||
if enter {
|
||||
return r.renderNode(node, enter)
|
||||
return r.RenderNode(node, enter)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Search until the last non-whitespace.
|
||||
var i = r.buf.Len() - 1
|
||||
for bytes := r.buf.Bytes(); i > 0 && isSpace(bytes[i]); i-- {
|
||||
var i = r.Buffer.Len() - 1
|
||||
for bytes := r.Buffer.Bytes(); i > 0 && isSpace(bytes[i]); i-- {
|
||||
}
|
||||
|
||||
// The ending will have a trailing character that's not covered, so
|
||||
// we'll need to do that ourselves.
|
||||
// End the codeblock at that non-whitespace location.
|
||||
seg.end = i + 1
|
||||
r.append(seg)
|
||||
seg.End = i + 1
|
||||
r.Append(seg)
|
||||
}
|
||||
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
func (b BlockquoteSegment) Bounds() (start, end int) {
|
||||
return b.start, b.end
|
||||
type Segment struct {
|
||||
empty.TextSegment
|
||||
Start, End int
|
||||
}
|
||||
|
||||
func (b BlockquoteSegment) Quote() {}
|
||||
var (
|
||||
_ text.Segment = (*Segment)(nil)
|
||||
_ text.Quoteblocker = (*Segment)(nil)
|
||||
)
|
||||
|
||||
func (b Segment) Bounds() (start, end int) {
|
||||
return b.Start, b.End
|
||||
}
|
||||
|
||||
func (b Segment) AsBlockquoter() text.Quoteblocker {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b Segment) QuotePrefix() string {
|
||||
return "> "
|
||||
}
|
||||
|
||||
// isSpace is a quick function that matches if the byte is a space, a new line
|
||||
// or a return carriage.
|
|
@ -1,53 +0,0 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
type CodeblockSegment struct {
|
||||
start, end int
|
||||
language string
|
||||
}
|
||||
|
||||
var _ text.Codeblocker = (*CodeblockSegment)(nil)
|
||||
|
||||
func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
// Open the block by adding formatting and all.
|
||||
r.startBlock()
|
||||
r.buf.WriteString("---\n")
|
||||
|
||||
// Create a segment.
|
||||
seg := CodeblockSegment{
|
||||
start: r.buf.Len(),
|
||||
language: string(n.Language(r.src)),
|
||||
}
|
||||
|
||||
// Join all lines together.
|
||||
var lines = n.Lines()
|
||||
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
r.buf.Write(line.Value(r.src))
|
||||
}
|
||||
|
||||
// Close the segment.
|
||||
seg.end = r.buf.Len()
|
||||
r.append(seg)
|
||||
|
||||
// Close the block.
|
||||
r.buf.WriteString("\n---")
|
||||
r.endBlock()
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func (c CodeblockSegment) Bounds() (start, end int) {
|
||||
return c.start, c.end
|
||||
}
|
||||
|
||||
func (c CodeblockSegment) CodeblockLanguage() string {
|
||||
return c.language
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package codeblock
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func init() {
|
||||
renderer.Register(ast.KindCodeBlock, codeblock)
|
||||
}
|
||||
|
||||
func codeblock(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*ast.FencedCodeBlock)
|
||||
|
||||
if enter {
|
||||
// Open the block by adding formatting and all.
|
||||
r.StartBlock()
|
||||
r.Buffer.WriteString("---\n")
|
||||
|
||||
// Create a segment.
|
||||
seg := CodeblockSegment{
|
||||
Start: r.Buffer.Len(),
|
||||
Language: string(n.Language(r.Source)),
|
||||
}
|
||||
|
||||
// Join all lines together.
|
||||
var lines = n.Lines()
|
||||
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
r.Buffer.Write(line.Value(r.Source))
|
||||
}
|
||||
|
||||
// Close the segment.
|
||||
seg.End = r.Buffer.Len()
|
||||
r.Append(seg)
|
||||
|
||||
// Close the block.
|
||||
r.Buffer.WriteString("\n---")
|
||||
r.EndBlock()
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
type CodeblockSegment struct {
|
||||
empty.TextSegment
|
||||
Start, End int
|
||||
Language string
|
||||
}
|
||||
|
||||
var _ text.Codeblocker = (*CodeblockSegment)(nil)
|
||||
|
||||
func (c CodeblockSegment) Bounds() (start, end int) {
|
||||
return c.Start, c.End
|
||||
}
|
||||
|
||||
func (c CodeblockSegment) AsCodeblocker() text.Codeblocker {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c CodeblockSegment) CodeblockLanguage() string {
|
||||
return c.Language
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package segments
|
||||
|
||||
import "github.com/diamondburned/cchat/text"
|
||||
|
||||
const blurple = 0x7289DA
|
||||
|
||||
type Colored struct {
|
||||
start int
|
||||
end int
|
||||
color uint32
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Colorer = (*Colored)(nil)
|
||||
_ text.Segment = (*Colored)(nil)
|
||||
)
|
||||
|
||||
func NewColored(strlen int, color uint32) Colored {
|
||||
return Colored{0, strlen, color}
|
||||
}
|
||||
|
||||
func NewBlurpleSegment(start, end int) Colored {
|
||||
return Colored{start, end, blurple}
|
||||
}
|
||||
|
||||
func NewColoredSegment(start, end int, color uint32) Colored {
|
||||
return Colored{start, end, color}
|
||||
}
|
||||
|
||||
func (color Colored) Bounds() (start, end int) {
|
||||
return color.start, color.end
|
||||
}
|
||||
|
||||
func (color Colored) Color() uint32 {
|
||||
return color.color
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package colored
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
)
|
||||
|
||||
const Blurple = 0x7289DAFF
|
||||
|
||||
type Color uint32
|
||||
|
||||
var _ text.Colorer = (*Color)(nil)
|
||||
|
||||
// FromRGB converts the 24-bit RGB color to 32-bit RGBA.
|
||||
func FromRGB(rgb uint32) Color {
|
||||
return Color(text.SolidColor(rgb))
|
||||
}
|
||||
|
||||
func (c Color) Color() uint32 {
|
||||
return uint32(c)
|
||||
}
|
||||
|
||||
// Segment implements a colored text segment.
|
||||
type Segment struct {
|
||||
empty.TextSegment
|
||||
start int
|
||||
end int
|
||||
color Color
|
||||
}
|
||||
|
||||
var _ text.Segment = (*Segment)(nil)
|
||||
|
||||
func New(strlen int, color uint32) Segment {
|
||||
return NewSegment(0, strlen, color)
|
||||
}
|
||||
|
||||
func NewBlurple(start, end int) Segment {
|
||||
return Segment{
|
||||
start: start,
|
||||
end: end,
|
||||
color: Blurple,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSegment(start, end int, color uint32) Segment {
|
||||
return Segment{
|
||||
start: start,
|
||||
end: end,
|
||||
color: FromRGB(color),
|
||||
}
|
||||
}
|
||||
|
||||
func (seg Segment) Bounds() (start, end int) {
|
||||
return seg.start, seg.end
|
||||
}
|
||||
|
||||
func (seg Segment) AsColorer() text.Colorer {
|
||||
return seg.color
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
var imageExts = []string{".jpg", ".jpeg", ".png", ".webp", ".gif"}
|
||||
|
||||
func (r *TextRenderer) writeEmbedSep(embedColor discord.Color) {
|
||||
if start, end := r.writeString("---"); embedColor > 0 {
|
||||
r.append(NewColoredSegment(start, end, embedColor.Uint32()))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderEmbeds(embeds []discord.Embed, m *discord.Message, s state.Store) {
|
||||
for _, embed := range embeds {
|
||||
r.startBlock()
|
||||
r.writeEmbedSep(embed.Color)
|
||||
r.ensureBreak()
|
||||
|
||||
r.renderEmbed(embed, m, s)
|
||||
|
||||
r.ensureBreak()
|
||||
r.writeEmbedSep(embed.Color) // render prepends newline already
|
||||
r.endBlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s state.Store) {
|
||||
if a := embed.Author; a != nil && a.Name != "" {
|
||||
if a.ProxyIcon != "" {
|
||||
r.append(EmbedAuthor(r.buf.Len(), *a))
|
||||
r.buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
start, end := r.writeString(a.Name)
|
||||
r.ensureBreak()
|
||||
|
||||
if a.URL != "" {
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
a.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if embed.Title != "" {
|
||||
start, end := r.writeString(embed.Title)
|
||||
r.ensureBreak()
|
||||
|
||||
// Make the title bold.
|
||||
r.append(InlineSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
attributes: text.AttrBold,
|
||||
})
|
||||
|
||||
if embed.URL != "" {
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
embed.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a thumbnail, then write one.
|
||||
if embed.Thumbnail != nil {
|
||||
r.append(EmbedThumbnail(r.buf.Len(), *embed.Thumbnail))
|
||||
// Guarantee 2 lines because thumbnail needs its own.
|
||||
r.startBlockN(2)
|
||||
}
|
||||
|
||||
if embed.Description != "" {
|
||||
// Since Discord embeds' descriptions are technically Markdown, we can
|
||||
// borrow our Markdown parser for this.
|
||||
node := md.ParseWithMessage([]byte(embed.Description), s, m, false)
|
||||
// Create a new renderer with inherited state and buffer but a new byte
|
||||
// source.
|
||||
desc := r.clone([]byte(embed.Description))
|
||||
// Walk using the newly created state.
|
||||
desc.walk(node)
|
||||
// Join the created state.
|
||||
r.join(desc)
|
||||
// Write a new line.
|
||||
r.ensureBreak()
|
||||
}
|
||||
|
||||
if len(embed.Fields) > 0 {
|
||||
// Pad two new lines.
|
||||
r.startBlockN(2)
|
||||
|
||||
// Write fields indented once.
|
||||
for _, field := range embed.Fields {
|
||||
fmt.Fprintf(r.buf, "\t%s: %s\n", field.Name, field.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if f := embed.Footer; f != nil && f.Text != "" {
|
||||
if f.ProxyIcon != "" {
|
||||
r.append(EmbedFooter(r.buf.Len(), *f))
|
||||
r.buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
r.buf.WriteString(f.Text)
|
||||
r.ensureBreak()
|
||||
}
|
||||
|
||||
if embed.Timestamp.IsValid() {
|
||||
if embed.Footer != nil {
|
||||
r.buf.WriteString(" - ")
|
||||
}
|
||||
|
||||
r.buf.WriteString(embed.Timestamp.Format(time.RFC1123))
|
||||
r.ensureBreak()
|
||||
}
|
||||
|
||||
// Write an image if there's one.
|
||||
if embed.Image != nil {
|
||||
r.append(EmbedImage(r.buf.Len(), *embed.Image))
|
||||
// Images take up its own empty line, so we should guarantee 2 empty
|
||||
// lines.
|
||||
r.startBlockN(2)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
|
||||
// Don't do anything if there are no attachments.
|
||||
if len(attachments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Start a (small)new block before rendering attachments.
|
||||
r.ensureBreak()
|
||||
|
||||
// Render all attachments. Newline delimited.
|
||||
for i, attachment := range attachments {
|
||||
r.renderAttachment(attachment)
|
||||
|
||||
if i != len(attachments) {
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderAttachment(a discord.Attachment) {
|
||||
if urlutils.ExtIs(a.Proxy, imageExts) {
|
||||
r.append(EmbedAttachment(r.buf.Len(), a))
|
||||
return
|
||||
}
|
||||
|
||||
start, end := r.writeStringf(
|
||||
"File: %s (%s)",
|
||||
a.Filename, humanize.Bytes(a.Size),
|
||||
)
|
||||
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
a.URL,
|
||||
})
|
||||
}
|
||||
|
||||
type AvatarSegment struct {
|
||||
start int
|
||||
url string
|
||||
text string
|
||||
size int
|
||||
}
|
||||
|
||||
func EmbedAuthor(start int, a discord.EmbedAuthor) AvatarSegment {
|
||||
return AvatarSegment{
|
||||
start: start,
|
||||
url: a.ProxyIcon,
|
||||
text: "Avatar",
|
||||
}
|
||||
}
|
||||
|
||||
// EmbedFooter uses an avatar segment to comply with Discord.
|
||||
func EmbedFooter(start int, f discord.EmbedFooter) AvatarSegment {
|
||||
return AvatarSegment{
|
||||
start: start,
|
||||
url: f.ProxyIcon,
|
||||
text: "Icon",
|
||||
}
|
||||
}
|
||||
|
||||
func (a AvatarSegment) Bounds() (int, int) {
|
||||
return a.start, a.start
|
||||
}
|
||||
|
||||
// Avatar returns the avatar URL.
|
||||
func (a AvatarSegment) Avatar() (url string) {
|
||||
return a.url
|
||||
}
|
||||
|
||||
// AvatarSize returns the size of a small emoji.
|
||||
func (a AvatarSegment) AvatarSize() int {
|
||||
if a.size > 0 {
|
||||
return a.size
|
||||
}
|
||||
return InlineEmojiSize
|
||||
}
|
||||
|
||||
func (a AvatarSegment) AvatarText() string {
|
||||
return a.text
|
||||
}
|
||||
|
||||
type ImageSegment struct {
|
||||
start int
|
||||
url string
|
||||
w, h int
|
||||
text string
|
||||
}
|
||||
|
||||
func EmbedImage(start int, i discord.EmbedImage) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: i.Proxy,
|
||||
w: int(i.Width),
|
||||
h: int(i.Height),
|
||||
text: fmt.Sprintf("Image (%s)", urlutils.Name(i.URL)),
|
||||
}
|
||||
}
|
||||
|
||||
func EmbedThumbnail(start int, t discord.EmbedThumbnail) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: t.Proxy,
|
||||
w: int(t.Width),
|
||||
h: int(t.Height),
|
||||
text: fmt.Sprintf("Thumbnail (%s)", urlutils.Name(t.URL)),
|
||||
}
|
||||
}
|
||||
|
||||
func EmbedAttachment(start int, a discord.Attachment) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: a.Proxy,
|
||||
w: int(a.Width),
|
||||
h: int(a.Height),
|
||||
text: fmt.Sprintf("%s (%s)", a.Filename, humanize.Bytes(a.Size)),
|
||||
}
|
||||
}
|
||||
|
||||
func (i ImageSegment) Bounds() (start, end int) {
|
||||
return i.start, i.start
|
||||
}
|
||||
|
||||
// Image returns the URL.
|
||||
func (i ImageSegment) Image() string {
|
||||
return i.url
|
||||
}
|
||||
|
||||
func (i ImageSegment) ImageSize() (w, h int) {
|
||||
return i.w, i.h
|
||||
}
|
||||
|
||||
func (i ImageSegment) ImageText() string {
|
||||
return i.text
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package embed
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/emoji"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
)
|
||||
|
||||
type AvatarSegment struct {
|
||||
empty.TextSegment
|
||||
start int
|
||||
url string
|
||||
text string
|
||||
size int
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Avatarer = (*AvatarSegment)(nil)
|
||||
_ text.Segment = (*AvatarSegment)(nil)
|
||||
)
|
||||
|
||||
func Author(start int, a discord.EmbedAuthor) AvatarSegment {
|
||||
return AvatarSegment{
|
||||
start: start,
|
||||
url: a.ProxyIcon,
|
||||
text: "Avatar",
|
||||
}
|
||||
}
|
||||
|
||||
// Footer uses an avatar segment to comply with Discord.
|
||||
func Footer(start int, f discord.EmbedFooter) AvatarSegment {
|
||||
return AvatarSegment{
|
||||
start: start,
|
||||
url: f.ProxyIcon,
|
||||
text: "Icon",
|
||||
}
|
||||
}
|
||||
|
||||
func (a AvatarSegment) Bounds() (int, int) {
|
||||
return a.start, a.start
|
||||
}
|
||||
|
||||
func (a AvatarSegment) AsAvatarer() text.Avatarer {
|
||||
return a
|
||||
}
|
||||
|
||||
// Avatar returns the avatar URL.
|
||||
func (a AvatarSegment) Avatar() (url string) {
|
||||
return a.url
|
||||
}
|
||||
|
||||
// AvatarSize returns the size of a small emoji.
|
||||
func (a AvatarSegment) AvatarSize() int {
|
||||
if a.size > 0 {
|
||||
return a.size
|
||||
}
|
||||
return emoji.InlineSize
|
||||
}
|
||||
|
||||
func (a AvatarSegment) AvatarText() string {
|
||||
return a.text
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package embed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/inline"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/link"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
var imageExts = []string{".jpg", ".jpeg", ".png", ".webp", ".gif"}
|
||||
|
||||
func writeEmbedSep(r *renderer.Text, embedColor discord.Color) {
|
||||
if start, end := r.WriteString("---"); embedColor > 0 {
|
||||
r.Append(colored.NewSegment(start, end, embedColor.Uint32()))
|
||||
}
|
||||
}
|
||||
|
||||
func RenderEmbeds(r *renderer.Text, embeds []discord.Embed, m *discord.Message, s state.Store) {
|
||||
for _, embed := range embeds {
|
||||
r.StartBlock()
|
||||
writeEmbedSep(r, embed.Color)
|
||||
r.EnsureBreak()
|
||||
|
||||
RenderEmbed(r, embed, m, s)
|
||||
|
||||
r.EnsureBreak()
|
||||
writeEmbedSep(r, embed.Color) // render prepends newline already
|
||||
r.EndBlock()
|
||||
}
|
||||
}
|
||||
|
||||
func RenderEmbed(r *renderer.Text, embed discord.Embed, m *discord.Message, s state.Store) {
|
||||
if a := embed.Author; a != nil && a.Name != "" {
|
||||
if a.ProxyIcon != "" {
|
||||
r.Append(Author(r.Buffer.Len(), *a))
|
||||
r.Buffer.WriteByte(' ')
|
||||
}
|
||||
|
||||
start, end := r.WriteString(a.Name)
|
||||
r.EnsureBreak()
|
||||
|
||||
if a.URL != "" {
|
||||
r.Append(link.NewSegment(start, end, a.URL))
|
||||
}
|
||||
}
|
||||
|
||||
if embed.Title != "" {
|
||||
start, end := r.WriteString(embed.Title)
|
||||
r.EnsureBreak()
|
||||
|
||||
// Make the title bold.
|
||||
r.Append(inline.NewSegment(start, end, text.AttributeBold))
|
||||
|
||||
if embed.URL != "" {
|
||||
r.Append(link.NewSegment(start, end, embed.URL))
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a thumbnail, then write one.
|
||||
if embed.Thumbnail != nil {
|
||||
r.Append(Thumbnail(r.Buffer.Len(), *embed.Thumbnail))
|
||||
// Guarantee 2 lines because thumbnail needs its own.
|
||||
r.StartBlockN(2)
|
||||
}
|
||||
|
||||
if embed.Description != "" {
|
||||
// Since Discord embeds' descriptions are technically Markdown, we can
|
||||
// borrow our Markdown parser for this.
|
||||
node := md.ParseWithMessage([]byte(embed.Description), s, m, false)
|
||||
// Create a new renderer with inherited state and buffer but a new byte
|
||||
// source.
|
||||
desc := r.Clone([]byte(embed.Description))
|
||||
// Walk using the newly created state.
|
||||
desc.Walk(node)
|
||||
// Join the created state.
|
||||
r.Join(desc)
|
||||
// Write a new line.
|
||||
r.EnsureBreak()
|
||||
}
|
||||
|
||||
if len(embed.Fields) > 0 {
|
||||
// Pad two new lines.
|
||||
r.StartBlockN(2)
|
||||
|
||||
// Write fields indented once.
|
||||
for _, field := range embed.Fields {
|
||||
fmt.Fprintf(r.Buffer, "\t%s: %s\n", field.Name, field.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if f := embed.Footer; f != nil && f.Text != "" {
|
||||
if f.ProxyIcon != "" {
|
||||
r.Append(Footer(r.Buffer.Len(), *f))
|
||||
r.Buffer.WriteByte(' ')
|
||||
}
|
||||
|
||||
r.Buffer.WriteString(f.Text)
|
||||
r.EnsureBreak()
|
||||
}
|
||||
|
||||
if embed.Timestamp.IsValid() {
|
||||
if embed.Footer != nil {
|
||||
r.Buffer.WriteString(" - ")
|
||||
}
|
||||
|
||||
r.Buffer.WriteString(embed.Timestamp.Format(time.RFC1123))
|
||||
r.EnsureBreak()
|
||||
}
|
||||
|
||||
// Write an image if there's one.
|
||||
if embed.Image != nil {
|
||||
r.Append(Image(r.Buffer.Len(), *embed.Image))
|
||||
// Images take up its own empty line, so we should guarantee 2 empty
|
||||
// lines.
|
||||
r.StartBlockN(2)
|
||||
}
|
||||
}
|
||||
|
||||
func RenderAttachments(r *renderer.Text, attachments []discord.Attachment) {
|
||||
// Don't do anything if there are no attachments.
|
||||
if len(attachments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Start a (small)new block before rendering attachments.
|
||||
r.EnsureBreak()
|
||||
|
||||
// Render all attachments. Newline delimited.
|
||||
for i, attachment := range attachments {
|
||||
RenderAttachment(r, attachment)
|
||||
|
||||
if i != len(attachments) {
|
||||
r.Buffer.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RenderAttachment(r *renderer.Text, a discord.Attachment) {
|
||||
if urlutils.ExtIs(a.Proxy, imageExts) {
|
||||
r.Append(Attachment(r.Buffer.Len(), a))
|
||||
return
|
||||
}
|
||||
|
||||
start, end := r.WriteStringf(
|
||||
"File: %s (%s)",
|
||||
a.Filename, humanize.Bytes(a.Size),
|
||||
)
|
||||
|
||||
r.Append(link.NewSegment(start, end, a.URL))
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package embed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
type ImageSegment struct {
|
||||
empty.TextSegment
|
||||
start int
|
||||
url string
|
||||
w, h int
|
||||
text string
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Imager = (*ImageSegment)(nil)
|
||||
_ text.Segment = (*ImageSegment)(nil)
|
||||
)
|
||||
|
||||
func Image(start int, i discord.EmbedImage) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: i.Proxy,
|
||||
w: int(i.Width),
|
||||
h: int(i.Height),
|
||||
text: fmt.Sprintf("Image (%s)", urlutils.Name(i.URL)),
|
||||
}
|
||||
}
|
||||
|
||||
func Thumbnail(start int, t discord.EmbedThumbnail) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: t.Proxy,
|
||||
w: int(t.Width),
|
||||
h: int(t.Height),
|
||||
text: fmt.Sprintf("Thumbnail (%s)", urlutils.Name(t.URL)),
|
||||
}
|
||||
}
|
||||
|
||||
func Attachment(start int, a discord.Attachment) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: a.Proxy,
|
||||
w: int(a.Width),
|
||||
h: int(a.Height),
|
||||
text: fmt.Sprintf("%s (%s)", a.Filename, humanize.Bytes(a.Size)),
|
||||
}
|
||||
}
|
||||
|
||||
func (i ImageSegment) Bounds() (start, end int) {
|
||||
return i.start, i.start
|
||||
}
|
||||
|
||||
func (i ImageSegment) AsImager() text.Imager {
|
||||
return i
|
||||
}
|
||||
|
||||
// Image returns the URL.
|
||||
func (i ImageSegment) Image() string {
|
||||
return i.url
|
||||
}
|
||||
|
||||
func (i ImageSegment) ImageSize() (w, h int) {
|
||||
return i.w, i.h
|
||||
}
|
||||
|
||||
func (i ImageSegment) ImageText() string {
|
||||
return i.text
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
const (
|
||||
InlineEmojiSize = 22
|
||||
LargeEmojiSize = 48
|
||||
)
|
||||
|
||||
type EmojiSegment struct {
|
||||
Start int
|
||||
Name string
|
||||
EmojiURL string
|
||||
Large bool
|
||||
}
|
||||
|
||||
var _ text.Imager = (*EmojiSegment)(nil)
|
||||
|
||||
func (r *TextRenderer) emoji(n *md.Emoji, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
r.append(EmojiSegment{
|
||||
Start: r.buf.Len(),
|
||||
Name: n.Name,
|
||||
Large: n.Large,
|
||||
EmojiURL: n.EmojiURL() + "&size=64",
|
||||
})
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func (e EmojiSegment) Bounds() (start, end int) {
|
||||
return e.Start, e.Start
|
||||
}
|
||||
|
||||
func (e EmojiSegment) Image() string {
|
||||
return e.EmojiURL
|
||||
}
|
||||
|
||||
// TODO: large emoji
|
||||
|
||||
func (e EmojiSegment) ImageSize() (w, h int) {
|
||||
if e.Large {
|
||||
return LargeEmojiSize, LargeEmojiSize
|
||||
}
|
||||
return InlineEmojiSize, InlineEmojiSize
|
||||
}
|
||||
|
||||
func (e EmojiSegment) ImageText() string {
|
||||
return ":" + e.Name + ":"
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package emoji
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func init() {
|
||||
renderer.Register(md.KindEmoji, emoji)
|
||||
}
|
||||
|
||||
func emoji(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*md.Emoji)
|
||||
|
||||
if enter {
|
||||
r.Append(Segment{
|
||||
Start: r.Buffer.Len(),
|
||||
Emoji: EmojiFromNode(n),
|
||||
})
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
const (
|
||||
InlineSize = 22
|
||||
LargeSize = 48
|
||||
)
|
||||
|
||||
type Emoji struct {
|
||||
Name string
|
||||
EmojiURL string
|
||||
Large bool
|
||||
}
|
||||
|
||||
var _ text.Imager = (*Emoji)(nil)
|
||||
|
||||
func injectSizeURL(fullURL string) string {
|
||||
u, err := url.Parse(fullURL)
|
||||
if err != nil {
|
||||
return fullURL
|
||||
}
|
||||
|
||||
v := u.Query()
|
||||
v.Set("size", "64")
|
||||
|
||||
u.RawQuery = v.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func EmojiFromNode(n *md.Emoji) Emoji {
|
||||
return Emoji{
|
||||
Name: n.Name,
|
||||
Large: n.Large,
|
||||
EmojiURL: injectSizeURL(n.EmojiURL()),
|
||||
}
|
||||
}
|
||||
|
||||
func EmojiFromDiscord(e discord.Emoji, large bool) Emoji {
|
||||
return Emoji{
|
||||
Name: e.Name,
|
||||
EmojiURL: injectSizeURL(e.EmojiURL()),
|
||||
Large: large,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Emoji) Image() string {
|
||||
return e.EmojiURL
|
||||
}
|
||||
|
||||
func (e Emoji) ImageSize() (w, h int) {
|
||||
if e.Large {
|
||||
return LargeSize, LargeSize
|
||||
}
|
||||
return InlineSize, InlineSize
|
||||
}
|
||||
|
||||
func (e Emoji) ImageText() string {
|
||||
return ":" + e.Name + ":"
|
||||
}
|
||||
|
||||
type Segment struct {
|
||||
empty.TextSegment
|
||||
Start int
|
||||
Emoji Emoji
|
||||
}
|
||||
|
||||
var _ text.Segment = (*Segment)(nil)
|
||||
|
||||
func (e Segment) Bounds() (start, end int) {
|
||||
return e.Start, e.Start
|
||||
}
|
||||
|
||||
func (e Segment) AsImager() text.Imager { return e.Emoji }
|
|
@ -0,0 +1,113 @@
|
|||
package inline
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func init() {
|
||||
renderer.Register(md.KindInline, inline)
|
||||
}
|
||||
|
||||
// inline parses an inline node. This method at the moment will always create a
|
||||
// new segment for overlapping attributes.
|
||||
func inline(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*md.Inline)
|
||||
// For instructions on how this works, refer to inline_attr.jpg.
|
||||
|
||||
// Pop the last segment if it's not empty.
|
||||
if !r.Inlines.Empty() {
|
||||
r.Inlines.End = r.Buffer.Len()
|
||||
|
||||
// Only use this section if the length is not zero.
|
||||
if r.Inlines.Start != r.Inlines.End {
|
||||
r.Append(NewSegmentFromState(r.Inlines))
|
||||
}
|
||||
}
|
||||
|
||||
if enter {
|
||||
r.Inlines.Add(n.Attr)
|
||||
} else {
|
||||
r.Inlines.Remove(n.Attr)
|
||||
}
|
||||
|
||||
// Update the start pointer of the current segment.
|
||||
r.Inlines.Start = r.Buffer.Len()
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
type Attribute text.Attribute
|
||||
|
||||
var _ text.Attributor = (*Attribute)(nil)
|
||||
|
||||
func (attr Attribute) Attribute() text.Attribute {
|
||||
return text.Attribute(attr)
|
||||
}
|
||||
|
||||
type Segment struct {
|
||||
empty.TextSegment
|
||||
start, end int
|
||||
attributes Attribute
|
||||
}
|
||||
|
||||
// NewSegmentFromState creates a new rich text segment from the renderer's
|
||||
// inline attribute state.
|
||||
func NewSegmentFromState(state renderer.InlineState) Segment {
|
||||
return NewSegmentFromMD(state.Start, state.End, state.Attributes)
|
||||
}
|
||||
|
||||
// NewSegmentFromMD creates a new rich text segment from the start, end indices
|
||||
// and the markdown inline attributes.
|
||||
func NewSegmentFromMD(start, end int, attr md.Attribute) Segment {
|
||||
var seg = Segment{
|
||||
start: start,
|
||||
end: end,
|
||||
}
|
||||
|
||||
if attr.Has(md.AttrBold) {
|
||||
seg.attributes |= Attribute(text.AttributeBold)
|
||||
}
|
||||
if attr.Has(md.AttrItalics) {
|
||||
seg.attributes |= Attribute(text.AttributeItalics)
|
||||
}
|
||||
if attr.Has(md.AttrUnderline) {
|
||||
seg.attributes |= Attribute(text.AttributeUnderline)
|
||||
}
|
||||
if attr.Has(md.AttrStrikethrough) {
|
||||
seg.attributes |= Attribute(text.AttributeStrikethrough)
|
||||
}
|
||||
if attr.Has(md.AttrSpoiler) {
|
||||
seg.attributes |= Attribute(text.AttributeSpoiler)
|
||||
}
|
||||
if attr.Has(md.AttrMonospace) {
|
||||
seg.attributes |= Attribute(text.AttributeMonospace)
|
||||
}
|
||||
|
||||
return seg
|
||||
}
|
||||
|
||||
func NewSegment(start, end int, attrs ...text.Attribute) Segment {
|
||||
var attr = text.AttributeNormal
|
||||
for _, a := range attrs {
|
||||
attr |= a
|
||||
}
|
||||
return Segment{
|
||||
start: start,
|
||||
end: end,
|
||||
attributes: Attribute(attr),
|
||||
}
|
||||
}
|
||||
|
||||
var _ text.Segment = (*Segment)(nil)
|
||||
|
||||
func (i Segment) Bounds() (start, end int) {
|
||||
return i.start, i.end
|
||||
}
|
||||
|
||||
func (i Segment) AsAttributor() text.Attributor {
|
||||
return i.attributes
|
||||
}
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
|
@ -1,104 +0,0 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
type inlineState struct {
|
||||
// TODO: use a stack to allow overlapping
|
||||
InlineSegment
|
||||
}
|
||||
|
||||
func (i *inlineState) add(attr md.Attribute) {
|
||||
if attr.Has(md.AttrBold) {
|
||||
i.attributes |= text.AttrBold
|
||||
}
|
||||
if attr.Has(md.AttrItalics) {
|
||||
i.attributes |= text.AttrItalics
|
||||
}
|
||||
if attr.Has(md.AttrUnderline) {
|
||||
i.attributes |= text.AttrUnderline
|
||||
}
|
||||
if attr.Has(md.AttrStrikethrough) {
|
||||
i.attributes |= text.AttrStrikethrough
|
||||
}
|
||||
if attr.Has(md.AttrSpoiler) {
|
||||
i.attributes |= text.AttrSpoiler
|
||||
}
|
||||
if attr.Has(md.AttrMonospace) {
|
||||
i.attributes |= text.AttrMonospace
|
||||
}
|
||||
}
|
||||
|
||||
func (i *inlineState) remove(attr md.Attribute) {
|
||||
if attr.Has(md.AttrBold) {
|
||||
i.attributes &= ^text.AttrBold
|
||||
}
|
||||
if attr.Has(md.AttrItalics) {
|
||||
i.attributes &= ^text.AttrItalics
|
||||
}
|
||||
if attr.Has(md.AttrUnderline) {
|
||||
i.attributes &= ^text.AttrUnderline
|
||||
}
|
||||
if attr.Has(md.AttrStrikethrough) {
|
||||
i.attributes &= ^text.AttrStrikethrough
|
||||
}
|
||||
if attr.Has(md.AttrSpoiler) {
|
||||
i.attributes &= ^text.AttrSpoiler
|
||||
}
|
||||
if attr.Has(md.AttrMonospace) {
|
||||
i.attributes &= ^text.AttrMonospace
|
||||
}
|
||||
}
|
||||
|
||||
func (i inlineState) copy() InlineSegment {
|
||||
return i.InlineSegment
|
||||
}
|
||||
|
||||
type InlineSegment struct {
|
||||
start, end int
|
||||
attributes text.Attribute
|
||||
}
|
||||
|
||||
var _ text.Attributor = (*InlineSegment)(nil)
|
||||
|
||||
// inline parses an inline node. This method at the moment will always create a
|
||||
// new segment for overlapping attributes.
|
||||
func (r *TextRenderer) inline(n *md.Inline, enter bool) ast.WalkStatus {
|
||||
// For instructions on how this works, refer to inline_attr.jpg.
|
||||
|
||||
// Pop the last segment if it's not empty.
|
||||
if !r.inls.empty() {
|
||||
r.inls.end = r.buf.Len()
|
||||
|
||||
// Only use this section if the length is not zero.
|
||||
if r.inls.start != r.inls.end {
|
||||
r.append(r.inls.copy())
|
||||
}
|
||||
}
|
||||
|
||||
if enter {
|
||||
r.inls.add(n.Attr)
|
||||
} else {
|
||||
r.inls.remove(n.Attr)
|
||||
}
|
||||
|
||||
// Update the start pointer of the current segment.
|
||||
r.inls.start = r.buf.Len()
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func (i InlineSegment) Bounds() (start, end int) {
|
||||
return i.start, i.end
|
||||
}
|
||||
|
||||
func (i InlineSegment) Attribute() text.Attribute {
|
||||
return i.attributes
|
||||
}
|
||||
|
||||
func (i InlineSegment) empty() bool {
|
||||
return i.attributes == 0 || i.start < i.end
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// linkState is used for ast.Link segments.
|
||||
type linkState struct {
|
||||
linkstack []int // stack of starting integers
|
||||
}
|
||||
|
||||
type LinkSegment struct {
|
||||
start, end int
|
||||
url string
|
||||
}
|
||||
|
||||
var _ text.Linker = (*LinkSegment)(nil)
|
||||
|
||||
func (r *TextRenderer) link(n *ast.Link, enter bool) ast.WalkStatus {
|
||||
// If we're entering the link node, then add the starting point to the stack
|
||||
// and move on.
|
||||
if enter {
|
||||
r.lnks.linkstack = append(r.lnks.linkstack, r.buf.Len())
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
// If there's nothing in the stack, then don't do anything. This shouldn't
|
||||
// happen.
|
||||
if len(r.lnks.linkstack) == 0 {
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
// We're exiting the link node. Pop the segment off the stack.
|
||||
ilast := len(r.lnks.linkstack) - 1
|
||||
start := r.lnks.linkstack[ilast]
|
||||
r.lnks.linkstack = r.lnks.linkstack[:ilast]
|
||||
|
||||
// Close the segment on enter false.
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
r.buf.Len(),
|
||||
string(n.Destination),
|
||||
})
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func (r *TextRenderer) autoLink(n *ast.AutoLink, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
start, end := r.write(n.URL(r.src))
|
||||
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
string(n.URL((r.src))),
|
||||
})
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func (l LinkSegment) Bounds() (start, end int) {
|
||||
return l.start, l.end
|
||||
}
|
||||
|
||||
func (l LinkSegment) Link() (url string) {
|
||||
return l.url
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package link
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func init() {
|
||||
renderer.Register(ast.KindLink, link)
|
||||
renderer.Register(ast.KindAutoLink, autoLink)
|
||||
}
|
||||
|
||||
func link(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*ast.Link)
|
||||
|
||||
// If we're entering the link node, then add the starting point to the stack
|
||||
// and move on.
|
||||
if enter {
|
||||
r.Links.Append(r.Buffer.Len())
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
// If there's nothing in the stack, then don't do anything. This shouldn't
|
||||
// happen.
|
||||
if r.Links.Len() == 0 {
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
// We're exiting the link node. Pop the segment off the stack.
|
||||
start := r.Links.Pop()
|
||||
|
||||
// Close the segment on enter false.
|
||||
r.Append(NewSegment(start, r.Buffer.Len(), string(n.Destination)))
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func autoLink(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*ast.AutoLink)
|
||||
|
||||
if enter {
|
||||
start, end := r.Write(n.URL(r.Source))
|
||||
r.Append(NewSegment(start, end, string(n.URL(r.Source))))
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
type URL string
|
||||
|
||||
var _ text.Linker = (*URL)(nil)
|
||||
|
||||
func (u URL) Link() string { return string(u) }
|
||||
|
||||
type Segment struct {
|
||||
empty.TextSegment
|
||||
start int
|
||||
end int
|
||||
url URL
|
||||
}
|
||||
|
||||
var _ text.Segment = (*Segment)(nil)
|
||||
|
||||
func NewSegment(start, end int, url string) Segment {
|
||||
return Segment{
|
||||
start: start,
|
||||
end: end,
|
||||
url: URL(url),
|
||||
}
|
||||
}
|
||||
|
||||
func (l Segment) Bounds() (start, end int) {
|
||||
return l.start, l.end
|
||||
}
|
||||
|
||||
func (l Segment) AsLinker() text.Linker {
|
||||
return l.url
|
||||
}
|
|
@ -1,259 +1,34 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/embed"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func Write(rich *text.Rich, content string, segs ...text.Segment) (start, end int) {
|
||||
start = len(rich.Content)
|
||||
end = len(rich.Content) + len(content)
|
||||
rich.Content += content
|
||||
return
|
||||
}
|
||||
|
||||
func ParseMessage(m *discord.Message, s state.Store) text.Rich {
|
||||
var content = []byte(m.Content)
|
||||
var node = md.ParseWithMessage(content, s, m, true)
|
||||
|
||||
r := NewTextReader(content, node)
|
||||
r := renderer.New(content, node)
|
||||
// Register the needed states for some renderers.
|
||||
r.WithState(m, s)
|
||||
// Render the main message body.
|
||||
r.walk(node)
|
||||
r.Walk(node)
|
||||
// Render the extra bits.
|
||||
r.renderAttachments(m.Attachments)
|
||||
r.renderEmbeds(m.Embeds, m, s)
|
||||
embed.RenderAttachments(r, m.Attachments)
|
||||
embed.RenderEmbeds(r, m.Embeds, m, s)
|
||||
|
||||
return text.Rich{
|
||||
Content: r.String(),
|
||||
Segments: r.segs,
|
||||
Segments: r.Segments,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseWithMessage(b []byte, m *discord.Message, s state.Store, msg bool) text.Rich {
|
||||
node := md.ParseWithMessage(b, s, m, msg)
|
||||
return RenderNode(b, node)
|
||||
}
|
||||
|
||||
func Parse(b []byte) text.Rich {
|
||||
node := md.Parse(b)
|
||||
return RenderNode(b, node)
|
||||
}
|
||||
|
||||
func RenderNode(source []byte, n ast.Node) text.Rich {
|
||||
r := NewTextReader(source, n)
|
||||
r.walk(n)
|
||||
|
||||
return text.Rich{
|
||||
Content: r.String(),
|
||||
Segments: r.segs,
|
||||
}
|
||||
}
|
||||
|
||||
type TextRenderer struct {
|
||||
buf *bytes.Buffer
|
||||
src []byte
|
||||
segs []text.Segment
|
||||
inls inlineState
|
||||
lnks linkState
|
||||
|
||||
// these fields can be nil
|
||||
msg *discord.Message
|
||||
store state.Store
|
||||
}
|
||||
|
||||
func NewTextReader(src []byte, node ast.Node) TextRenderer {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(len(src))
|
||||
|
||||
return TextRenderer{
|
||||
src: src,
|
||||
buf: buf,
|
||||
segs: make([]text.Segment, 0, node.ChildCount()),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) WithState(m *discord.Message, s state.Store) {
|
||||
r.msg = m
|
||||
r.store = s
|
||||
}
|
||||
|
||||
// String returns a stringified version of Bytes().
|
||||
func (r *TextRenderer) String() string {
|
||||
return string(r.Bytes())
|
||||
}
|
||||
|
||||
// Bytes returns the plain content of the buffer with right spaces trimmed as
|
||||
// best as it could, that is, the function will not trim right spaces that
|
||||
// segments use.
|
||||
func (r *TextRenderer) Bytes() []byte {
|
||||
// Get the rightmost index out of all the segments.
|
||||
var rightmost int
|
||||
for _, seg := range r.segs {
|
||||
if _, end := seg.Bounds(); end > rightmost {
|
||||
rightmost = end
|
||||
}
|
||||
}
|
||||
|
||||
// Get the original byte slice.
|
||||
org := r.buf.Bytes()
|
||||
|
||||
// Trim the right spaces.
|
||||
trbuf := bytes.TrimRight(org, "\n")
|
||||
|
||||
// If we trimmed way too far, then slice so that we get as far as the
|
||||
// rightmost segment.
|
||||
if len(trbuf) < rightmost {
|
||||
return org[:rightmost]
|
||||
}
|
||||
|
||||
// Else, we're safe returning the trimmed slice.
|
||||
return trbuf
|
||||
}
|
||||
|
||||
func (r *TextRenderer) writeStringf(f string, v ...interface{}) (start, end int) {
|
||||
return r.writeString(fmt.Sprintf(f, v...))
|
||||
}
|
||||
|
||||
func (r *TextRenderer) writeString(s string) (start, end int) {
|
||||
return writestringbuf(r.buf, s)
|
||||
}
|
||||
|
||||
func (r *TextRenderer) write(b []byte) (start, end int) {
|
||||
return writebuf(r.buf, b)
|
||||
}
|
||||
|
||||
// startBlock guarantees enough indentation to start a new block.
|
||||
func (r *TextRenderer) startBlock() {
|
||||
r.startBlockN(2)
|
||||
}
|
||||
|
||||
// ensureBreak ensures that the current line is a new line.
|
||||
func (r *TextRenderer) ensureBreak() {
|
||||
r.startBlockN(1)
|
||||
}
|
||||
|
||||
// startBlockN allows a custom block level.
|
||||
func (r *TextRenderer) startBlockN(n int) {
|
||||
var maxNewlines = 0
|
||||
|
||||
// Peek twice. If the last character is already a new line or we're only at
|
||||
// the start of line (length 0), then don't pad.
|
||||
if r.buf.Len() > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
if r.peekLast(i) != '\n' {
|
||||
maxNewlines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the padding.
|
||||
r.buf.Grow(maxNewlines)
|
||||
for i := 0; i < maxNewlines; i++ {
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) endBlock() {
|
||||
// Do the same thing as starting a block.
|
||||
r.startBlock()
|
||||
}
|
||||
|
||||
// peekLast returns the previous byte that matches the offset, or 0 if the
|
||||
// offset goes past the first byte.
|
||||
func (r *TextRenderer) peekLast(offset int) byte {
|
||||
if i := r.buf.Len() - offset - 1; i > 0 {
|
||||
return r.buf.Bytes()[i]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *TextRenderer) append(segs ...text.Segment) {
|
||||
r.segs = append(r.segs, segs...)
|
||||
}
|
||||
|
||||
// clone returns a shallow copy of TextRenderer with the new source.
|
||||
func (r *TextRenderer) clone(src []byte) *TextRenderer {
|
||||
cpy := *r
|
||||
cpy.src = src
|
||||
return &cpy
|
||||
}
|
||||
|
||||
// join combines the states from renderer with r. Use this with clone.
|
||||
func (r *TextRenderer) join(renderer *TextRenderer) {
|
||||
r.segs = renderer.segs
|
||||
r.inls = renderer.inls
|
||||
}
|
||||
|
||||
func (r *TextRenderer) walk(n ast.Node) {
|
||||
ast.Walk(n, r.renderNode)
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderNode(n ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
switch n := n.(type) {
|
||||
case *ast.Document:
|
||||
case *ast.Paragraph:
|
||||
if !enter {
|
||||
// TODO: investigate
|
||||
// r.buf.WriteByte('\n')
|
||||
}
|
||||
case *ast.Blockquote:
|
||||
return r.blockquote(n, enter), nil
|
||||
case *ast.FencedCodeBlock:
|
||||
return r.codeblock(n, enter), nil
|
||||
case *ast.Link:
|
||||
return r.link(n, enter), nil
|
||||
case *ast.AutoLink:
|
||||
return r.autoLink(n, enter), nil
|
||||
case *md.Inline:
|
||||
return r.inline(n, enter), nil
|
||||
case *md.Emoji:
|
||||
return r.emoji(n, enter), nil
|
||||
case *md.Mention:
|
||||
return r.mention(n, enter), nil
|
||||
case *ast.String:
|
||||
if enter {
|
||||
r.buf.Write(n.Value)
|
||||
}
|
||||
case *ast.Text:
|
||||
if enter {
|
||||
r.buf.Write(n.Segment.Value(r.src))
|
||||
|
||||
switch {
|
||||
case n.HardLineBreak():
|
||||
r.buf.WriteString("\n\n")
|
||||
case n.SoftLineBreak():
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// helper global functions
|
||||
|
||||
func writebuf(w *bytes.Buffer, b []byte) (start, end int) {
|
||||
start = w.Len()
|
||||
w.Write(b)
|
||||
end = w.Len()
|
||||
return start, end
|
||||
}
|
||||
|
||||
func writestringbuf(w *bytes.Buffer, b string) (start, end int) {
|
||||
start = w.Len()
|
||||
w.WriteString(b)
|
||||
end = w.Len()
|
||||
return start, end
|
||||
}
|
||||
|
||||
func segmentadd(r *text.Rich, seg ...text.Segment) {
|
||||
r.Segments = append(r.Segments, seg...)
|
||||
return renderer.RenderNode(b, node)
|
||||
}
|
||||
|
|
|
@ -1,393 +0,0 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// NameSegment represents a clickable member name; it does not implement colors.
|
||||
type NameSegment struct {
|
||||
start, end int
|
||||
|
||||
guild discord.Guild
|
||||
member discord.Member
|
||||
state *ningen.State // optional
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Segment = (*NameSegment)(nil)
|
||||
_ text.Mentioner = (*NameSegment)(nil)
|
||||
_ text.MentionerAvatar = (*NameSegment)(nil)
|
||||
)
|
||||
|
||||
func UserSegment(start, end int, u discord.User) NameSegment {
|
||||
return NameSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
member: discord.Member{User: u},
|
||||
}
|
||||
}
|
||||
|
||||
func MemberSegment(start, end int, guild discord.Guild, m discord.Member) NameSegment {
|
||||
return NameSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
guild: guild,
|
||||
member: m,
|
||||
}
|
||||
}
|
||||
|
||||
// WithState assigns a ningen state into the given name segment. This allows the
|
||||
// popovers to have additional information such as user notes.
|
||||
func (m *NameSegment) WithState(state *ningen.State) {
|
||||
m.state = state
|
||||
}
|
||||
|
||||
func (m NameSegment) Bounds() (start, end int) {
|
||||
return m.start, m.end
|
||||
}
|
||||
|
||||
func (m NameSegment) MentionInfo() text.Rich {
|
||||
return userInfo(m.guild, m.member, m.state)
|
||||
}
|
||||
|
||||
// Avatar returns the large avatar URL.
|
||||
func (m NameSegment) Avatar() string {
|
||||
return m.member.User.AvatarURL()
|
||||
}
|
||||
|
||||
type MentionSegment struct {
|
||||
start, end int
|
||||
*md.Mention
|
||||
|
||||
store state.Store
|
||||
guild discord.GuildID
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Segment = (*MentionSegment)(nil)
|
||||
_ text.Colorer = (*MentionSegment)(nil)
|
||||
_ text.Mentioner = (*MentionSegment)(nil)
|
||||
_ text.MentionerAvatar = (*MentionSegment)(nil)
|
||||
)
|
||||
|
||||
func (r *TextRenderer) mention(n *md.Mention, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
var seg = MentionSegment{
|
||||
Mention: n,
|
||||
store: r.store,
|
||||
guild: r.msg.GuildID,
|
||||
}
|
||||
|
||||
switch {
|
||||
case n.Channel != nil:
|
||||
seg.start, seg.end = r.writeString("#" + n.Channel.Name)
|
||||
case n.GuildUser != nil:
|
||||
seg.start, seg.end = r.writeString("@" + n.GuildUser.Username)
|
||||
case n.GuildRole != nil:
|
||||
seg.start, seg.end = r.writeString("@" + n.GuildRole.Name)
|
||||
default:
|
||||
// Unexpected error; skip.
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
r.append(seg)
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
func (m MentionSegment) Bounds() (start, end int) {
|
||||
return m.start, m.end
|
||||
}
|
||||
|
||||
// Color tries to return the color of the mention segment, or it returns the
|
||||
// usual blurple if none.
|
||||
func (m MentionSegment) Color() (color uint32) {
|
||||
// Try digging through what we have for a color.
|
||||
switch {
|
||||
case m.GuildUser != nil && m.GuildUser.Member != nil:
|
||||
g, err := m.store.Guild(m.guild)
|
||||
if err != nil {
|
||||
return blurple
|
||||
}
|
||||
|
||||
color = discord.MemberColor(*g, *m.GuildUser.Member).Uint32()
|
||||
|
||||
case m.GuildRole != nil && m.GuildRole.Color > 0:
|
||||
color = m.GuildRole.Color.Uint32()
|
||||
}
|
||||
|
||||
if color > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
return blurple
|
||||
}
|
||||
|
||||
// TODO
|
||||
func (m MentionSegment) MentionInfo() text.Rich {
|
||||
switch {
|
||||
case m.Channel != nil:
|
||||
return m.channelInfo()
|
||||
case m.GuildUser != nil:
|
||||
return m.userInfo()
|
||||
case m.GuildRole != nil:
|
||||
return m.roleInfo()
|
||||
}
|
||||
|
||||
// Unknown; return an empty text.
|
||||
return text.Rich{}
|
||||
}
|
||||
|
||||
// Avatar returns the user avatar if any, else it returns an empty URL.
|
||||
func (m MentionSegment) Avatar() string {
|
||||
if m.GuildUser != nil {
|
||||
return m.GuildUser.AvatarURL()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m MentionSegment) channelInfo() text.Rich {
|
||||
var topic = m.Channel.Topic
|
||||
if m.Channel.NSFW {
|
||||
topic = "(NSFW)\n" + topic
|
||||
}
|
||||
|
||||
if topic == "" {
|
||||
return text.Rich{}
|
||||
}
|
||||
|
||||
return Parse([]byte(topic))
|
||||
}
|
||||
|
||||
func (m MentionSegment) userInfo() text.Rich {
|
||||
if m.GuildUser.Member == nil {
|
||||
m.GuildUser.Member = &discord.Member{
|
||||
User: m.GuildUser.User,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the guild for the role slice. If not, then too bad.
|
||||
g, err := m.store.Guild(m.guild)
|
||||
if err != nil {
|
||||
g = &discord.Guild{}
|
||||
}
|
||||
|
||||
return userInfo(*g, *m.GuildUser.Member, nil)
|
||||
}
|
||||
|
||||
func (m MentionSegment) roleInfo() text.Rich {
|
||||
// // We don't have much to write here.
|
||||
// var segment = text.Rich{
|
||||
// Content: m.GuildRole.Name,
|
||||
// }
|
||||
|
||||
// // Maybe add a color if we have any.
|
||||
// if c := m.GuildRole.Color.Uint32(); c > 0 {
|
||||
// segment.Segments = []text.Segment{
|
||||
// NewColored(len(m.GuildRole.Name), m.GuildRole.Color.Uint32()),
|
||||
// }
|
||||
// }
|
||||
|
||||
return text.Rich{}
|
||||
}
|
||||
|
||||
type LargeActivityImage struct {
|
||||
start int
|
||||
url string
|
||||
text string
|
||||
}
|
||||
|
||||
func NewLargeActivityImage(start int, ac discord.Activity) LargeActivityImage {
|
||||
var text = ac.Assets.LargeText
|
||||
if text == "" {
|
||||
text = "Activity Image"
|
||||
}
|
||||
|
||||
return LargeActivityImage{
|
||||
start: start,
|
||||
url: urlutils.AssetURL(ac.ApplicationID, ac.Assets.LargeImage),
|
||||
text: ac.Assets.LargeText,
|
||||
}
|
||||
}
|
||||
|
||||
func (i LargeActivityImage) Bounds() (start, end int) { return i.start, i.start }
|
||||
func (i LargeActivityImage) Image() string { return i.url }
|
||||
func (i LargeActivityImage) ImageSize() (w, h int) { return 60, 60 }
|
||||
func (i LargeActivityImage) ImageText() string { return i.text }
|
||||
|
||||
func userInfo(guild discord.Guild, member discord.Member, state *ningen.State) text.Rich {
|
||||
var content bytes.Buffer
|
||||
var segment text.Rich
|
||||
|
||||
// Write the username if the user has a nickname.
|
||||
if member.Nick != "" {
|
||||
content.WriteString("Username: ")
|
||||
content.WriteString(member.User.Username)
|
||||
content.WriteByte('#')
|
||||
content.WriteString(member.User.Discriminator)
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Write extra information if any, but only if we have the guild state.
|
||||
if len(member.RoleIDs) > 0 && guild.ID.IsValid() {
|
||||
// Write a prepended new line, as role writes will always prepend a new
|
||||
// line. This is to prevent a trailing new line.
|
||||
formatSectionf(&segment, &content, "Roles")
|
||||
|
||||
for _, id := range member.RoleIDs {
|
||||
rl, ok := findRole(guild.Roles, id)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prepend a new line before each item.
|
||||
content.WriteByte('\n')
|
||||
// Write exactly the role name, then grab the segment and color it.
|
||||
start, end := writestringbuf(&content, "@"+rl.Name)
|
||||
// But we only add the color if the role has one.
|
||||
if color := rl.Color.Uint32(); color > 0 {
|
||||
segmentadd(&segment, NewColoredSegment(start, end, rl.Color.Uint32()))
|
||||
}
|
||||
}
|
||||
|
||||
// End section.
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// These information can only be obtained from the state. As such, we check
|
||||
// if the state is given.
|
||||
if state != nil {
|
||||
// Does the user have rich presence? If so, write.
|
||||
if p, err := state.Presence(guild.ID, member.User.ID); err == nil {
|
||||
for _, ac := range p.Activities {
|
||||
formatActivity(&segment, &content, ac)
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
} else if guild.ID.IsValid() {
|
||||
// If we're still in a guild, then we can ask Discord for that
|
||||
// member with their presence attached.
|
||||
state.MemberState.RequestMember(guild.ID, member.User.ID)
|
||||
}
|
||||
|
||||
// Write the user's note if any.
|
||||
if note := state.NoteState.Note(member.User.ID); note != "" {
|
||||
formatSectionf(&segment, &content, "Note")
|
||||
content.WriteRune('\n')
|
||||
|
||||
start, end := writestringbuf(&content, note)
|
||||
segmentadd(&segment, InlineSegment{start, end, text.AttrMonospace})
|
||||
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the written content into the text segment and return it after
|
||||
// trimming the trailing new line.
|
||||
segment.Content = strings.TrimSuffix(content.String(), "\n")
|
||||
return segment
|
||||
}
|
||||
|
||||
func formatSectionf(segment *text.Rich, content *bytes.Buffer, f string, argv ...interface{}) {
|
||||
// Treat f as a regular string at first.
|
||||
var str = fmt.Sprintf("%s", f)
|
||||
|
||||
// If there are argvs, then treat f as a format string.
|
||||
if len(argv) > 0 {
|
||||
str = fmt.Sprintf(str, argv...)
|
||||
}
|
||||
|
||||
start, end := writestringbuf(content, str)
|
||||
segmentadd(segment, InlineSegment{start, end, text.AttrBold | text.AttrUnderline})
|
||||
}
|
||||
|
||||
func formatActivity(segment *text.Rich, content *bytes.Buffer, ac discord.Activity) {
|
||||
switch ac.Type {
|
||||
case discord.GameActivity:
|
||||
formatSectionf(segment, content, "Playing %s", ac.Name)
|
||||
content.WriteByte('\n')
|
||||
|
||||
case discord.ListeningActivity:
|
||||
formatSectionf(segment, content, "Listening to %s", ac.Name)
|
||||
content.WriteByte('\n')
|
||||
|
||||
case discord.StreamingActivity:
|
||||
formatSectionf(segment, content, "Streaming on %s", ac.Name)
|
||||
content.WriteByte('\n')
|
||||
|
||||
case discord.CustomActivity:
|
||||
formatSectionf(segment, content, "Status")
|
||||
content.WriteByte('\n')
|
||||
|
||||
if ac.Emoji != nil {
|
||||
if !ac.Emoji.ID.IsValid() {
|
||||
content.WriteString(ac.Emoji.Name)
|
||||
} else {
|
||||
segmentadd(segment, EmojiSegment{
|
||||
Start: content.Len(),
|
||||
Name: ac.Emoji.Name,
|
||||
EmojiURL: ac.Emoji.EmojiURL() + "&size=64",
|
||||
Large: ac.State == "",
|
||||
})
|
||||
}
|
||||
|
||||
content.WriteByte(' ')
|
||||
}
|
||||
|
||||
default:
|
||||
formatSectionf(segment, content, "Status")
|
||||
content.WriteByte('\n')
|
||||
}
|
||||
|
||||
// Insert an image if there's any.
|
||||
if ac.Assets != nil && ac.Assets.LargeImage != "" {
|
||||
segmentadd(segment, NewLargeActivityImage(content.Len(), ac))
|
||||
content.WriteString(" ")
|
||||
}
|
||||
|
||||
if ac.Details != "" {
|
||||
start, end := writestringbuf(content, ac.Details)
|
||||
segmentadd(segment, InlineSegment{start, end, text.AttrBold})
|
||||
content.WriteByte('\n')
|
||||
}
|
||||
|
||||
if ac.State != "" {
|
||||
content.WriteString(ac.State)
|
||||
}
|
||||
}
|
||||
|
||||
func getPresence(
|
||||
state *ningen.State,
|
||||
guildID discord.GuildID, userID discord.UserID) *discord.Activity {
|
||||
|
||||
p, err := state.Presence(guildID, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(p.Activities) > 0 {
|
||||
return &p.Activities[0]
|
||||
}
|
||||
|
||||
return p.Game
|
||||
}
|
||||
|
||||
func findRole(roles []discord.Role, id discord.RoleID) (discord.Role, bool) {
|
||||
for _, role := range roles {
|
||||
if role.ID == id {
|
||||
return role, true
|
||||
}
|
||||
}
|
||||
return discord.Role{}, false
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package mention
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/emoji"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/inline"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/diamondburned/ningen"
|
||||
)
|
||||
|
||||
type LargeActivityImage struct {
|
||||
empty.TextSegment
|
||||
start int
|
||||
url string
|
||||
text string
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Imager = (*LargeActivityImage)(nil)
|
||||
_ text.Segment = (*LargeActivityImage)(nil)
|
||||
)
|
||||
|
||||
func NewLargeActivityImage(start int, ac discord.Activity) LargeActivityImage {
|
||||
var text = ac.Assets.LargeText
|
||||
if text == "" {
|
||||
text = "Activity Image"
|
||||
}
|
||||
|
||||
return LargeActivityImage{
|
||||
start: start,
|
||||
url: urlutils.AssetURL(ac.ApplicationID, ac.Assets.LargeImage),
|
||||
text: ac.Assets.LargeText,
|
||||
}
|
||||
}
|
||||
|
||||
func (i LargeActivityImage) Bounds() (start, end int) { return i.start, i.start }
|
||||
func (i LargeActivityImage) AsImager() text.Imager { return i }
|
||||
func (i LargeActivityImage) Image() string { return i.url }
|
||||
func (i LargeActivityImage) ImageSize() (w, h int) { return 60, 60 }
|
||||
func (i LargeActivityImage) ImageText() string { return i.text }
|
||||
|
||||
func formatSectionf(segment *text.Rich, content *bytes.Buffer, f string, argv ...interface{}) {
|
||||
// Treat f as a regular string at first.
|
||||
var str = fmt.Sprintf("%s", f)
|
||||
|
||||
// If there are argvs, then treat f as a format string.
|
||||
if len(argv) > 0 {
|
||||
str = fmt.Sprintf(str, argv...)
|
||||
}
|
||||
|
||||
start, end := segutil.WriteStringBuf(content, str)
|
||||
segutil.Add(segment, inline.NewSegment(
|
||||
start, end,
|
||||
text.AttributeBold,
|
||||
text.AttributeUnderline,
|
||||
))
|
||||
}
|
||||
|
||||
func formatActivity(segment *text.Rich, content *bytes.Buffer, ac discord.Activity) {
|
||||
switch ac.Type {
|
||||
case discord.GameActivity:
|
||||
formatSectionf(segment, content, "Playing %s", ac.Name)
|
||||
content.WriteByte('\n')
|
||||
|
||||
case discord.ListeningActivity:
|
||||
formatSectionf(segment, content, "Listening to %s", ac.Name)
|
||||
content.WriteByte('\n')
|
||||
|
||||
case discord.StreamingActivity:
|
||||
formatSectionf(segment, content, "Streaming on %s", ac.Name)
|
||||
content.WriteByte('\n')
|
||||
|
||||
case discord.CustomActivity:
|
||||
formatSectionf(segment, content, "Status")
|
||||
content.WriteByte('\n')
|
||||
|
||||
if ac.Emoji != nil {
|
||||
if !ac.Emoji.ID.IsValid() {
|
||||
content.WriteString(ac.Emoji.Name)
|
||||
} else {
|
||||
segutil.Add(segment, emoji.Segment{
|
||||
Start: content.Len(),
|
||||
Emoji: emoji.EmojiFromDiscord(*ac.Emoji, ac.State == ""),
|
||||
})
|
||||
}
|
||||
|
||||
content.WriteByte(' ')
|
||||
}
|
||||
|
||||
default:
|
||||
formatSectionf(segment, content, "Status")
|
||||
content.WriteByte('\n')
|
||||
}
|
||||
|
||||
// Insert an image if there's any.
|
||||
if ac.Assets != nil && ac.Assets.LargeImage != "" {
|
||||
segutil.Add(segment, NewLargeActivityImage(content.Len(), ac))
|
||||
content.WriteString(" ")
|
||||
}
|
||||
|
||||
if ac.Details != "" {
|
||||
start, end := segutil.WriteStringBuf(content, ac.Details)
|
||||
segutil.Add(segment, inline.NewSegment(start, end, text.AttributeBold))
|
||||
content.WriteByte('\n')
|
||||
}
|
||||
|
||||
if ac.State != "" {
|
||||
content.WriteString(ac.State)
|
||||
}
|
||||
}
|
||||
|
||||
func getPresence(
|
||||
state *ningen.State,
|
||||
guildID discord.GuildID, userID discord.UserID) *discord.Activity {
|
||||
|
||||
p, err := state.Presence(guildID, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(p.Activities) > 0 {
|
||||
return &p.Activities[0]
|
||||
}
|
||||
|
||||
return p.Game
|
||||
}
|
||||
|
||||
func findRole(roles []discord.Role, id discord.RoleID) (discord.Role, bool) {
|
||||
for _, role := range roles {
|
||||
if role.ID == id {
|
||||
return role, true
|
||||
}
|
||||
}
|
||||
return discord.Role{}, false
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package mention
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
discord.Channel
|
||||
}
|
||||
|
||||
func NewChannel(ch discord.Channel) *Channel {
|
||||
return &Channel{
|
||||
Channel: ch,
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) MentionInfo() text.Rich {
|
||||
var topic = ch.Topic
|
||||
if ch.NSFW {
|
||||
topic = "(NSFW)\n" + topic
|
||||
}
|
||||
|
||||
if topic == "" {
|
||||
return text.Rich{}
|
||||
}
|
||||
|
||||
return renderer.Parse([]byte(topic))
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package mention
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/renderer"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func init() {
|
||||
renderer.Register(md.KindMention, mention)
|
||||
}
|
||||
|
||||
func mention(r *renderer.Text, node ast.Node, enter bool) ast.WalkStatus {
|
||||
n := node.(*md.Mention)
|
||||
|
||||
if enter {
|
||||
var seg = Segment{}
|
||||
|
||||
switch {
|
||||
case n.Channel != nil:
|
||||
seg.Start, seg.End = r.WriteString("#" + n.Channel.Name)
|
||||
seg.Channel = NewChannel(*n.Channel)
|
||||
case n.GuildUser != nil:
|
||||
seg.Start, seg.End = r.WriteString("@" + n.GuildUser.Username)
|
||||
seg.User = NewUser(r.Store, r.Message.GuildID, *n.GuildUser)
|
||||
case n.GuildRole != nil:
|
||||
seg.Start, seg.End = r.WriteString("@" + n.GuildRole.Name)
|
||||
seg.Role = NewRole(*n.GuildRole)
|
||||
default:
|
||||
// Unexpected error; skip.
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
r.Append(seg)
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
||||
type Segment struct {
|
||||
empty.TextSegment
|
||||
Start, End int
|
||||
|
||||
// enums?
|
||||
Channel *Channel
|
||||
User *User
|
||||
Role *Role
|
||||
}
|
||||
|
||||
func (s Segment) Bounds() (start, end int) {
|
||||
return s.Start, s.End
|
||||
}
|
||||
|
||||
func (s Segment) AsColorer() text.Colorer {
|
||||
switch {
|
||||
case s.User != nil:
|
||||
return s.User
|
||||
case s.Role != nil:
|
||||
return s.Role
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Segment) AsAvatarer() text.Avatarer {
|
||||
switch {
|
||||
case s.User != nil:
|
||||
return s.User
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Segment) AsMentioner() text.Mentioner {
|
||||
switch {
|
||||
case s.Channel != nil:
|
||||
return s.Channel
|
||||
case s.User != nil:
|
||||
return s.User
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package mention
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
discord.Role
|
||||
}
|
||||
|
||||
func NewRole(role discord.Role) *Role {
|
||||
return &Role{role}
|
||||
}
|
||||
|
||||
func (r *Role) Color() uint32 {
|
||||
if r.Role.Color == 0 {
|
||||
return colored.Blurple
|
||||
}
|
||||
return text.SolidColor(r.Role.Color.Uint32())
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package mention
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/inline"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/diamondburned/ningen"
|
||||
)
|
||||
|
||||
// NameSegment represents a clickable member name; it does not implement colors.
|
||||
type NameSegment struct {
|
||||
empty.TextSegment
|
||||
start int
|
||||
end int
|
||||
um User
|
||||
}
|
||||
|
||||
var _ text.Segment = (*NameSegment)(nil)
|
||||
|
||||
func UserSegment(start, end int, u discord.User) NameSegment {
|
||||
return NameSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
um: User{
|
||||
member: discord.Member{User: u},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func MemberSegment(start, end int, guild discord.Guild, m discord.Member) NameSegment {
|
||||
return NameSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
um: User{
|
||||
guild: guild,
|
||||
member: m,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithState assigns a ningen state into the given name segment. This allows the
|
||||
// popovers to have additional information such as user notes.
|
||||
func (m *NameSegment) WithState(state *ningen.State) {
|
||||
m.um.state = state
|
||||
}
|
||||
|
||||
func (m NameSegment) Bounds() (start, end int) {
|
||||
return m.start, m.end
|
||||
}
|
||||
|
||||
func (m NameSegment) AsMentioner() text.Mentioner {
|
||||
return m.um
|
||||
}
|
||||
|
||||
func (m NameSegment) AsAvatarer() text.Avatarer {
|
||||
return m.um
|
||||
}
|
||||
|
||||
type User struct {
|
||||
state state.Store
|
||||
guild discord.Guild
|
||||
member discord.Member
|
||||
}
|
||||
|
||||
var (
|
||||
_ text.Colorer = (*User)(nil)
|
||||
_ text.Avatarer = (*User)(nil)
|
||||
_ text.Mentioner = (*User)(nil)
|
||||
)
|
||||
|
||||
// NewUser creates a new user mention. If state is of type *ningen.State, then
|
||||
// it'll fetch additional information asynchronously.
|
||||
func NewUser(state state.Store, guild discord.GuildID, guser discord.GuildUser) *User {
|
||||
if guser.Member == nil {
|
||||
m, err := state.Member(guild, guser.ID)
|
||||
if err != nil {
|
||||
guser.Member = &discord.Member{}
|
||||
} else {
|
||||
guser.Member = m
|
||||
}
|
||||
}
|
||||
|
||||
guser.Member.User = guser.User
|
||||
|
||||
// Get the guild for the role slice. If not, then too bad.
|
||||
g, err := state.Guild(guild)
|
||||
if err != nil {
|
||||
g = &discord.Guild{}
|
||||
}
|
||||
|
||||
return &User{
|
||||
state: state,
|
||||
guild: *g,
|
||||
member: *guser.Member,
|
||||
}
|
||||
}
|
||||
|
||||
func (um *User) Color() uint32 {
|
||||
g, err := um.state.Guild(um.guild.ID)
|
||||
if err != nil {
|
||||
return colored.Blurple
|
||||
}
|
||||
|
||||
return text.SolidColor(discord.MemberColor(*g, um.member).Uint32())
|
||||
}
|
||||
|
||||
func (um *User) AvatarSize() int {
|
||||
return 96
|
||||
}
|
||||
|
||||
func (um *User) AvatarText() string {
|
||||
if um.member.Nick != "" {
|
||||
return um.member.Nick
|
||||
}
|
||||
return um.member.User.Username
|
||||
}
|
||||
|
||||
func (um *User) Avatar() (url string) {
|
||||
return um.member.User.AvatarURL()
|
||||
}
|
||||
|
||||
func (um *User) MentionInfo() text.Rich {
|
||||
var content bytes.Buffer
|
||||
var segment text.Rich
|
||||
|
||||
// Write the username if the user has a nickname.
|
||||
if um.member.Nick != "" {
|
||||
content.WriteString("Username: ")
|
||||
content.WriteString(um.member.User.Username)
|
||||
content.WriteByte('#')
|
||||
content.WriteString(um.member.User.Discriminator)
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Write extra information if any, but only if we have the guild state.
|
||||
if len(um.member.RoleIDs) > 0 && um.guild.ID.IsValid() {
|
||||
// Write a prepended new line, as role writes will always prepend a new
|
||||
// line. This is to prevent a trailing new line.
|
||||
formatSectionf(&segment, &content, "Roles")
|
||||
|
||||
for _, id := range um.member.RoleIDs {
|
||||
rl, ok := findRole(um.guild.Roles, id)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prepend a new line before each item.
|
||||
content.WriteByte('\n')
|
||||
// Write exactly the role name, then grab the segment and color it.
|
||||
start, end := segutil.WriteStringBuf(&content, "@"+rl.Name)
|
||||
// But we only add the color if the role has one.
|
||||
if rgb := rl.Color.Uint32(); rgb > 0 {
|
||||
segutil.Add(&segment, colored.NewSegment(start, end, rgb))
|
||||
}
|
||||
}
|
||||
|
||||
// End section.
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// These information can only be obtained from the state. As such, we check
|
||||
// if the state is given.
|
||||
if ningenState, ok := um.state.(*ningen.State); ok {
|
||||
// Does the user have rich presence? If so, write.
|
||||
if p, err := um.state.Presence(um.guild.ID, um.member.User.ID); err == nil {
|
||||
for _, ac := range p.Activities {
|
||||
formatActivity(&segment, &content, ac)
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
} else if um.guild.ID.IsValid() {
|
||||
// If we're still in a guild, then we can ask Discord for that
|
||||
// member with their presence attached.
|
||||
ningenState.MemberState.RequestMember(um.guild.ID, um.member.User.ID)
|
||||
}
|
||||
|
||||
// Write the user's note if any.
|
||||
if note := ningenState.NoteState.Note(um.member.User.ID); note != "" {
|
||||
formatSectionf(&segment, &content, "Note")
|
||||
content.WriteRune('\n')
|
||||
|
||||
start, end := segutil.WriteStringBuf(&content, note)
|
||||
segutil.Add(&segment, inline.NewSegment(start, end, text.AttributeMonospace))
|
||||
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the written content into the text segment and return it after
|
||||
// trimming the trailing new line.
|
||||
segment.Content = strings.TrimSuffix(content.String(), "\n")
|
||||
return segment
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/ningen/md"
|
||||
)
|
||||
|
||||
// InlineState assists in keeping a stateful inline segment builder.
|
||||
type InlineState struct {
|
||||
// TODO: use a stack to allow overlapping
|
||||
Start, End int
|
||||
Attributes md.Attribute
|
||||
}
|
||||
|
||||
func (i *InlineState) Add(attr md.Attribute) {
|
||||
i.Attributes |= attr
|
||||
}
|
||||
|
||||
func (i *InlineState) Remove(attr md.Attribute) {
|
||||
i.Attributes &= ^attr
|
||||
}
|
||||
|
||||
func (i InlineState) Copy() InlineState {
|
||||
return i
|
||||
}
|
||||
|
||||
func (i InlineState) Empty() bool {
|
||||
return i.Attributes == 0 || i.Start < i.End
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package renderer
|
||||
|
||||
// LinkState is used for ast.Link segments.
|
||||
type LinkState struct {
|
||||
Linkstack []int // stack of starting integers
|
||||
}
|
||||
|
||||
func (ls *LinkState) Append(l int) {
|
||||
ls.Linkstack = append(ls.Linkstack, l)
|
||||
}
|
||||
|
||||
func (ls *LinkState) Pop() int {
|
||||
ilast := len(ls.Linkstack) - 1
|
||||
start := ls.Linkstack[ilast]
|
||||
ls.Linkstack = ls.Linkstack[:ilast]
|
||||
return start
|
||||
}
|
||||
|
||||
func (ls LinkState) Len() int {
|
||||
return len(ls.Linkstack)
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
package renderer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
type Renderer func(r *Text, n ast.Node, enter bool) ast.WalkStatus
|
||||
|
||||
var renderers = map[ast.NodeKind]Renderer{}
|
||||
|
||||
// Register registers a renderer to a node kind.
|
||||
func Register(kind ast.NodeKind, r Renderer) {
|
||||
renderers[kind] = r
|
||||
}
|
||||
|
||||
// Parse parses the raw Markdown bytes into a rich text.
|
||||
func Parse(b []byte) text.Rich {
|
||||
node := md.Parse(b)
|
||||
return RenderNode(b, node)
|
||||
}
|
||||
|
||||
// RenderNode renders the given raw Markdown bytes with the parsed AST node into
|
||||
// a rich text.
|
||||
func RenderNode(source []byte, n ast.Node) text.Rich {
|
||||
r := New(source, n)
|
||||
r.Walk(n)
|
||||
|
||||
return text.Rich{
|
||||
Content: r.String(),
|
||||
Segments: r.Segments,
|
||||
}
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Buffer *bytes.Buffer
|
||||
Source []byte
|
||||
Segments []text.Segment
|
||||
Inlines InlineState
|
||||
Links LinkState
|
||||
|
||||
// these fields can be nil
|
||||
Message *discord.Message
|
||||
Store state.Store
|
||||
}
|
||||
|
||||
func New(src []byte, node ast.Node) *Text {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(len(src))
|
||||
|
||||
return &Text{
|
||||
Source: src,
|
||||
Buffer: buf,
|
||||
Segments: make([]text.Segment, 0, node.ChildCount()),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Text) WithState(m *discord.Message, s state.Store) {
|
||||
r.Message = m
|
||||
r.Store = s
|
||||
}
|
||||
|
||||
// String returns a stringified version of Bytes().
|
||||
func (r *Text) String() string {
|
||||
return string(r.Bytes())
|
||||
}
|
||||
|
||||
// Bytes returns the plain content of the buffer with right spaces trimmed as
|
||||
// best as it could, that is, the function will not trim right spaces that
|
||||
// segments use.
|
||||
func (r *Text) Bytes() []byte {
|
||||
// Get the rightmost index out of all the segments.
|
||||
var rightmost int
|
||||
for _, seg := range r.Segments {
|
||||
if _, end := seg.Bounds(); end > rightmost {
|
||||
rightmost = end
|
||||
}
|
||||
}
|
||||
|
||||
// Get the original byte slice.
|
||||
org := r.Buffer.Bytes()
|
||||
|
||||
// Trim the right spaces.
|
||||
trbuf := bytes.TrimRight(org, "\n")
|
||||
|
||||
// If we trimmed way too far, then slice so that we get as far as the
|
||||
// rightmost segment.
|
||||
if len(trbuf) < rightmost {
|
||||
return org[:rightmost]
|
||||
}
|
||||
|
||||
// Else, we're safe returning the trimmed slice.
|
||||
return trbuf
|
||||
}
|
||||
|
||||
func (r *Text) WriteStringf(f string, v ...interface{}) (start, end int) {
|
||||
return r.WriteString(fmt.Sprintf(f, v...))
|
||||
}
|
||||
|
||||
func (r *Text) WriteString(s string) (start, end int) {
|
||||
return segutil.WriteStringBuf(r.Buffer, s)
|
||||
}
|
||||
|
||||
func (r *Text) Write(b []byte) (start, end int) {
|
||||
return segutil.WriteBuf(r.Buffer, b)
|
||||
}
|
||||
|
||||
// StartBlock guarantees enough indentation to start a new block.
|
||||
func (r *Text) StartBlock() {
|
||||
r.StartBlockN(2)
|
||||
}
|
||||
|
||||
// EnsureBreak ensures that the current line is a new line.
|
||||
func (r *Text) EnsureBreak() {
|
||||
r.StartBlockN(1)
|
||||
}
|
||||
|
||||
// StartBlockN allows a custom block level.
|
||||
func (r *Text) StartBlockN(n int) {
|
||||
var maxNewlines = 0
|
||||
|
||||
// Peek twice. If the last character is already a new line or we're only at
|
||||
// the start of line (length 0), then don't pad.
|
||||
if r.Buffer.Len() > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
if r.PeekLast(i) != '\n' {
|
||||
maxNewlines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the padding.
|
||||
r.Buffer.Grow(maxNewlines)
|
||||
for i := 0; i < maxNewlines; i++ {
|
||||
r.Buffer.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Text) EndBlock() {
|
||||
// Do the same thing as starting a block.
|
||||
r.StartBlock()
|
||||
}
|
||||
|
||||
// Segments returns the previous byte that matches the offset, or 0 if the
|
||||
// offset goes past the first byte.
|
||||
func (r *Text) PeekLast(offset int) byte {
|
||||
if i := r.Buffer.Len() - offset - 1; i > 0 {
|
||||
return r.Buffer.Bytes()[i]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *Text) Append(segs ...text.Segment) {
|
||||
r.Segments = append(r.Segments, segs...)
|
||||
}
|
||||
|
||||
// Clone returns a shallow copy of Text with the new source.
|
||||
func (r *Text) Clone(src []byte) *Text {
|
||||
cpy := *r
|
||||
cpy.Source = src
|
||||
return &cpy
|
||||
}
|
||||
|
||||
// Join combines the states from renderer with r. Use this with clone.
|
||||
func (r *Text) Join(renderer *Text) {
|
||||
r.Segments = append([]text.Segment(nil), renderer.Segments...)
|
||||
r.Inlines = renderer.Inlines.Copy()
|
||||
}
|
||||
|
||||
// Walk walks on the given node with the RenderNode as the walker function.
|
||||
func (r *Text) Walk(n ast.Node) {
|
||||
ast.Walk(n, r.RenderNode)
|
||||
}
|
||||
|
||||
func (r *Text) RenderNode(n ast.Node, enter bool) (ast.WalkStatus, error) {
|
||||
f, ok := renderers[n.Kind()]
|
||||
if ok {
|
||||
return f(r, n, enter), nil
|
||||
}
|
||||
|
||||
switch n := n.(type) {
|
||||
case *ast.Document:
|
||||
case *ast.Paragraph:
|
||||
if !enter {
|
||||
// TODO: investigate
|
||||
// r.Buffer.WriteByte('\n')
|
||||
}
|
||||
// case *ast.Blockquote:
|
||||
// return r.blockquote(n, enter), nil
|
||||
// case *ast.FencedCodeBlock:
|
||||
// return r.codeblock(n, enter), nil
|
||||
// case *ast.Link:
|
||||
// return r.link(n, enter), nil
|
||||
// case *ast.AutoLink:
|
||||
// return r.autoLink(n, enter), nil
|
||||
// case *md.Inline:
|
||||
// return r.inline(n, enter), nil
|
||||
// case *md.Emoji:
|
||||
// return r.emoji(n, enter), nil
|
||||
// case *md.Mention:
|
||||
// return r.mention(n, enter), nil
|
||||
case *ast.String:
|
||||
if enter {
|
||||
r.Buffer.Write(n.Value)
|
||||
}
|
||||
case *ast.Text:
|
||||
if enter {
|
||||
r.Buffer.Write(n.Segment.Value(r.Source))
|
||||
|
||||
switch {
|
||||
case n.HardLineBreak():
|
||||
r.Buffer.WriteString("\n\n")
|
||||
case n.SoftLineBreak():
|
||||
r.Buffer.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package segutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
// helper global functions
|
||||
|
||||
func WriteBuf(w *bytes.Buffer, b []byte) (start, end int) {
|
||||
start = w.Len()
|
||||
w.Write(b)
|
||||
end = w.Len()
|
||||
return start, end
|
||||
}
|
||||
|
||||
func WriteStringBuf(w *bytes.Buffer, b string) (start, end int) {
|
||||
start = w.Len()
|
||||
w.WriteString(b)
|
||||
end = w.Len()
|
||||
return start, end
|
||||
}
|
||||
|
||||
func Add(r *text.Rich, seg ...text.Segment) {
|
||||
r.Segments = append(r.Segments, seg...)
|
||||
}
|
Loading…
Reference in New Issue