code.gitea.io/gitea@v1.19.3/modules/markup/markdown/math/inline_parser.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package math
     5  
     6  import (
     7  	"bytes"
     8  
     9  	"github.com/yuin/goldmark/ast"
    10  	"github.com/yuin/goldmark/parser"
    11  	"github.com/yuin/goldmark/text"
    12  )
    13  
    14  type inlineParser struct {
    15  	start []byte
    16  	end   []byte
    17  }
    18  
    19  var defaultInlineDollarParser = &inlineParser{
    20  	start: []byte{'$'},
    21  	end:   []byte{'$'},
    22  }
    23  
    24  // NewInlineDollarParser returns a new inline parser
    25  func NewInlineDollarParser() parser.InlineParser {
    26  	return defaultInlineDollarParser
    27  }
    28  
    29  var defaultInlineBracketParser = &inlineParser{
    30  	start: []byte{'\\', '('},
    31  	end:   []byte{'\\', ')'},
    32  }
    33  
    34  // NewInlineDollarParser returns a new inline parser
    35  func NewInlineBracketParser() parser.InlineParser {
    36  	return defaultInlineBracketParser
    37  }
    38  
    39  // Trigger triggers this parser on $ or \
    40  func (parser *inlineParser) Trigger() []byte {
    41  	return parser.start[0:1]
    42  }
    43  
    44  func isAlphanumeric(b byte) bool {
    45  	// Github only cares about 0-9A-Za-z
    46  	return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
    47  }
    48  
    49  // Parse parses the current line and returns a result of parsing.
    50  func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
    51  	line, _ := block.PeekLine()
    52  
    53  	if !bytes.HasPrefix(line, parser.start) {
    54  		// We'll catch this one on the next time round
    55  		return nil
    56  	}
    57  
    58  	precedingCharacter := block.PrecendingCharacter()
    59  	if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
    60  		// need to exclude things like `a$` from being considered a start
    61  		return nil
    62  	}
    63  
    64  	// move the opener marker point at the start of the text
    65  	opener := len(parser.start)
    66  
    67  	// Now look for an ending line
    68  	ender := opener
    69  	for {
    70  		pos := bytes.Index(line[ender:], parser.end)
    71  		if pos < 0 {
    72  			return nil
    73  		}
    74  
    75  		ender += pos
    76  
    77  		// Now we want to check the character at the end of our parser section
    78  		// that is ender + len(parser.end)
    79  		pos = ender + len(parser.end)
    80  		if len(line) <= pos {
    81  			break
    82  		}
    83  		if !isAlphanumeric(line[pos]) {
    84  			break
    85  		}
    86  		// move the pointer onwards
    87  		ender += len(parser.end)
    88  	}
    89  
    90  	block.Advance(opener)
    91  	_, pos := block.Position()
    92  	node := NewInline()
    93  	segment := pos.WithStop(pos.Start + ender - opener)
    94  	node.AppendChild(node, ast.NewRawTextSegment(segment))
    95  	block.Advance(ender - opener + len(parser.end))
    96  
    97  	trimBlock(node, block)
    98  	return node
    99  }
   100  
   101  func trimBlock(node *Inline, block text.Reader) {
   102  	if node.IsBlank(block.Source()) {
   103  		return
   104  	}
   105  
   106  	// trim first space and last space
   107  	first := node.FirstChild().(*ast.Text)
   108  	if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') {
   109  		return
   110  	}
   111  
   112  	last := node.LastChild().(*ast.Text)
   113  	if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') {
   114  		return
   115  	}
   116  
   117  	first.Segment = first.Segment.WithStart(first.Segment.Start + 1)
   118  	last.Segment = last.Segment.WithStop(last.Segment.Stop - 1)
   119  }