blob: c6f5cac27e1bb50d267cdda482601f2f3ce069a9 [file] [log] [blame] [edit]
// Package printer provides functions for printing formatted textproto messages.
package printer
import (
"bytes"
"fmt"
"strings"
"github.com/protocolbuffers/txtpbfmt/ast"
"github.com/protocolbuffers/txtpbfmt/config"
"github.com/protocolbuffers/txtpbfmt/impl"
)
const indentSpaces = " "
// Format formats a text proto file preserving comments.
func Format(in []byte) ([]byte, error) {
return FormatWithConfig(in, config.Config{})
}
// FormatWithConfig functions similar to format, but allows the user to pass in
// additional configuration options.
func FormatWithConfig(in []byte, c config.Config) ([]byte, error) {
if err := impl.AddMetaCommentsToConfig(in, &c); err != nil {
return nil, err
}
if c.Disable {
c.Infof("Ignored file with 'disable' comment.")
return in, nil
}
nodes, err := impl.ParseWithMetaCommentConfig(in, c)
if err != nil {
return nil, err
}
return FormatNodes(nodes), nil
}
func removeDeleted(nodes []*ast.Node) []*ast.Node {
var res []*ast.Node
res = []*ast.Node{} // empty children is different from nil children
// When removing a node which has an empty line before it, we should keep
// the empty line before the next non-removed node to maintain the visual separation.
// Consider the following:
// foo: { name: "foo1" }
// foo: { name: "foo2" }
//
// bar: { name: "bar1" }
// bar: { name: "bar2" }
//
// If we decide to remove both foo2 and bar1, the result should still have one empty
// line between foo1 and bar2.
addEmptyLine := false
for _, node := range nodes {
if node.Deleted {
if len(node.PreComments) > 0 && node.PreComments[0] == "" {
addEmptyLine = true
}
continue
}
if len(node.Children) > 0 {
node.Children = removeDeleted(node.Children)
}
if addEmptyLine && (len(node.PreComments) == 0 || node.PreComments[0] != "") {
node.PreComments = append([]string{""}, node.PreComments...)
}
addEmptyLine = false
res = append(res, node)
}
return res
}
// Debug returns a textual representation of the specified nodes for
// consumption by humans when debugging (e.g. in test failures). No guarantees
// are made about the specific output.
func Debug(nodes []*ast.Node, depth int) string {
res := []string{""}
prefix := strings.Repeat(".", depth)
for _, nd := range nodes {
var value string
if nd.Deleted {
res = append(res, "DELETED")
}
if nd.Children != nil { // Also for 0 children.
value = fmt.Sprintf("children:%s", Debug(nd.Children, depth+1))
} else {
value = fmt.Sprintf("values: %v\n", nd.Values)
}
res = append(res,
fmt.Sprintf("name: %q", nd.Name),
fmt.Sprintf("PreComments: %q (len %d)", strings.Join(nd.PreComments, "\n"), len(nd.PreComments)),
value)
}
return strings.Join(res, fmt.Sprintf("\n%s ", prefix))
}
// FormatNodes returns formatted nodes at the given indentation depth (0 = top-level) as bytes.
func FormatNodes(nodes []*ast.Node) []byte {
return FormatNodesWithDepth(nodes, 0 /* depth */)
}
// FormatNodesWithDepth returns formatted nodes at the given indentation depth (0 = top-level) as bytes.
func FormatNodesWithDepth(nodes []*ast.Node, depth int) []byte {
var result bytes.Buffer
formatter{&result}.writeNodes(removeDeleted(nodes), depth, false /* isSameLine */, false /* asListItems */)
return result.Bytes()
}
// stringWriter abstracts over bytes.Buffer and strings.Builder
type stringWriter interface {
WriteString(s string) (int, error)
}
// formatter accumulates pretty-printed textproto contents into a stringWriter.
type formatter struct {
stringWriter
}
func (f formatter) writeNode(nd *ast.Node, depth int, isSameLine, asListItems bool, index, lastNonCommentIndex int) {
if len(nd.Raw) > 0 {
f.WriteString(nd.Raw)
return
}
indent := " "
if !isSameLine {
indent = strings.Repeat(indentSpaces, depth)
}
f.writePreComments(nd, indent, depth, index)
if nd.IsCommentOnly() {
// The comments have been printed already, no more work to do.
return
}
f.WriteString(indent)
// Node name may be empty in alternative-style textproto files, because they
// contain a sequence of proto messages of the same type:
// { name: "first_msg" }
// { name: "second_msg" }
// In all other cases, nd.Name is not empty and should be printed.
if nd.Name != "" {
f.writeNodeName(nd, indent)
}
f.writeNodeValues(nd, indent)
f.writeNodeChildren(nd, depth, isSameLine)
if asListItems && index < lastNonCommentIndex {
f.WriteString(",")
}
f.writeNodeClosingBraceComment(nd)
}
func (f formatter) writePreComments(nd *ast.Node, indent string, depth int, index int) {
for _, comment := range nd.PreComments {
if len(comment) == 0 {
if !(depth == 0 && index == 0) {
f.WriteString("\n")
}
continue
}
f.WriteString(indent)
f.WriteString(comment)
f.WriteString("\n")
}
}
func (f formatter) writeNodes(nodes []*ast.Node, depth int, isSameLine, asListItems bool) {
lastNonCommentIndex := 0
if asListItems {
for i := len(nodes) - 1; i >= 0; i-- {
if !nodes[i].IsCommentOnly() {
lastNonCommentIndex = i
break
}
}
}
for index, nd := range nodes {
f.writeNode(nd, depth, isSameLine, asListItems, index, lastNonCommentIndex)
if !isSameLine && len(nd.Raw) == 0 && !nd.IsCommentOnly() {
f.WriteString("\n")
}
}
}
func (f formatter) writeNodeName(nd *ast.Node, indent string) {
f.WriteString(nd.Name)
if !nd.SkipColon {
f.WriteString(":")
}
// The space after the name is required for one-liners and message fields:
// title: "there was a space here"
// metadata: { ... }
// In other cases, there is a newline right after the colon, so no space required.
if nd.Children != nil || (len(nd.Values) == 1 && len(nd.Values[0].PreComments) == 0) || nd.ValuesAsList {
if nd.PutSingleValueOnNextLine {
f.WriteString("\n" + indent + indentSpaces)
} else {
f.WriteString(" ")
}
}
}
func (f formatter) writeNodeValues(nd *ast.Node, indent string) {
if nd.ValuesAsList { // For ValuesAsList option we will preserve even empty list `field: []`
f.writeValuesAsList(nd, nd.Values, indent+indentSpaces)
} else if len(nd.Values) > 0 {
f.writeValues(nd, nd.Values, indent+indentSpaces)
}
}
func (f formatter) writeNodeChildren(nd *ast.Node, depth int, isSameLine bool) {
if nd.Children != nil { // Also for 0 Children.
if nd.ChildrenAsList {
f.writeChildrenAsListItems(nd.Children, depth+1, isSameLine || nd.ChildrenSameLine)
} else {
f.writeChildren(nd.Children, depth+1, isSameLine || nd.ChildrenSameLine, nd.IsAngleBracket)
}
}
}
func (f formatter) writeNodeClosingBraceComment(nd *ast.Node) {
if (nd.Children != nil || nd.ValuesAsList) && len(nd.ClosingBraceComment) > 0 {
f.WriteString(indentSpaces)
f.WriteString(nd.ClosingBraceComment)
}
}
func (f formatter) writeValues(nd *ast.Node, vals []*ast.Value, indent string) {
if len(vals) == 0 {
// This should never happen: formatValues can be called only if there are some values.
return
}
sep := "\n" + indent
if len(vals) == 1 && len(vals[0].PreComments) == 0 {
sep = ""
}
for _, v := range vals {
f.WriteString(sep)
for _, comment := range v.PreComments {
f.WriteString(comment)
f.WriteString(sep)
}
f.WriteString(v.Value)
if len(v.InlineComment) > 0 {
f.WriteString(indentSpaces)
f.WriteString(v.InlineComment)
}
}
for _, comment := range nd.PostValuesComments {
f.WriteString(sep)
f.WriteString(comment)
}
}
func (f formatter) canWriteValuesAsListOnSameLine(nd *ast.Node, vals []*ast.Value) bool {
if !nd.ChildrenSameLine || len(nd.PostValuesComments) > 0 {
return false
}
// Parser found all children on a same line, but we need to check again.
// It's possible that AST was modified after parsing.
for _, val := range vals {
if len(val.PreComments) > 0 || len(val.InlineComment) > 0 {
return false
}
}
return true
}
func (f formatter) writeValuesAsList(nd *ast.Node, vals []*ast.Value, indent string) {
// Checks if it's possible to put whole list in a single line.
sameLine := f.canWriteValuesAsListOnSameLine(nd, vals)
sep := ""
if !sameLine {
sep = "\n" + indent
}
f.WriteString("[")
for idx, v := range vals {
for _, comment := range v.PreComments {
f.WriteString(sep)
f.WriteString(comment)
}
f.WriteString(sep)
f.WriteString(v.Value)
if idx < len(vals)-1 { // Don't put trailing comma that fails Python parser.
f.WriteString(",")
if sameLine {
f.WriteString(" ")
}
}
if len(v.InlineComment) > 0 {
f.WriteString(indentSpaces)
f.WriteString(v.InlineComment)
}
}
for _, comment := range nd.PostValuesComments {
f.WriteString(sep)
f.WriteString(comment)
}
f.WriteString(strings.Replace(sep, indentSpaces, "", 1))
f.WriteString("]")
}
// writeChildren writes the child nodes. The result always ends with a closing brace.
func (f formatter) writeChildren(children []*ast.Node, depth int, sameLine, isAngleBracket bool) {
openBrace := "{"
closeBrace := "}"
if isAngleBracket {
openBrace = "<"
closeBrace = ">"
}
switch {
case sameLine && len(children) == 0:
f.WriteString(openBrace + closeBrace)
case sameLine:
f.WriteString(openBrace)
f.writeNodes(children, depth, sameLine, false /* asListItems */)
f.WriteString(" " + closeBrace)
default:
f.WriteString(openBrace + "\n")
f.writeNodes(children, depth, sameLine, false /* asListItems */)
f.WriteString(strings.Repeat(indentSpaces, depth-1))
f.WriteString(closeBrace)
}
}
// writeChildrenAsListItems writes the child nodes as list items.
func (f formatter) writeChildrenAsListItems(children []*ast.Node, depth int, sameLine bool) {
openBrace := "["
closeBrace := "]"
switch {
case sameLine && len(children) == 0:
f.WriteString(openBrace + closeBrace)
case sameLine:
f.WriteString(openBrace)
f.writeNodes(children, depth, sameLine, true /* asListItems */)
f.WriteString(" " + closeBrace)
default:
f.WriteString(openBrace + "\n")
f.writeNodes(children, depth, sameLine, true /* asListItems */)
f.WriteString(strings.Repeat(indentSpaces, depth-1))
f.WriteString(closeBrace)
}
}