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.
353 lines
7.9 KiB
353 lines
7.9 KiB
3 years ago
|
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)
|
||
|
}
|