1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-discord.git synced 2024-11-01 04:14:21 +00:00
cchat-discord/segments/md.go

221 lines
4.8 KiB
Go
Raw Normal View History

2020-06-19 01:00:24 +00:00
package segments
import (
"bytes"
"fmt"
2020-06-19 01:00:24 +00:00
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen/md"
"github.com/yuin/goldmark/ast"
)
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.walk(node)
r.renderEmbeds(m.Embeds, m, s)
r.renderAttachments(m.Attachments)
return text.Rich{
Content: r.String(),
Segments: r.segs,
}
2020-06-19 01:00:24 +00:00
}
func ParseWithMessage(b []byte, m *discord.Message, s state.Store, msg bool) text.Rich {
2020-06-19 01:00:24 +00:00
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
}
func NewTextReader(src []byte, node ast.Node) TextRenderer {
2020-06-19 01:00:24 +00:00
buf := &bytes.Buffer{}
buf.Grow(len(src))
2020-06-19 01:00:24 +00:00
return TextRenderer{
src: src,
2020-06-19 01:00:24 +00:00
buf: buf,
segs: make([]text.Segment, 0, node.ChildCount()),
2020-06-19 01:00:24 +00:00
}
}
2020-06-19 01:00:24 +00:00
// String returns a stringified version of Bytes().
func (r *TextRenderer) String() string {
return string(r.Bytes())
}
2020-06-19 01:00:24 +00:00
// 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
}
2020-06-19 01:00:24 +00:00
}
// 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
2020-06-19 01:00:24 +00:00
}
// i returns the current cursor position.
func (r *TextRenderer) i() int {
return r.buf.Len()
}
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) {
start = r.i()
r.buf.WriteString(s)
end = r.i()
return
}
func (r *TextRenderer) write(b []byte) (start, end int) {
start = r.i()
r.buf.Write(b)
end = r.i()
return
}
// startBlock guarantees enough indentation to start a new block.
func (r *TextRenderer) startBlock() {
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 {
if r.peekLast(0) != '\n' {
maxNewlines++
}
if r.peekLast(1) != '\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
}
2020-06-19 01:00:24 +00:00
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)
}
2020-06-19 01:00:24 +00:00
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
}