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@:%_\+.~#!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?` )
var urlRegexp = regexp .MustCompile (`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?` )
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"
)
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 )
}
}
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
}
func WithLinkifyAllowedProtocols [T []byte | string ](value []T ) LinkifyOption {
opt := &withLinkifyAllowedProtocols {}
for _ , v := range value {
opt .value = append (opt .value , []byte (v ))
}
return opt
}
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
}
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
}
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
}
func WithLinkifyEmailRegexp (value *regexp .Regexp ) LinkifyOption {
return &withLinkifyEmailRegexp {
value : value ,
}
}
type linkifyParser struct {
LinkifyConfig
}
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 {
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 ]
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 ) {
}
type linkify struct {
options []LinkifyOption
}
var Linkify = &linkify {}
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 ),
),
)
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .