Skip to content

Commit

Permalink
Implement Spoiler and Strikethrough parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
belak committed Feb 22, 2024
1 parent 38ca635 commit 0df448f
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 50 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@

*.pb.go
.env
.direnv
.envrc
49 changes: 0 additions & 49 deletions discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,8 @@ import (

"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"github.com/seabird-chat/seabird-go/pb"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)

func TextToBlocks(data string) []*pb.Block {
src := []byte(data)

// TODO:
// - Strikethrough
// - Spoiler

reader := text.NewReader(src)
parser := parser.NewParser(
parser.WithBlockParsers(
//util.Prioritized(NewSetextHeadingParser(), 100),
//util.Prioritized(parser.NewThematicBreakParser(), 200),
util.Prioritized(parser.NewListParser(), 300),
//util.Prioritized(NewListItemParser(), 400),
util.Prioritized(parser.NewCodeBlockParser(), 500),
//util.Prioritized(NewATXHeadingParser(), 600),
util.Prioritized(parser.NewFencedCodeBlockParser(), 700),
util.Prioritized(parser.NewBlockquoteParser(), 800),
//util.Prioritized(NewHTMLBlockParser(), 900),
util.Prioritized(parser.NewParagraphParser(), 1000),
),
parser.WithInlineParsers(
util.Prioritized(parser.NewCodeSpanParser(), 100),
util.Prioritized(parser.NewLinkParser(), 200),
util.Prioritized(parser.NewAutoLinkParser(), 300),
//util.Prioritized(parser.NewRawHTMLParser(), 400),
util.Prioritized(parser.NewEmphasisParser(), 500),
),
parser.WithParagraphTransformers(
util.Prioritized(parser.LinkReferenceParagraphTransformer, 100),
),
)
doc := parser.Parse(reader)

/// TODO: remove debug text
doc.Dump(src, 0)

return nodeToBlocks(doc)
}

func nodeToBlocks(doc ast.Node) []*pb.Block {
return nil
}

// ComesFromDM returns true if a message comes from a DM channel
func ComesFromDM(s *discordgo.Session, m *discordgo.MessageCreate) (bool, error) {
channel, err := s.State.Channel(m.ChannelID)
Expand Down
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
pkgs.gofumpt
pkgs.gopls
pkgs.gotools
pkgs.golangci-lint
];
};
});
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/rs/zerolog v1.32.0
github.com/seabird-chat/seabird-go v0.4.1-0.20240221063203-d8e69692c30b
github.com/yuin/goldmark v1.7.0 // indirect
github.com/stretchr/testify v1.8.4
github.com/yuin/goldmark v1.7.0
golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.6.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/grpc v1.61.1 // indirect
)

replace github.com/yuin/goldmark => ../../vendor/goldmark
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,7 @@ github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWH
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
Expand Down Expand Up @@ -1605,6 +1606,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
Expand Down Expand Up @@ -1646,6 +1648,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -2504,6 +2507,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
196 changes: 196 additions & 0 deletions messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package seabird_discord

import (
"fmt"
"strings"

"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"

"github.com/seabird-chat/seabird-go/pb"
)

func TextToBlocks(data string) []*pb.Block {
var isAction bool

// If the message starts and ends with an underscore, it's an "action"
// message. This parsing is actually *more* accurate than Discord's because
// the /me command blindly adds an _ to the start and end, but it's
// displayed as normal italics.
if len(data) > 2 && strings.HasPrefix(data, "_") && strings.HasSuffix(data, "_") {
data = strings.TrimPrefix(strings.TrimSuffix(data, "_"), "_")
isAction = true
}

src := []byte(data)

reader := text.NewReader(src)

// This parser roughly approximates Discord's markdown parsing.
parser := parser.NewParser(
parser.WithBlockParsers(
//util.Prioritized(parser.NewSetextHeadingParser(), 100),
//util.Prioritized(parser.NewThematicBreakParser(), 200),
util.Prioritized(parser.NewListParser(), 300),
util.Prioritized(parser.NewListItemParser(), 400),
util.Prioritized(parser.NewCodeBlockParser(), 500),
util.Prioritized(parser.NewATXHeadingParser(), 600),
util.Prioritized(parser.NewFencedCodeBlockParser(), 700),
util.Prioritized(parser.NewBlockquoteParser(), 800),
//util.Prioritized(NewHTMLBlockParser(), 900),
util.Prioritized(parser.NewParagraphParser(), 1000),
),
parser.WithInlineParsers(
util.Prioritized(parser.NewCodeSpanParser(), 100),
util.Prioritized(parser.NewLinkParser(), 200),
util.Prioritized(parser.NewAutoLinkParser(), 300),
//util.Prioritized(parser.NewRawHTMLParser(), 400),
util.Prioritized(parser.NewEmphasisParser(), 500),

// Custom additions
util.Prioritized(newMultiCharInlineParser('|', "Spoiler"), 1000),
util.Prioritized(newMultiCharInlineParser('~', "Strikethrough"), 1000),
),
parser.WithParagraphTransformers(
util.Prioritized(parser.LinkReferenceParagraphTransformer, 100),
),
)
doc := parser.Parse(reader)

blocks := nodeToBlocks(doc.FirstChild())

if isAction {
blocks = []*pb.Block{
&pb.Block{
Inner: &pb.Block_Action{
Action: &pb.ActionBlock{
Inner: blocks,
},
},
},
}
}

return blocks
}

