Rewrote segments to adapt to later cchat API

This commit is contained in:
diamondburned 2020-10-04 20:45:34 -07:00
parent 0f1cdafec6
commit e9796170f8
31 changed files with 1551 additions and 1232 deletions

4
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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 {

View File

@ -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 == "" {

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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 + ":"
}

View File

@ -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 }

View File

@ -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
}

View File

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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...)
}