code.gitea.io/gitea@v1.22.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 isPunctuation(b byte) bool {
    45  	return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
    46  }
    47  
    48  func isBracket(b byte) bool {
    49  	return b == ')'
    50  }
    51  
    52  func isAlphanumeric(b byte) bool {
    53  	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
    54  }
    55  
    56  // Parse parses the current line and returns a result of parsing.
    57  func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
    58  	line, _ := block.PeekLine()
    59  
    60  	if !bytes.HasPrefix(line, parser.start) {
    61  		// We'll catch this one on the next time round
    62  		return nil
    63  	}
    64  
    65  	precedingCharacter := block.PrecendingCharacter()
    66  	if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
    67  		// need to exclude things like `a$` from being considered a start
    68  		return nil
    69  	}
    70  
    71  	// move the opener marker point at the start of the text
    72  	opener := len(parser.start)
    73  
    74  	// Now look for an ending line
    75  	ender := opener
    76  	for {
    77  		pos := bytes.Index(line[ender:], parser.end)
    78  		if pos < 0 {
    79  			return nil
    80  		}
    81  
    82  		ender += pos
    83  
    84  		// Now we want to check the character at the end of our parser section
    85  		// that is ender + len(parser.end) and check if char before ender is '\'
    86  		pos = ender + len(parser.end)
    87  		if len(line) <= pos {
    88  			break
    89  		}
    90  		suceedingCharacter := line[pos]
    91  		if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') && !isBracket(suceedingCharacter) {
    92  			return nil
    93  		}
    94  		if line[ender-1] != '\\' {
    95  			break
    96  		}
    97  
    98  		// move the pointer onwards
    99  		ender += len(parser.end)
   100  	}
   101  
   102  	block.Advance(opener)
   103  	_, pos := block.Position()
   104  	node := NewInline()
   105  	segment := pos.WithStop(pos.Start + ender - opener)
   106  	node.AppendChild(node, ast.NewRawTextSegment(segment))
   107  	block.Advance(ender - opener + len(parser.end))
   108  
   109  	trimBlock(node, block)
   110  	return node
   111  }
   112  
   113  func trimBlock(node *Inline, block text.Reader) {
   114  	if node.IsBlank(block.Source()) {
   115  		return
   116  	}
   117  
   118  	// trim first space and last space
   119  	first := node.FirstChild().(*ast.Text)
   120  	if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') {
   121  		return
   122  	}
   123  
   124  	last := node.LastChild().(*ast.Text)
   125  	if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') {
   126  		return
   127  	}
   128  
   129  	first.Segment = first.Segment.WithStart(first.Segment.Start + 1)
   130  	last.Segment = last.Segment.WithStop(last.Segment.Stop - 1)
   131  }