mirror of
https://github.com/diamondburned/cchat-discord.git
synced 2025-03-24 02:49:20 +00:00
Added embed support; minor changes to codeblock formatting
This commit is contained in:
parent
fbf95b9b6c
commit
983e18a9d5
|
@ -428,11 +428,10 @@ func (ch *Channel) TypingTimeout() time.Duration {
|
|||
|
||||
func (ch *Channel) TypingSubscribe(ti cchat.TypingIndicator) (func(), error) {
|
||||
return ch.session.AddHandler(func(t *gateway.TypingStartEvent) {
|
||||
if t.ChannelID != ch.id {
|
||||
return
|
||||
}
|
||||
if t, err := NewTyper(ch.session.Store, t); err == nil {
|
||||
ti.AddTyper(t)
|
||||
if t.ChannelID == ch.id {
|
||||
if typer, err := NewTyper(ch.session.Store, t); err == nil {
|
||||
ti.AddTyper(typer)
|
||||
}
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
|
@ -179,7 +180,7 @@ func (ch *Channel) completeEmojis(word string) (entries []cchat.CompletionEntry)
|
|||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: emoji.String(),
|
||||
Text: text.Rich{Content: ":" + emoji.Name + ":"},
|
||||
IconURL: URLSized(emoji.EmojiURL(), 32), // small
|
||||
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
|
||||
Image: true,
|
||||
})
|
||||
if len(entries) >= MaxCompletion {
|
||||
|
|
3
go.mod
3
go.mod
|
@ -4,8 +4,9 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/diamondburned/arikawa v0.9.5
|
||||
github.com/diamondburned/cchat v0.0.40
|
||||
github.com/diamondburned/cchat v0.0.41
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-test/deep v1.0.6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/yuin/goldmark v1.1.30
|
||||
|
|
4
go.sum
4
go.sum
|
@ -37,8 +37,12 @@ github.com/diamondburned/cchat v0.0.39 h1:Hxd7swmAIECm0MBd5wb1IFvreChwDFwnAshqgA
|
|||
github.com/diamondburned/cchat v0.0.39/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I=
|
||||
github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
github.com/diamondburned/cchat v0.0.41 h1:6y32s2wWTiDw4hWN/Gna6ay3uUrRAW5V8Cj0/xLKovw=
|
||||
github.com/diamondburned/cchat v0.0.41/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
|
22
message.go
22
message.go
|
@ -1,14 +1,13 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/segments"
|
||||
"github.com/diamondburned/cchat-discord/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
|
@ -55,21 +54,7 @@ func (m messageHeader) Time() time.Time {
|
|||
|
||||
// AvatarURL wraps the URL with URL queries for the avatar.
|
||||
func AvatarURL(URL string) string {
|
||||
return URLSized(URL, 64)
|
||||
}
|
||||
|
||||
// URLSized wraps the URL with the size query.
|
||||
func URLSized(URL string, size int) string {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return URL
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Set("size", strconv.Itoa(size))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
return urlutils.Sized(URL, 64)
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
|
@ -202,6 +187,9 @@ func NewMessage(m discord.Message, s *Session, author Author) Message {
|
|||
}
|
||||
|
||||
func (m Message) Author() cchat.MessageAuthor {
|
||||
if !m.author.id.Valid() {
|
||||
return nil
|
||||
}
|
||||
return m.author
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,9 @@ 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()
|
||||
defer r.endBlock()
|
||||
r.buf.WriteString("---\n")
|
||||
|
||||
// Create a segment.
|
||||
seg := CodeblockSegment{
|
||||
|
@ -34,6 +35,10 @@ func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkSta
|
|||
// Close the segment.
|
||||
seg.end = r.i()
|
||||
r.append(seg)
|
||||
|
||||
// Close the block.
|
||||
r.buf.WriteString("\n---")
|
||||
r.endBlock()
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
|
|
235
segments/embed.go
Normal file
235
segments/embed.go
Normal file
|
@ -0,0 +1,235 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/cchat-discord/urlutils"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
var imageExts = []string{".jpg", ".jpeg", ".png", ".webp", ".gif"}
|
||||
|
||||
func (r *TextRenderer) renderEmbeds(embeds []discord.Embed, m *discord.Message, s state.Store) {
|
||||
for _, embed := range embeds {
|
||||
r.startBlock()
|
||||
r.buf.WriteString("---\n")
|
||||
|
||||
r.renderEmbed(embed, m, s)
|
||||
|
||||
r.buf.WriteString("---") // 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.i(), *a))
|
||||
r.buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
start, end := r.writeString(a.Name)
|
||||
r.buf.WriteByte('\n')
|
||||
|
||||
if a.URL != "" {
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
a.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if embed.Title != "" {
|
||||
start, end := r.writeString(embed.Title)
|
||||
r.buf.WriteByte('\n')
|
||||
|
||||
if embed.URL != "" {
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
embed.URL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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.buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
if len(embed.Fields) > 0 {
|
||||
// Pad another new line.
|
||||
r.buf.WriteByte('\n')
|
||||
|
||||
// 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.i(), *f))
|
||||
r.buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
r.buf.WriteString(f.Text)
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
if embed.Timestamp.Valid() {
|
||||
if embed.Footer != nil {
|
||||
r.buf.WriteString(" - ")
|
||||
}
|
||||
|
||||
r.buf.WriteString(embed.Timestamp.Format(time.RFC1123))
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
|
||||
// Don't do anything if there are no attachments.
|
||||
if len(attachments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Start a new block before rendering attachments.
|
||||
r.startBlock()
|
||||
|
||||
// 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.i(), 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
|
||||
}
|
||||
|
||||
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, a discord.EmbedFooter) AvatarSegment {
|
||||
return AvatarSegment{
|
||||
start: start,
|
||||
url: a.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 {
|
||||
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, text string) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: i.Proxy,
|
||||
w: int(i.Width),
|
||||
h: int(i.Height),
|
||||
text: text,
|
||||
}
|
||||
}
|
||||
|
||||
func EmbedThumbnail(start int, t discord.EmbedThumbnail, text string) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: t.Proxy,
|
||||
w: int(t.Width),
|
||||
h: int(t.Height),
|
||||
text: text,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -14,18 +14,15 @@ var _ text.Linker = (*LinkSegment)(nil)
|
|||
|
||||
func (r *TextRenderer) link(n *ast.Link, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
// Make a segment with a start pointing to the end of buffer.
|
||||
seg := LinkSegment{
|
||||
start: r.i(),
|
||||
url: string(n.Destination),
|
||||
}
|
||||
|
||||
// Write the actual title.
|
||||
r.buf.Write(n.Title)
|
||||
start, end := r.write(n.Title)
|
||||
|
||||
// Close the segment.
|
||||
seg.end = r.i()
|
||||
r.append(seg)
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
string(n.Destination),
|
||||
})
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
|
@ -33,15 +30,13 @@ func (r *TextRenderer) link(n *ast.Link, enter bool) ast.WalkStatus {
|
|||
|
||||
func (r *TextRenderer) autoLink(n *ast.AutoLink, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
seg := LinkSegment{
|
||||
start: r.i(),
|
||||
url: string(n.URL(r.src)),
|
||||
}
|
||||
start, end := r.write(n.URL(r.src))
|
||||
|
||||
r.buf.Write(n.URL(r.src))
|
||||
|
||||
seg.end = r.i()
|
||||
r.append(seg)
|
||||
r.append(LinkSegment{
|
||||
start,
|
||||
end,
|
||||
string(n.URL((r.src))),
|
||||
})
|
||||
}
|
||||
|
||||
return ast.WalkContinue
|
||||
|
|
131
segments/md.go
131
segments/md.go
|
@ -2,7 +2,7 @@ package segments
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
|
@ -11,18 +11,22 @@ import (
|
|||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
type TextRenderer struct {
|
||||
buf *bytes.Buffer
|
||||
src []byte
|
||||
segs []text.Segment
|
||||
inls inlineState
|
||||
}
|
||||
|
||||
func ParseMessage(m *discord.Message, s state.Store) text.Rich {
|
||||
return ParseWithMessage([]byte(m.Content), s, m, true)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseWithMessage(b []byte, s state.Store, m *discord.Message, msg bool) text.Rich {
|
||||
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)
|
||||
}
|
||||
|
@ -33,28 +37,89 @@ func Parse(b []byte) text.Rich {
|
|||
}
|
||||
|
||||
func RenderNode(source []byte, n ast.Node) text.Rich {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(len(source))
|
||||
|
||||
r := TextRenderer{
|
||||
src: source,
|
||||
buf: buf,
|
||||
segs: make([]text.Segment, 0, n.ChildCount()),
|
||||
}
|
||||
|
||||
ast.Walk(n, r.renderNode)
|
||||
r := NewTextReader(source, n)
|
||||
r.walk(n)
|
||||
|
||||
return text.Rich{
|
||||
Content: buf.String(),
|
||||
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 {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(len(src))
|
||||
|
||||
return TextRenderer{
|
||||
src: src,
|
||||
buf: buf,
|
||||
segs: make([]text.Segment, 0, node.ChildCount()),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -71,7 +136,10 @@ func (r *TextRenderer) startBlock() {
|
|||
}
|
||||
|
||||
// Write the padding.
|
||||
r.buf.WriteString(strings.Repeat("\n", maxNewlines))
|
||||
r.buf.Grow(maxNewlines)
|
||||
for i := 0; i < maxNewlines; i++ {
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) endBlock() {
|
||||
|
@ -79,6 +147,8 @@ func (r *TextRenderer) endBlock() {
|
|||
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]
|
||||
|
@ -90,6 +160,23 @@ 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:
|
||||
|
|
44
urlutils/urlutils.go
Normal file
44
urlutils/urlutils.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package urlutils
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sized wraps the URL with the size query.
|
||||
func Sized(URL string, size int) string {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return URL
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Set("size", strconv.Itoa(size))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Ext returns the lowercased file extension of the URL.
|
||||
func Ext(URL string) string {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(path.Ext(u.Path))
|
||||
}
|
||||
|
||||
func ExtIs(URL string, exts []string) bool {
|
||||
var ext = Ext(URL)
|
||||
|
||||
for _, e := range exts {
|
||||
if e == ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Loading…
Reference in a new issue