321 lines
7.7 KiB
Go
321 lines
7.7 KiB
Go
package extension
|
|
|
|
import (
|
|
"bytes"
|
|
"regexp"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/text"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?:[/#?][-a-zA-Z0-9@:%_\+.~#!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
|
|
|
|
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
|
|
|
|
// An LinkifyConfig struct is a data structure that holds configuration of the
|
|
// Linkify extension.
|
|
type LinkifyConfig struct {
|
|
AllowedProtocols [][]byte
|
|
URLRegexp *regexp.Regexp
|
|
WWWRegexp *regexp.Regexp
|
|
EmailRegexp *regexp.Regexp
|
|
}
|
|
|
|
const (
|
|
optLinkifyAllowedProtocols parser.OptionName = "LinkifyAllowedProtocols"
|
|
optLinkifyURLRegexp parser.OptionName = "LinkifyURLRegexp"
|
|
optLinkifyWWWRegexp parser.OptionName = "LinkifyWWWRegexp"
|
|
optLinkifyEmailRegexp parser.OptionName = "LinkifyEmailRegexp"
|
|
)
|
|
|
|
// SetOption implements SetOptioner.
|
|
func (c *LinkifyConfig) SetOption(name parser.OptionName, value interface{}) {
|
|
switch name {
|
|
case optLinkifyAllowedProtocols:
|
|
c.AllowedProtocols = value.([][]byte)
|
|
case optLinkifyURLRegexp:
|
|
c.URLRegexp = value.(*regexp.Regexp)
|
|
case optLinkifyWWWRegexp:
|
|
c.WWWRegexp = value.(*regexp.Regexp)
|
|
case optLinkifyEmailRegexp:
|
|
c.EmailRegexp = value.(*regexp.Regexp)
|
|
}
|
|
}
|
|
|
|
// A LinkifyOption interface sets options for the LinkifyOption.
|
|
type LinkifyOption interface {
|
|
parser.Option
|
|
SetLinkifyOption(*LinkifyConfig)
|
|
}
|
|
|
|
type withLinkifyAllowedProtocols struct {
|
|
value [][]byte
|
|
}
|
|
|
|
func (o *withLinkifyAllowedProtocols) SetParserOption(c *parser.Config) {
|
|
c.Options[optLinkifyAllowedProtocols] = o.value
|
|
}
|
|
|
|
func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
|
|
p.AllowedProtocols = o.value
|
|
}
|
|
|
|
// WithLinkifyAllowedProtocols is a functional option that specify allowed
|
|
// protocols in autolinks. Each protocol must end with ':' like
|
|
// 'http:' .
|
|
func WithLinkifyAllowedProtocols(value [][]byte) LinkifyOption {
|
|
return &withLinkifyAllowedProtocols{
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
type withLinkifyURLRegexp struct {
|
|
value *regexp.Regexp
|
|
}
|
|
|
|
func (o *withLinkifyURLRegexp) SetParserOption(c *parser.Config) {
|
|
c.Options[optLinkifyURLRegexp] = o.value
|
|
}
|
|
|
|
func (o *withLinkifyURLRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
|
p.URLRegexp = o.value
|
|
}
|
|
|
|
// WithLinkifyURLRegexp is a functional option that specify
|
|
// a pattern of the URL including a protocol.
|
|
func WithLinkifyURLRegexp(value *regexp.Regexp) LinkifyOption {
|
|
return &withLinkifyURLRegexp{
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
type withLinkifyWWWRegexp struct {
|
|
value *regexp.Regexp
|
|
}
|
|
|
|
func (o *withLinkifyWWWRegexp) SetParserOption(c *parser.Config) {
|
|
c.Options[optLinkifyWWWRegexp] = o.value
|
|
}
|
|
|
|
func (o *withLinkifyWWWRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
|
p.WWWRegexp = o.value
|
|
}
|
|
|
|
// WithLinkifyWWWRegexp is a functional option that specify
|
|
// a pattern of the URL without a protocol.
|
|
// This pattern must start with 'www.' .
|
|
func WithLinkifyWWWRegexp(value *regexp.Regexp) LinkifyOption {
|
|
return &withLinkifyWWWRegexp{
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
type withLinkifyEmailRegexp struct {
|
|
value *regexp.Regexp
|
|
}
|
|
|
|
func (o *withLinkifyEmailRegexp) SetParserOption(c *parser.Config) {
|
|
c.Options[optLinkifyEmailRegexp] = o.value
|
|
}
|
|
|
|
func (o *withLinkifyEmailRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
|
p.EmailRegexp = o.value
|
|
}
|
|
|
|
// WithLinkifyEmailRegexp is a functional otpion that specify
|
|
// a pattern of the email address.
|
|
func WithLinkifyEmailRegexp(value *regexp.Regexp) LinkifyOption {
|
|
return &withLinkifyEmailRegexp{
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
type linkifyParser struct {
|
|
LinkifyConfig
|
|
}
|
|
|
|
// NewLinkifyParser return a new InlineParser can parse
|
|
// text that seems like a URL.
|
|
func NewLinkifyParser(opts ...LinkifyOption) parser.InlineParser {
|
|
p := &linkifyParser{
|
|
LinkifyConfig: LinkifyConfig{
|
|
AllowedProtocols: nil,
|
|
URLRegexp: urlRegexp,
|
|
WWWRegexp: wwwURLRegxp,
|
|
},
|
|
}
|
|
for _, o := range opts {
|
|
o.SetLinkifyOption(&p.LinkifyConfig)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (s *linkifyParser) Trigger() []byte {
|
|
// ' ' indicates any white spaces and a line head
|
|
return []byte{' ', '*', '_', '~', '('}
|
|
}
|
|
|
|
var (
|
|
protoHTTP = []byte("http:")
|
|
protoHTTPS = []byte("https:")
|
|
protoFTP = []byte("ftp:")
|
|
domainWWW = []byte("www.")
|
|
)
|
|
|
|
func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
|
if pc.IsInLinkLabel() {
|
|
return nil
|
|
}
|
|
line, segment := block.PeekLine()
|
|
consumes := 0
|
|
start := segment.Start
|
|
c := line[0]
|
|
// advance if current position is not a line head.
|
|
if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' {
|
|
consumes++
|
|
start++
|
|
line = line[1:]
|
|
}
|
|
|
|
var m []int
|
|
var protocol []byte
|
|
var typ ast.AutoLinkType = ast.AutoLinkURL
|
|
if s.LinkifyConfig.AllowedProtocols == nil {
|
|
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
|
|
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
|
|
}
|
|
} else {
|
|
for _, prefix := range s.LinkifyConfig.AllowedProtocols {
|
|
if bytes.HasPrefix(line, prefix) {
|
|
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if m == nil && bytes.HasPrefix(line, domainWWW) {
|
|
m = s.LinkifyConfig.WWWRegexp.FindSubmatchIndex(line)
|
|
protocol = []byte("http")
|
|
}
|
|
if m != nil && m[0] != 0 {
|
|
m = nil
|
|
}
|
|
if m != nil && m[0] == 0 {
|
|
lastChar := line[m[1]-1]
|
|
if lastChar == '.' {
|
|
m[1]--
|
|
} else if lastChar == ')' {
|
|
closing := 0
|
|
for i := m[1] - 1; i >= m[0]; i-- {
|
|
if line[i] == ')' {
|
|
closing++
|
|
} else if line[i] == '(' {
|
|
closing--
|
|
}
|
|
}
|
|
if closing > 0 {
|
|
m[1] -= closing
|
|
}
|
|
} else if lastChar == ';' {
|
|
i := m[1] - 2
|
|
for ; i >= m[0]; i-- {
|
|
if util.IsAlphaNumeric(line[i]) {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if i != m[1]-2 {
|
|
if line[i] == '&' {
|
|
m[1] -= m[1] - i
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if m == nil {
|
|
if len(line) > 0 && util.IsPunct(line[0]) {
|
|
return nil
|
|
}
|
|
typ = ast.AutoLinkEmail
|
|
stop := -1
|
|
if s.LinkifyConfig.EmailRegexp == nil {
|
|
stop = util.FindEmailIndex(line)
|
|
} else {
|
|
m := s.LinkifyConfig.EmailRegexp.FindSubmatchIndex(line)
|
|
if m != nil && m[0] == 0 {
|
|
stop = m[1]
|
|
}
|
|
}
|
|
if stop < 0 {
|
|
return nil
|
|
}
|
|
at := bytes.IndexByte(line, '@')
|
|
m = []int{0, stop, at, stop - 1}
|
|
if m == nil || bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
|
|
return nil
|
|
}
|
|
lastChar := line[m[1]-1]
|
|
if lastChar == '.' {
|
|
m[1]--
|
|
}
|
|
if m[1] < len(line) {
|
|
nextChar := line[m[1]]
|
|
if nextChar == '-' || nextChar == '_' {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
if consumes != 0 {
|
|
s := segment.WithStop(segment.Start + 1)
|
|
ast.MergeOrAppendTextSegment(parent, s)
|
|
}
|
|
i := m[1] - 1
|
|
for ; i > 0; i-- {
|
|
c := line[i]
|
|
switch c {
|
|
case '?', '!', '.', ',', ':', '*', '_', '~':
|
|
default:
|
|
goto endfor
|
|
}
|
|
}
|
|
endfor:
|
|
i++
|
|
consumes += i
|
|
block.Advance(consumes)
|
|
n := ast.NewTextSegment(text.NewSegment(start, start+i))
|
|
link := ast.NewAutoLink(typ, n)
|
|
link.Protocol = protocol
|
|
return link
|
|
}
|
|
|
|
func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
|
// nothing to do
|
|
}
|
|
|
|
type linkify struct {
|
|
options []LinkifyOption
|
|
}
|
|
|
|
// Linkify is an extension that allow you to parse text that seems like a URL.
|
|
var Linkify = &linkify{}
|
|
|
|
// NewLinkify creates a new [goldmark.Extender] that
|
|
// allow you to parse text that seems like a URL.
|
|
func NewLinkify(opts ...LinkifyOption) goldmark.Extender {
|
|
return &linkify{
|
|
options: opts,
|
|
}
|
|
}
|
|
|
|
func (e *linkify) Extend(m goldmark.Markdown) {
|
|
m.Parser().AddOptions(
|
|
parser.WithInlineParsers(
|
|
util.Prioritized(NewLinkifyParser(e.options...), 999),
|
|
),
|
|
)
|
|
}
|