github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/yaml/printer/printer.go (about)

     1  package printer
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"strings"
     7  
     8  	"github.com/bingoohuang/gg/pkg/yaml/ast"
     9  	"github.com/bingoohuang/gg/pkg/yaml/token"
    10  	"github.com/fatih/color"
    11  )
    12  
    13  // Property additional property set for each the token
    14  type Property struct {
    15  	Prefix string
    16  	Suffix string
    17  }
    18  
    19  // PrintFunc returns property instance
    20  type PrintFunc func() *Property
    21  
    22  // Printer create text from token collection or ast
    23  type Printer struct {
    24  	LineNumber       bool
    25  	LineNumberFormat func(num int) string
    26  	MapKey           PrintFunc
    27  	Anchor           PrintFunc
    28  	Alias            PrintFunc
    29  	Bool             PrintFunc
    30  	String           PrintFunc
    31  	Number           PrintFunc
    32  }
    33  
    34  func defaultLineNumberFormat(num int) string {
    35  	return fmt.Sprintf("%2d | ", num)
    36  }
    37  
    38  func (p *Printer) property(tk *token.Token) *Property {
    39  	prop := &Property{}
    40  	switch tk.PreviousType() {
    41  	case token.AnchorType:
    42  		if p.Anchor != nil {
    43  			return p.Anchor()
    44  		}
    45  		return prop
    46  	case token.AliasType:
    47  		if p.Alias != nil {
    48  			return p.Alias()
    49  		}
    50  		return prop
    51  	}
    52  	switch tk.NextType() {
    53  	case token.MappingValueType:
    54  		if p.MapKey != nil {
    55  			return p.MapKey()
    56  		}
    57  		return prop
    58  	}
    59  	switch tk.Type {
    60  	case token.BoolType:
    61  		if p.Bool != nil {
    62  			return p.Bool()
    63  		}
    64  		return prop
    65  	case token.AnchorType:
    66  		if p.Anchor != nil {
    67  			return p.Anchor()
    68  		}
    69  		return prop
    70  	case token.AliasType:
    71  		if p.Anchor != nil {
    72  			return p.Alias()
    73  		}
    74  		return prop
    75  	case token.StringType, token.SingleQuoteType, token.DoubleQuoteType:
    76  		if p.String != nil {
    77  			return p.String()
    78  		}
    79  		return prop
    80  	case token.IntegerType, token.FloatType:
    81  		if p.Number != nil {
    82  			return p.Number()
    83  		}
    84  		return prop
    85  	default:
    86  	}
    87  	return prop
    88  }
    89  
    90  // PrintTokens create text from token collection
    91  func (p *Printer) PrintTokens(tokens token.Tokens) string {
    92  	if len(tokens) == 0 {
    93  		return ""
    94  	}
    95  	if p.LineNumber {
    96  		if p.LineNumberFormat == nil {
    97  			p.LineNumberFormat = defaultLineNumberFormat
    98  		}
    99  	}
   100  	texts := []string{}
   101  	lineNumber := tokens[0].Position.Line
   102  	for _, tk := range tokens {
   103  		lines := strings.Split(tk.Origin, "\n")
   104  		prop := p.property(tk)
   105  		header := ""
   106  		if p.LineNumber {
   107  			header = p.LineNumberFormat(lineNumber)
   108  		}
   109  		if len(lines) == 1 {
   110  			line := prop.Prefix + lines[0] + prop.Suffix
   111  			if len(texts) == 0 {
   112  				texts = append(texts, header+line)
   113  				lineNumber++
   114  			} else {
   115  				text := texts[len(texts)-1]
   116  				texts[len(texts)-1] = text + line
   117  			}
   118  		} else {
   119  			for idx, src := range lines {
   120  				if p.LineNumber {
   121  					header = p.LineNumberFormat(lineNumber)
   122  				}
   123  				line := prop.Prefix + src + prop.Suffix
   124  				if idx == 0 {
   125  					if len(texts) == 0 {
   126  						texts = append(texts, header+line)
   127  						lineNumber++
   128  					} else {
   129  						text := texts[len(texts)-1]
   130  						texts[len(texts)-1] = text + line
   131  					}
   132  				} else {
   133  					texts = append(texts, fmt.Sprintf("%s%s", header, line))
   134  					lineNumber++
   135  				}
   136  			}
   137  		}
   138  	}
   139  	return strings.Join(texts, "\n")
   140  }
   141  
   142  // PrintNode create text from ast.Node
   143  func (p *Printer) PrintNode(node ast.Node) []byte {
   144  	return []byte(fmt.Sprintf("%+v\n", node))
   145  }
   146  
   147  const escape = "\x1b"
   148  
   149  func format(attr color.Attribute) string {
   150  	return fmt.Sprintf("%s[%dm", escape, attr)
   151  }
   152  
   153  func (p *Printer) setDefaultColorSet() {
   154  	p.Bool = func() *Property {
   155  		return &Property{
   156  			Prefix: format(color.FgHiMagenta),
   157  			Suffix: format(color.Reset),
   158  		}
   159  	}
   160  	p.Number = func() *Property {
   161  		return &Property{
   162  			Prefix: format(color.FgHiMagenta),
   163  			Suffix: format(color.Reset),
   164  		}
   165  	}
   166  	p.MapKey = func() *Property {
   167  		return &Property{
   168  			Prefix: format(color.FgHiCyan),
   169  			Suffix: format(color.Reset),
   170  		}
   171  	}
   172  	p.Anchor = func() *Property {
   173  		return &Property{
   174  			Prefix: format(color.FgHiYellow),
   175  			Suffix: format(color.Reset),
   176  		}
   177  	}
   178  	p.Alias = func() *Property {
   179  		return &Property{
   180  			Prefix: format(color.FgHiYellow),
   181  			Suffix: format(color.Reset),
   182  		}
   183  	}
   184  	p.String = func() *Property {
   185  		return &Property{
   186  			Prefix: format(color.FgHiGreen),
   187  			Suffix: format(color.Reset),
   188  		}
   189  	}
   190  }
   191  
   192  func (p *Printer) PrintErrorMessage(msg string, isColored bool) string {
   193  	if isColored {
   194  		return fmt.Sprintf("%s%s%s",
   195  			format(color.FgHiRed),
   196  			msg,
   197  			format(color.Reset),
   198  		)
   199  	}
   200  	return msg
   201  }
   202  
   203  func (p *Printer) removeLeftSideNewLineChar(src string) string {
   204  	return strings.TrimLeft(strings.TrimLeft(strings.TrimLeft(src, "\r"), "\n"), "\r\n")
   205  }
   206  
   207  func (p *Printer) removeRightSideNewLineChar(src string) string {
   208  	return strings.TrimRight(strings.TrimRight(strings.TrimRight(src, "\r"), "\n"), "\r\n")
   209  }
   210  
   211  func (p *Printer) removeRightSideWhiteSpaceChar(src string) string {
   212  	return p.removeRightSideNewLineChar(strings.TrimRight(src, " "))
   213  }
   214  
   215  func (p *Printer) newLineCount(s string) int {
   216  	src := []rune(s)
   217  	size := len(src)
   218  	cnt := 0
   219  	for i := 0; i < size; i++ {
   220  		c := src[i]
   221  		switch c {
   222  		case '\r':
   223  			if i+1 < size && src[i+1] == '\n' {
   224  				i++
   225  			}
   226  			cnt++
   227  		case '\n':
   228  			cnt++
   229  		}
   230  	}
   231  	return cnt
   232  }
   233  
   234  func (p *Printer) isNewLineLastChar(s string) bool {
   235  	for i := len(s) - 1; i > 0; i-- {
   236  		c := s[i]
   237  		switch c {
   238  		case ' ':
   239  			continue
   240  		case '\n', '\r':
   241  			return true
   242  		}
   243  		break
   244  	}
   245  	return false
   246  }
   247  
   248  func (p *Printer) printBeforeTokens(tk *token.Token, minLine, extLine int) token.Tokens {
   249  	for {
   250  		if tk.Prev == nil {
   251  			break
   252  		}
   253  		if tk.Prev.Position.Line < minLine {
   254  			break
   255  		}
   256  		tk = tk.Prev
   257  	}
   258  	minTk := tk.Clone()
   259  	if minTk.Prev != nil {
   260  		// add white spaces to minTk by prev token
   261  		prev := minTk.Prev
   262  		whiteSpaceLen := len(prev.Origin) - len(strings.TrimRight(prev.Origin, " "))
   263  		minTk.Origin = strings.Repeat(" ", whiteSpaceLen) + minTk.Origin
   264  	}
   265  	minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin)
   266  	tokens := token.Tokens{minTk}
   267  	tk = minTk.Next
   268  	for tk != nil && tk.Position.Line <= extLine {
   269  		clonedTk := tk.Clone()
   270  		tokens.Add(clonedTk)
   271  		tk = clonedTk.Next
   272  	}
   273  	lastTk := tokens[len(tokens)-1]
   274  	trimmedOrigin := p.removeRightSideWhiteSpaceChar(lastTk.Origin)
   275  	suffix := lastTk.Origin[len(trimmedOrigin):]
   276  	lastTk.Origin = trimmedOrigin
   277  
   278  	if lastTk.Next != nil && len(suffix) > 1 {
   279  		next := lastTk.Next.Clone()
   280  		// add suffix to header of next token
   281  		if suffix[0] == '\n' || suffix[0] == '\r' {
   282  			suffix = suffix[1:]
   283  		}
   284  		next.Origin = suffix + next.Origin
   285  		lastTk.Next = next
   286  	}
   287  	return tokens
   288  }
   289  
   290  func (p *Printer) printAfterTokens(tk *token.Token, maxLine int) token.Tokens {
   291  	tokens := token.Tokens{}
   292  	if tk == nil {
   293  		return tokens
   294  	}
   295  	if tk.Position.Line > maxLine {
   296  		return tokens
   297  	}
   298  	minTk := tk.Clone()
   299  	minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin)
   300  	tokens.Add(minTk)
   301  	tk = minTk.Next
   302  	for tk != nil && tk.Position.Line <= maxLine {
   303  		clonedTk := tk.Clone()
   304  		tokens.Add(clonedTk)
   305  		tk = clonedTk.Next
   306  	}
   307  	return tokens
   308  }
   309  
   310  func (p *Printer) setupErrorTokenFormat(annotateLine int, isColored bool) {
   311  	prefix := func(annotateLine, num int) string {
   312  		if annotateLine == num {
   313  			return fmt.Sprintf("> %2d | ", num)
   314  		}
   315  		return fmt.Sprintf("  %2d | ", num)
   316  	}
   317  	p.LineNumber = true
   318  	p.LineNumberFormat = func(num int) string {
   319  		if isColored {
   320  			fn := color.New(color.Bold, color.FgHiWhite).SprintFunc()
   321  			return fn(prefix(annotateLine, num))
   322  		}
   323  		return prefix(annotateLine, num)
   324  	}
   325  	if isColored {
   326  		p.setDefaultColorSet()
   327  	}
   328  }
   329  
   330  func (p *Printer) PrintErrorToken(tk *token.Token, isColored bool) string {
   331  	errToken := tk
   332  	curLine := tk.Position.Line
   333  	curExtLine := curLine + p.newLineCount(p.removeLeftSideNewLineChar(tk.Origin))
   334  	if p.isNewLineLastChar(tk.Origin) {
   335  		// if last character ( exclude white space ) is new line character, ignore it.
   336  		curExtLine--
   337  	}
   338  
   339  	minLine := int(math.Max(float64(curLine-3), 1))
   340  	maxLine := curExtLine + 3
   341  	p.setupErrorTokenFormat(curLine, isColored)
   342  
   343  	beforeTokens := p.printBeforeTokens(tk, minLine, curExtLine)
   344  	lastTk := beforeTokens[len(beforeTokens)-1]
   345  	afterTokens := p.printAfterTokens(lastTk.Next, maxLine)
   346  
   347  	beforeSource := p.PrintTokens(beforeTokens)
   348  	prefixSpaceNum := len(fmt.Sprintf("  %2d | ", curLine))
   349  	annotateLine := strings.Repeat(" ", prefixSpaceNum+errToken.Position.Column-1) + "^"
   350  	afterSource := p.PrintTokens(afterTokens)
   351  	return fmt.Sprintf("%s\n%s\n%s", beforeSource, annotateLine, afterSource)
   352  }