github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/common/linkify.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // Copyright 2019 Yusuke Inuzuka 7 8 // Most of this file is a subtly changed version of github.com/yuin/goldmark/extension/linkify.go 9 10 package common 11 12 import ( 13 "bytes" 14 "regexp" 15 16 "github.com/yuin/goldmark" 17 "github.com/yuin/goldmark/ast" 18 "github.com/yuin/goldmark/parser" 19 "github.com/yuin/goldmark/text" 20 "github.com/yuin/goldmark/util" 21 ) 22 23 var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) 24 25 type linkifyParser struct{} 26 27 var defaultLinkifyParser = &linkifyParser{} 28 29 // NewLinkifyParser return a new InlineParser can parse 30 // text that seems like a URL. 31 func NewLinkifyParser() parser.InlineParser { 32 return defaultLinkifyParser 33 } 34 35 func (s *linkifyParser) Trigger() []byte { 36 // ' ' indicates any white spaces and a line head 37 return []byte{' ', '*', '_', '~', '('} 38 } 39 40 var ( 41 protoHTTP = []byte("http:") 42 protoHTTPS = []byte("https:") 43 protoFTP = []byte("ftp:") 44 domainWWW = []byte("www.") 45 ) 46 47 func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 48 if pc.IsInLinkLabel() { 49 return nil 50 } 51 line, segment := block.PeekLine() 52 consumes := 0 53 start := segment.Start 54 c := line[0] 55 // advance if current position is not a line head. 56 if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' { 57 consumes++ 58 start++ 59 line = line[1:] 60 } 61 62 var m []int 63 var protocol []byte 64 typ := ast.AutoLinkURL 65 if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) { 66 m = LinkRegex.FindSubmatchIndex(line) 67 } 68 if m == nil && bytes.HasPrefix(line, domainWWW) { 69 m = wwwURLRegxp.FindSubmatchIndex(line) 70 protocol = []byte("http") 71 } 72 if m != nil { 73 lastChar := line[m[1]-1] 74 if lastChar == '.' { 75 m[1]-- 76 } else if lastChar == ')' { 77 closing := 0 78 for i := m[1] - 1; i >= m[0]; i-- { 79 if line[i] == ')' { 80 closing++ 81 } else if line[i] == '(' { 82 closing-- 83 } 84 } 85 if closing > 0 { 86 m[1] -= closing 87 } 88 } else if lastChar == ';' { 89 i := m[1] - 2 90 for ; i >= m[0]; i-- { 91 if util.IsAlphaNumeric(line[i]) { 92 continue 93 } 94 break 95 } 96 if i != m[1]-2 { 97 if line[i] == '&' { 98 m[1] -= m[1] - i 99 } 100 } 101 } 102 } 103 if m == nil { 104 if len(line) > 0 && util.IsPunct(line[0]) { 105 return nil 106 } 107 typ = ast.AutoLinkEmail 108 stop := util.FindEmailIndex(line) 109 if stop < 0 { 110 return nil 111 } 112 at := bytes.IndexByte(line, '@') 113 m = []int{0, stop, at, stop - 1} 114 if bytes.IndexByte(line[m[2]:m[3]], '.') < 0 { 115 return nil 116 } 117 lastChar := line[m[1]-1] 118 if lastChar == '.' { 119 m[1]-- 120 } 121 if m[1] < len(line) { 122 nextChar := line[m[1]] 123 if nextChar == '-' || nextChar == '_' { 124 return nil 125 } 126 } 127 } 128 129 if consumes != 0 { 130 s := segment.WithStop(segment.Start + 1) 131 ast.MergeOrAppendTextSegment(parent, s) 132 } 133 consumes += m[1] 134 block.Advance(consumes) 135 n := ast.NewTextSegment(text.NewSegment(start, start+m[1])) 136 link := ast.NewAutoLink(typ, n) 137 link.Protocol = protocol 138 return link 139 } 140 141 func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) { 142 // nothing to do 143 } 144 145 type linkify struct{} 146 147 // Linkify is an extension that allow you to parse text that seems like a URL. 148 var Linkify = &linkify{} 149 150 func (e *linkify) Extend(m goldmark.Markdown) { 151 m.Parser().AddOptions( 152 parser.WithInlineParsers( 153 util.Prioritized(NewLinkifyParser(), 999), 154 ), 155 ) 156 }