You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

352 lines
7.9 KiB

package printer
import (
"fmt"
"math"
"strings"
"github.com/fatih/color"
"github.com/goccy/go-yaml/ast"
"github.com/goccy/go-yaml/token"
)
// Property additional property set for each the token
type Property struct {
Prefix string
Suffix string
}
// PrintFunc returns property instance
type PrintFunc func() *Property
// Printer create text from token collection or ast
type Printer struct {
LineNumber bool
LineNumberFormat func(num int) string
MapKey PrintFunc
Anchor PrintFunc
Alias PrintFunc
Bool PrintFunc
String PrintFunc
Number PrintFunc
}
func defaultLineNumberFormat(num int) string {
return fmt.Sprintf("%2d | ", num)
}
func (p *Printer) property(tk *token.Token) *Property {
prop := &Property{}
switch tk.PreviousType() {
case token.AnchorType:
if p.Anchor != nil {
return p.Anchor()
}
return prop
case token.AliasType:
if p.Alias != nil {
return p.Alias()
}
return prop
}
switch tk.NextType() {
case token.MappingValueType:
if p.MapKey != nil {
return p.MapKey()
}
return prop
}
switch tk.Type {
case token.BoolType:
if p.Bool != nil {
return p.Bool()
}
return prop
case token.AnchorType:
if p.Anchor != nil {
return p.Anchor()
}
return prop
case token.AliasType:
if p.Anchor != nil {
return p.Alias()
}
return prop
case token.StringType, token.SingleQuoteType, token.DoubleQuoteType:
if p.String != nil {
return p.String()
}
return prop
case token.IntegerType, token.FloatType:
if p.Number != nil {
return p.Number()
}
return prop
default:
}
return prop
}
// PrintTokens create text from token collection
func (p *Printer) PrintTokens(tokens token.Tokens) string {
if len(tokens) == 0 {
return ""
}
if p.LineNumber {
if p.LineNumberFormat == nil {
p.LineNumberFormat = defaultLineNumberFormat
}
}
texts := []string{}
lineNumber := tokens[0].Position.Line
for _, tk := range tokens {
lines := strings.Split(tk.Origin, "\n")
prop := p.property(tk)
header := ""
if p.LineNumber {
header = p.LineNumberFormat(lineNumber)
}
if len(lines) == 1 {
line := prop.Prefix + lines[0] + prop.Suffix
if len(texts) == 0 {
texts = append(texts, header+line)
lineNumber++
} else {
text := texts[len(texts)-1]
texts[len(texts)-1] = text + line
}
} else {
for idx, src := range lines {
if p.LineNumber {
header = p.LineNumberFormat(lineNumber)
}
line := prop.Prefix + src + prop.Suffix
if idx == 0 {
if len(texts) == 0 {
texts = append(texts, header+line)
lineNumber++
} else {
text := texts[len(texts)-1]
texts[len(texts)-1] = text + line
}
} else {
texts = append(texts, fmt.Sprintf("%s%s", header, line))
lineNumber++
}
}
}
}
return strings.Join(texts, "\n")
}
// PrintNode create text from ast.Node
func (p *Printer) PrintNode(node ast.Node) []byte {
return []byte(fmt.Sprintf("%+v\n", node))
}
const escape = "\x1b"
func format(attr color.Attribute) string {
return fmt.Sprintf("%s[%dm", escape, attr)
}
func (p *Printer) setDefaultColorSet() {
p.Bool = func() *Property {
return &Property{
Prefix: format(color.FgHiMagenta),
Suffix: format(color.Reset),
}
}
p.Number = func() *Property {
return &Property{
Prefix: format(color.FgHiMagenta),
Suffix: format(color.Reset),
}
}
p.MapKey = func() *Property {
return &Property{
Prefix: format(color.FgHiCyan),
Suffix: format(color.Reset),
}
}
p.Anchor = func() *Property {
return &Property{
Prefix: format(color.FgHiYellow),
Suffix: format(color.Reset),
}
}
p.Alias = func() *Property {
return &Property{
Prefix: format(color.FgHiYellow),
Suffix: format(color.Reset),
}
}
p.String = func() *Property {
return &Property{
Prefix: format(color.FgHiGreen),
Suffix: format(color.Reset),
}
}
}
func (p *Printer) PrintErrorMessage(msg string, isColored bool) string {
if isColored {
return fmt.Sprintf("%s%s%s",
format(color.FgHiRed),
msg,
format(color.Reset),
)
}
return msg
}
func (p *Printer) removeLeftSideNewLineChar(src string) string {
return strings.TrimLeft(strings.TrimLeft(strings.TrimLeft(src, "\r"), "\n"), "\r\n")
}
func (p *Printer) removeRightSideNewLineChar(src string) string {
return strings.TrimRight(strings.TrimRight(strings.TrimRight(src, "\r"), "\n"), "\r\n")
}
func (p *Printer) removeRightSideWhiteSpaceChar(src string) string {
return p.removeRightSideNewLineChar(strings.TrimRight(src, " "))
}
func (p *Printer) newLineCount(s string) int {
src := []rune(s)
size := len(src)
cnt := 0
for i := 0; i < size; i++ {
c := src[i]
switch c {
case '\r':
if i+1 < size && src[i+1] == '\n' {
i++
}
cnt++
case '\n':
cnt++
}
}
return cnt
}
func (p *Printer) isNewLineLastChar(s string) bool {
for i := len(s) - 1; i > 0; i-- {
c := s[i]
switch c {
case ' ':
continue
case '\n', '\r':
return true
}
break
}
return false
}
func (p *Printer) printBeforeTokens(tk *token.Token, minLine, extLine int) token.Tokens {
for {
if tk.Prev == nil {
break
}
if tk.Prev.Position.Line < minLine {
break
}
tk = tk.Prev
}
minTk := tk.Clone()
if minTk.Prev != nil {
// add white spaces to minTk by prev token
prev := minTk.Prev
whiteSpaceLen := len(prev.Origin) - len(strings.TrimRight(prev.Origin, " "))
minTk.Origin = strings.Repeat(" ", whiteSpaceLen) + minTk.Origin
}
minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin)
tokens := token.Tokens{minTk}
tk = minTk.Next
for tk != nil && tk.Position.Line <= extLine {
clonedTk := tk.Clone()
tokens.Add(clonedTk)
tk = clonedTk.Next
}
lastTk := tokens[len(tokens)-1]
trimmedOrigin := p.removeRightSideWhiteSpaceChar(lastTk.Origin)
suffix := lastTk.Origin[len(trimmedOrigin):]
lastTk.Origin = trimmedOrigin
if lastTk.Next != nil && len(suffix) > 1 {
next := lastTk.Next.Clone()
// add suffix to header of next token
if suffix[0] == '\n' || suffix[0] == '\r' {
suffix = suffix[1:]
}
next.Origin = suffix + next.Origin
lastTk.Next = next
}
return tokens
}
func (p *Printer) printAfterTokens(tk *token.Token, maxLine int) token.Tokens {
tokens := token.Tokens{}
if tk == nil {
return tokens
}
if tk.Position.Line > maxLine {
return tokens
}
minTk := tk.Clone()
minTk.Origin = p.removeLeftSideNewLineChar(minTk.Origin)
tokens.Add(minTk)
tk = minTk.Next
for tk != nil && tk.Position.Line <= maxLine {
clonedTk := tk.Clone()
tokens.Add(clonedTk)
tk = clonedTk.Next
}
return tokens
}
func (p *Printer) setupErrorTokenFormat(annotateLine int, isColored bool) {
prefix := func(annotateLine, num int) string {
if annotateLine == num {
return fmt.Sprintf("> %2d | ", num)
}
return fmt.Sprintf(" %2d | ", num)
}
p.LineNumber = true
p.LineNumberFormat = func(num int) string {
if isColored {
fn := color.New(color.Bold, color.FgHiWhite).SprintFunc()
return fn(prefix(annotateLine, num))
}
return prefix(annotateLine, num)
}
if isColored {
p.setDefaultColorSet()
}
}
func (p *Printer) PrintErrorToken(tk *token.Token, isColored bool) string {
errToken := tk
curLine := tk.Position.Line
curExtLine := curLine + p.newLineCount(p.removeLeftSideNewLineChar(tk.Origin))
if p.isNewLineLastChar(tk.Origin) {
// if last character ( exclude white space ) is new line character, ignore it.
curExtLine--
}
minLine := int(math.Max(float64(curLine-3), 1))
maxLine := curExtLine + 3
p.setupErrorTokenFormat(curLine, isColored)
beforeTokens := p.printBeforeTokens(tk, minLine, curExtLine)
lastTk := beforeTokens[len(beforeTokens)-1]
afterTokens := p.printAfterTokens(lastTk.Next, maxLine)
beforeSource := p.PrintTokens(beforeTokens)
prefixSpaceNum := len(fmt.Sprintf(" %2d | ", curLine))
annotateLine := strings.Repeat(" ", prefixSpaceNum+errToken.Position.Column-1) + "^"
afterSource := p.PrintTokens(afterTokens)
return fmt.Sprintf("%s\n%s\n%s", beforeSource, annotateLine, afterSource)
}