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