mirror of
https://github.com/diamondburned/cchat-discord.git
synced 2024-12-22 20:36:45 +00:00
cchat to v0.0.42; added embed and attachment support; colored mentions
This commit is contained in:
parent
bea81ed346
commit
d0e43cc63b
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/diamondburned/arikawa v0.9.5
|
||||
github.com/diamondburned/cchat v0.0.41
|
||||
github.com/diamondburned/cchat v0.0.42
|
||||
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
|
||||
|
|
2
go.sum
2
go.sum
|
@ -39,6 +39,8 @@ github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0
|
|||
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/cchat v0.0.42 h1:FVMLy9hOTxKju8OWDBIStrekbgTHCaH8+GVnV4LOByg=
|
||||
github.com/diamondburned/cchat v0.0.42/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=
|
||||
|
|
|
@ -54,7 +54,7 @@ func (m messageHeader) Time() time.Time {
|
|||
|
||||
// AvatarURL wraps the URL with URL queries for the avatar.
|
||||
func AvatarURL(URL string) string {
|
||||
return urlutils.Sized(URL, 64)
|
||||
return urlutils.AvatarURL(URL)
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
|
|
|
@ -17,7 +17,7 @@ func (r *TextRenderer) blockquote(n *ast.Blockquote, enter bool) ast.WalkStatus
|
|||
defer r.endBlock()
|
||||
|
||||
// Create a segment.
|
||||
var seg = BlockquoteSegment{start: r.i()}
|
||||
var seg = BlockquoteSegment{start: r.buf.Len()}
|
||||
|
||||
// A blockquote contains a paragraph each line. Because Discord.
|
||||
for child := n.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
|
@ -34,7 +34,7 @@ func (r *TextRenderer) blockquote(n *ast.Blockquote, enter bool) ast.WalkStatus
|
|||
}
|
||||
|
||||
// Write the end of the segment.
|
||||
seg.end = r.i()
|
||||
seg.end = r.buf.Len()
|
||||
r.append(seg)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkSta
|
|||
|
||||
// Create a segment.
|
||||
seg := CodeblockSegment{
|
||||
start: r.i(),
|
||||
start: r.buf.Len(),
|
||||
language: string(n.Language(r.src)),
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ func (r *TextRenderer) codeblock(n *ast.FencedCodeBlock, enter bool) ast.WalkSta
|
|||
}
|
||||
|
||||
// Close the segment.
|
||||
seg.end = r.i()
|
||||
seg.end = r.buf.Len()
|
||||
r.append(seg)
|
||||
|
||||
// Close the block.
|
||||
|
|
|
@ -3,8 +3,9 @@ package segments
|
|||
import "github.com/diamondburned/cchat/text"
|
||||
|
||||
type Colored struct {
|
||||
strlen int
|
||||
color uint32
|
||||
start int
|
||||
end int
|
||||
color uint32
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -13,11 +14,15 @@ var (
|
|||
)
|
||||
|
||||
func NewColored(strlen int, color uint32) Colored {
|
||||
return Colored{strlen, color}
|
||||
return Colored{0, strlen, color}
|
||||
}
|
||||
|
||||
func NewColoredSegment(start, end int, color uint32) Colored {
|
||||
return Colored{start, end, color}
|
||||
}
|
||||
|
||||
func (color Colored) Bounds() (start, end int) {
|
||||
return 0, color.strlen
|
||||
return color.start, color.end
|
||||
}
|
||||
|
||||
func (color Colored) Color() uint32 {
|
|
@ -28,7 +28,7 @@ func (r *TextRenderer) renderEmbeds(embeds []discord.Embed, m *discord.Message,
|
|||
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.append(EmbedAuthor(r.buf.Len(), *a))
|
||||
r.buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
|
@ -57,6 +57,12 @@ func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s st
|
|||
}
|
||||
}
|
||||
|
||||
// If we have a thumbnail, then write one.
|
||||
if embed.Thumbnail != nil {
|
||||
r.append(EmbedThumbnail(r.buf.Len(), *embed.Thumbnail))
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
if embed.Description != "" {
|
||||
// Since Discord embeds' descriptions are technically Markdown, we can
|
||||
// borrow our Markdown parser for this.
|
||||
|
@ -84,7 +90,7 @@ func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s st
|
|||
|
||||
if f := embed.Footer; f != nil && f.Text != "" {
|
||||
if f.ProxyIcon != "" {
|
||||
r.append(EmbedFooter(r.i(), *f))
|
||||
r.append(EmbedFooter(r.buf.Len(), *f))
|
||||
r.buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
|
@ -100,6 +106,12 @@ func (r *TextRenderer) renderEmbed(embed discord.Embed, m *discord.Message, s st
|
|||
r.buf.WriteString(embed.Timestamp.Format(time.RFC1123))
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
// Write an image if there's one.
|
||||
if embed.Image != nil {
|
||||
r.append(EmbedImage(r.buf.Len(), *embed.Image))
|
||||
r.buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
|
||||
|
@ -108,8 +120,8 @@ func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
|
|||
return
|
||||
}
|
||||
|
||||
// Start a new block before rendering attachments.
|
||||
r.startBlock()
|
||||
// Start a (small) new block before rendering attachments.
|
||||
r.startBlockN(1)
|
||||
|
||||
// Render all attachments. Newline delimited.
|
||||
for i, attachment := range attachments {
|
||||
|
@ -123,7 +135,7 @@ func (r *TextRenderer) renderAttachments(attachments []discord.Attachment) {
|
|||
|
||||
func (r *TextRenderer) renderAttachment(a discord.Attachment) {
|
||||
if urlutils.ExtIs(a.Proxy, imageExts) {
|
||||
r.append(EmbedAttachment(r.i(), a))
|
||||
r.append(EmbedAttachment(r.buf.Len(), a))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -187,23 +199,23 @@ type ImageSegment struct {
|
|||
text string
|
||||
}
|
||||
|
||||
func EmbedImage(start int, i discord.EmbedImage, text string) ImageSegment {
|
||||
func EmbedImage(start int, i discord.EmbedImage) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: i.Proxy,
|
||||
w: int(i.Width),
|
||||
h: int(i.Height),
|
||||
text: text,
|
||||
text: fmt.Sprintf("Image (%s)", urlutils.Name(i.URL)),
|
||||
}
|
||||
}
|
||||
|
||||
func EmbedThumbnail(start int, t discord.EmbedThumbnail, text string) ImageSegment {
|
||||
func EmbedThumbnail(start int, t discord.EmbedThumbnail) ImageSegment {
|
||||
return ImageSegment{
|
||||
start: start,
|
||||
url: t.Proxy,
|
||||
w: int(t.Width),
|
||||
h: int(t.Height),
|
||||
text: text,
|
||||
text: fmt.Sprintf("Thumbnail (%s)", urlutils.Name(t.URL)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ var _ text.Imager = (*EmojiSegment)(nil)
|
|||
func (r *TextRenderer) emoji(n *md.Emoji, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
r.append(EmojiSegment{
|
||||
start: r.i(),
|
||||
start: r.buf.Len(),
|
||||
name: n.Name,
|
||||
large: n.Large,
|
||||
emojiURL: n.EmojiURL() + "&size=64",
|
||||
|
|
|
@ -71,7 +71,7 @@ func (r *TextRenderer) inline(n *md.Inline, enter bool) ast.WalkStatus {
|
|||
|
||||
// Pop the last segment if it's not empty.
|
||||
if !r.inls.empty() {
|
||||
r.inls.end = r.i()
|
||||
r.inls.end = r.buf.Len()
|
||||
|
||||
// Only use this section if the length is not zero.
|
||||
if r.inls.start != r.inls.end {
|
||||
|
@ -86,7 +86,7 @@ func (r *TextRenderer) inline(n *md.Inline, enter bool) ast.WalkStatus {
|
|||
}
|
||||
|
||||
// Update the start pointer of the current segment.
|
||||
r.inls.start = r.i()
|
||||
r.inls.start = r.buf.Len()
|
||||
|
||||
return ast.WalkContinue
|
||||
}
|
||||
|
|
|
@ -16,9 +16,13 @@ func ParseMessage(m *discord.Message, s state.Store) text.Rich {
|
|||
var node = md.ParseWithMessage(content, s, m, true)
|
||||
|
||||
r := NewTextReader(content, node)
|
||||
// Register the needed states for some renderers.
|
||||
r.WithState(m, s)
|
||||
// Render the main message body.
|
||||
r.walk(node)
|
||||
r.renderEmbeds(m.Embeds, m, s)
|
||||
// Render the extra bits.
|
||||
r.renderAttachments(m.Attachments)
|
||||
r.renderEmbeds(m.Embeds, m, s)
|
||||
|
||||
return text.Rich{
|
||||
Content: r.String(),
|
||||
|
@ -51,6 +55,10 @@ type TextRenderer struct {
|
|||
src []byte
|
||||
segs []text.Segment
|
||||
inls inlineState
|
||||
|
||||
// these fields can be nil
|
||||
msg *discord.Message
|
||||
store state.Store
|
||||
}
|
||||
|
||||
func NewTextReader(src []byte, node ast.Node) TextRenderer {
|
||||
|
@ -64,6 +72,11 @@ func NewTextReader(src []byte, node ast.Node) TextRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
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())
|
||||
|
@ -97,41 +110,34 @@ func (r *TextRenderer) Bytes() []byte {
|
|||
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
|
||||
return writestringbuf(r.buf, s)
|
||||
}
|
||||
|
||||
func (r *TextRenderer) write(b []byte) (start, end int) {
|
||||
start = r.i()
|
||||
r.buf.Write(b)
|
||||
end = r.i()
|
||||
return
|
||||
return writebuf(r.buf, b)
|
||||
}
|
||||
|
||||
// startBlock guarantees enough indentation to start a new block.
|
||||
func (r *TextRenderer) startBlock() {
|
||||
r.startBlockN(2)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if r.peekLast(0) != '\n' {
|
||||
maxNewlines++
|
||||
}
|
||||
if r.peekLast(1) != '\n' {
|
||||
maxNewlines++
|
||||
for i := 0; i < n; i++ {
|
||||
if r.peekLast(i) != '\n' {
|
||||
maxNewlines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,3 +224,23 @@ func (r *TextRenderer) renderNode(n ast.Node, enter bool) (ast.WalkStatus, error
|
|||
|
||||
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...)
|
||||
}
|
||||
|
|
|
@ -1,37 +1,86 @@
|
|||
package segments
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/ningen/md"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
const (
|
||||
mentionChannel uint8 = iota
|
||||
mentionUser
|
||||
mentionRole
|
||||
)
|
||||
const blurple = 0x7289DA
|
||||
|
||||
type roleInfo struct {
|
||||
name string
|
||||
color uint32
|
||||
position int // used for sorting
|
||||
}
|
||||
|
||||
func (r *TextRenderer) userRoles(user *discord.GuildUser) []roleInfo {
|
||||
if user.Member == nil || r.msg == nil || !r.msg.GuildID.Valid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var roles = make([]roleInfo, 0, len(user.Member.RoleIDs))
|
||||
|
||||
for _, roleID := range user.Member.RoleIDs {
|
||||
r, err := r.store.Role(r.msg.GuildID, roleID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
roles = append(roles, roleInfo{
|
||||
name: r.Name,
|
||||
color: r.Color.Uint32(), // default 0
|
||||
position: r.Position,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort the roles so the first roles stay in front. We need to do this to
|
||||
// both render properly and to get the right role color.
|
||||
sort.Slice(roles, func(i, j int) bool {
|
||||
return roles[i].position < roles[j].position
|
||||
})
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
type MentionSegment struct {
|
||||
start, end int
|
||||
*md.Mention
|
||||
|
||||
// only non-nil if GuildUser is not nil and is in a guild.
|
||||
roles []roleInfo
|
||||
}
|
||||
|
||||
var _ text.Segment = (*MentionSegment)(nil)
|
||||
var (
|
||||
_ text.Segment = (*MentionSegment)(nil)
|
||||
_ text.Colorer = (*MentionSegment)(nil)
|
||||
_ text.Mentioner = (*MentionSegment)(nil)
|
||||
)
|
||||
|
||||
func (r *TextRenderer) mention(n *md.Mention, enter bool) ast.WalkStatus {
|
||||
if enter {
|
||||
seg := MentionSegment{start: r.i()}
|
||||
var seg = MentionSegment{Mention: n}
|
||||
|
||||
switch {
|
||||
case n.Channel != nil:
|
||||
r.buf.WriteString("#" + n.Channel.Name)
|
||||
seg.start, seg.end = r.writeString("#" + n.Channel.Name)
|
||||
case n.GuildUser != nil:
|
||||
r.buf.WriteString("@" + n.GuildUser.Username)
|
||||
seg.start, seg.end = r.writeString("@" + n.GuildUser.Username)
|
||||
seg.roles = r.userRoles(n.GuildUser) // get roles as well
|
||||
case n.GuildRole != nil:
|
||||
r.buf.WriteString("@" + n.GuildRole.Name)
|
||||
seg.start, seg.end = r.writeString("@" + n.GuildRole.Name)
|
||||
default:
|
||||
// Unexpected error; skip.
|
||||
return ast.WalkSkipChildren
|
||||
}
|
||||
|
||||
seg.end = r.i()
|
||||
r.append(seg)
|
||||
}
|
||||
|
||||
|
@ -42,7 +91,127 @@ 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() uint32 {
|
||||
// Try digging through what we have for a color.
|
||||
switch {
|
||||
case len(m.roles) > 0:
|
||||
for _, role := range m.roles {
|
||||
if role.color > 0 {
|
||||
return role.color
|
||||
}
|
||||
}
|
||||
case m.GuildRole != nil && m.GuildRole.Color > 0:
|
||||
return m.GuildRole.Color.Uint32()
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
func (m MentionSegment) channelInfo() text.Rich {
|
||||
content := strings.Builder{}
|
||||
content.WriteByte('#')
|
||||
content.WriteString(m.Channel.Name)
|
||||
|
||||
if m.Channel.NSFW {
|
||||
content.WriteString(" (NSFW)")
|
||||
}
|
||||
|
||||
if m.Channel.Topic != "" {
|
||||
content.WriteByte('\n')
|
||||
content.WriteString(m.Channel.Topic)
|
||||
}
|
||||
|
||||
return text.Rich{
|
||||
Content: content.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m MentionSegment) userInfo() text.Rich {
|
||||
var content bytes.Buffer
|
||||
var segment text.Rich
|
||||
|
||||
// Make a large avatar if there's one.
|
||||
if m.GuildUser != nil {
|
||||
segment.Segments = append(segment.Segments, AvatarSegment{
|
||||
start: 0,
|
||||
url: urlutils.AvatarURL(m.GuildUser.AvatarURL()),
|
||||
text: "Avatar",
|
||||
})
|
||||
// Space out.
|
||||
content.WriteByte(' ')
|
||||
}
|
||||
|
||||
// Write the nickname if there's one; else, write the username only.
|
||||
if m.GuildUser.Member != nil && m.GuildUser.Member.Nick != "" {
|
||||
content.WriteString(m.GuildUser.Member.Nick)
|
||||
content.WriteByte(' ')
|
||||
|
||||
start, end := writestringbuf(&content, fmt.Sprintf(
|
||||
"(%s#%s)",
|
||||
m.GuildUser.Username,
|
||||
m.GuildUser.Discriminator,
|
||||
))
|
||||
|
||||
segmentadd(&segment, InlineSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
attributes: text.AttrDimmed,
|
||||
})
|
||||
} else {
|
||||
content.WriteString(m.GuildUser.Username)
|
||||
content.WriteByte('#')
|
||||
content.WriteString(m.GuildUser.Discriminator)
|
||||
}
|
||||
|
||||
// Write roles, if any.
|
||||
if len(m.roles) > 0 {
|
||||
// Write a prepended new line, as role writes will always prepend a new
|
||||
// line. This is to prevent a trailing new line.
|
||||
content.WriteString("\n---\nRoles")
|
||||
|
||||
for _, role := range m.roles {
|
||||
// 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, role.name)
|
||||
segmentadd(&segment, NewColoredSegment(start, end, role.color))
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the written content into the text segment and return it.
|
||||
segment.Content = content.String()
|
||||
return segment
|
||||
}
|
||||
|
||||
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 segment
|
||||
}
|
||||
|
|
|
@ -7,6 +7,11 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// AvatarURL wraps the URL with URL queries for the avatar.
|
||||
func AvatarURL(URL string) string {
|
||||
return Sized(URL, 64)
|
||||
}
|
||||
|
||||
// Sized wraps the URL with the size query.
|
||||
func Sized(URL string, size int) string {
|
||||
u, err := url.Parse(URL)
|
||||
|
@ -31,6 +36,14 @@ func Ext(URL string) string {
|
|||
return strings.ToLower(path.Ext(u.Path))
|
||||
}
|
||||
|
||||
func Name(URL string) string {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return URL
|
||||
}
|
||||
return path.Base(u.Path)
|
||||
}
|
||||
|
||||
func ExtIs(URL string, exts []string) bool {
|
||||
var ext = Ext(URL)
|
||||
|
||||
|
|
Loading…
Reference in a new issue