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  }