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  }