func nodeToBlocks(doc ast.Node) []*pb.Block {
var ret []*pb.Block

fmt.Println(doc.Type())

return ret
}

// ScanDelimiter scans a multi-character delimiter by given DelimiterProcessor.
// This was originally based off parser.ScanDelimiter, but has been simplified
// and tweaked to work better with how spoiler and strikethrough blocks work in
// Discord to the point that it now no longer resembles the original.
func ScanMultiCharDelimiter(line []byte, targetLen int, processor parser.DelimiterProcessor) *parser.Delimiter {
if len(line) < targetLen {
return nil
}

c := line[0]

if !processor.IsDelimiter(c) {
return nil
}

for _, c2 := range line[1:targetLen] {
if c != c2 {
return nil
}
}

return parser.NewDelimiter(true, true, targetLen, c, processor)
}

type multiCharInlineParser struct {
baseChar byte
processor *multiCharDelimiterProcessor
}

func newMultiCharInlineParser(baseChar byte, kind string) parser.InlineParser {
return &multiCharInlineParser{
baseChar: baseChar,
processor: &multiCharDelimiterProcessor{
baseChar: baseChar,
kind: ast.NewNodeKind(kind),
},
}
}

func (p *multiCharInlineParser) Trigger() []byte {
return []byte{p.baseChar, p.baseChar}
}

func (p *multiCharInlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, segment := block.PeekLine()

// If the last delimiter ended where we're starting and matches the char we
// care about, bail.
lastDelim := pc.LastDelimiter()
if lastDelim != nil && lastDelim.Char == p.baseChar {
_, curSeg := block.Position()
if curSeg.Start == lastDelim.Segment.Stop {
return nil
}
}

node := ScanMultiCharDelimiter(line, 2, p.processor)
if node == nil {
return nil
}

node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
block.Advance(node.OriginalLength)
pc.PushDelimiter(node)

return node
}

type multiCharDelimiterProcessor struct {
baseChar byte
kind ast.NodeKind
}

func (p *multiCharDelimiterProcessor) IsDelimiter(b byte) bool {
return b == p.baseChar
}

func (p *multiCharDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
return opener.Char == closer.Char && opener.Length == closer.Length
}

func (p *multiCharDelimiterProcessor) OnMatch(consumes int) ast.Node {
return newMultiCharDelimiterNode(consumes, p.kind)
}

type multiCharDelimiterNode struct {
ast.BaseInline
Level int
kind ast.NodeKind
}

func newMultiCharDelimiterNode(level int, kind ast.NodeKind) ast.Node {
return &multiCharDelimiterNode{
Level: level,
kind: kind,
}
}

// Dump implements Node.Dump.
func (n *multiCharDelimiterNode) Dump(source []byte, level int) {
m := map[string]string{
"Level": fmt.Sprintf("%v", n.Level),
}
ast.DumpHelper(n, source, level, m, nil)
}

// Kind implements Node.Kind.
func (n *multiCharDelimiterNode) Kind() ast.NodeKind {
return n.kind
}
27 changes: 27 additions & 0 deletions messages_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package seabird_discord

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestTextToBlocks(t *testing.T) {
var testCases = []struct {
input string
}{
//{
// input: "asd ~~~~~ asd",
//},
{
input: "~a~ ~hello~ ~~~world~~~ ~~~~~asdf~~~~~",
},
//{
// input: "*a* *hello* ***world*** *****asdf*****",
//},
}

for _, testCase := range testCases {
assert.NotNil(t, TextToBlocks(testCase.input))
}
}

0 comments on commit 0df448f

Please sign in to comment.