vendor: move vendored sources in-tree
This should make it easier to see changes instead of just a blob
This commit is contained in:
602
vendor/github.com/tailscale/hujson/format.go
generated
vendored
Normal file
602
vendor/github.com/tailscale/hujson/format.go
generated
vendored
Normal file
@@ -0,0 +1,602 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package hujson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Standardize strips any features specific to HuJSON from b,
|
||||
// making it compliant with standard JSON per RFC 8259.
|
||||
// All comments and trailing commas are replaced with a space character
|
||||
// in order to preserve the original line numbers and byte offsets.
|
||||
// If an error is encountered, then b is returned as is along with the error.
|
||||
func Standardize(b []byte) ([]byte, error) {
|
||||
ast, err := Parse(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
ast.Standardize()
|
||||
return ast.Pack(), nil
|
||||
}
|
||||
|
||||
// Minimize removes all whitespace, comments, and trailing commas from b,
|
||||
// making it compliant with standard JSON per RFC 8259.
|
||||
// If an error is encountered, then b is returned as is along with the error.
|
||||
func Minimize(b []byte) ([]byte, error) {
|
||||
ast, err := Parse(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
ast.Minimize()
|
||||
return ast.Pack(), nil
|
||||
}
|
||||
|
||||
// Format formats b according to some opinionated heuristics for
|
||||
// how HuJSON should look. The exact output may change over time.
|
||||
// It is the equivalent of `go fmt` but for HuJSON.
|
||||
//
|
||||
// If the input is standard JSON, then the output will remain standard.
|
||||
// Format is idempotent such that formatting already formatted HuJSON
|
||||
// results in no changes.
|
||||
// If an error is encountered, then b is returned as is along with the error.
|
||||
func Format(b []byte) ([]byte, error) {
|
||||
ast, err := Parse(b)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
ast.Format()
|
||||
return ast.Pack(), nil
|
||||
}
|
||||
|
||||
const punchCardWidth = 80
|
||||
|
||||
var (
|
||||
newline = []byte("\n")
|
||||
twoNewlines = []byte("\n\n")
|
||||
endlineWindows = []byte("\r\n")
|
||||
endlineMacOSX = []byte("\n\r")
|
||||
carriageReturn = []byte("\r")
|
||||
space = []byte(" ")
|
||||
)
|
||||
|
||||
// Format formats the value according to some opinionated heuristics for
|
||||
// how HuJSON should look. The exact output may change over time.
|
||||
// It is the equivalent of `go fmt` but for HuJSON.
|
||||
//
|
||||
// If the input is standard JSON, then the output will remain standard.
|
||||
// Format is idempotent such that formatting already formatted HuJSON
|
||||
// results in no changes.
|
||||
func (v *Value) Format() {
|
||||
// Format leading extra.
|
||||
v.BeforeExtra.format(0, formatOptions{})
|
||||
v.BeforeExtra = v.BeforeExtra[consumeWhitespace(v.BeforeExtra):] // never has leading whitespace
|
||||
// Format the value.
|
||||
needExpand := make(map[composite]bool)
|
||||
isStandard := v.IsStandard()
|
||||
v.normalize()
|
||||
v.expandComposites(needExpand)
|
||||
v.formatWhitespace(0, needExpand, isStandard)
|
||||
v.alignObjectValues()
|
||||
// Format trailing extra.
|
||||
v.AfterExtra.format(0, formatOptions{})
|
||||
v.AfterExtra = append(bytes.TrimRightFunc(v.AfterExtra, unicode.IsSpace), '\n') // always has exactly one trailing newline
|
||||
|
||||
v.UpdateOffsets()
|
||||
}
|
||||
|
||||
// normalize performs simple normalization changes. In particular, it:
|
||||
// - normalizes strings,
|
||||
// - normalizes empty objects and arrays as simply {} or [],
|
||||
// - normalizes whitespace between names and colons,
|
||||
// - normalizes whitespace between values and commas.
|
||||
//
|
||||
// It always returns true to be compatible with composite.rangeValues.
|
||||
func (v *Value) normalize() bool {
|
||||
switch v2 := v.Value.(type) {
|
||||
case Literal:
|
||||
// Normalize string if there are escape characters.
|
||||
if v2.Kind() == '"' && bytes.IndexByte(v2, '\\') >= 0 {
|
||||
v.Value = String(v2.String())
|
||||
}
|
||||
case composite:
|
||||
// Cleanup for empty objects and arrays.
|
||||
if v2.length() == 0 {
|
||||
// If there is only whitespace, then remove the whitespace.
|
||||
if !v2.afterExtra().hasComment() {
|
||||
*v2.afterExtra() = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// If there is only whitespace between the name and colon,
|
||||
// or between the value and comma, then remove the whitespace.
|
||||
for v3 := range v2.allValues() {
|
||||
if !v3.AfterExtra.hasComment() {
|
||||
v3.AfterExtra = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize all sub-values.
|
||||
for v3 := range v2.allValues() {
|
||||
v3.normalize()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// lineStats carries statistics about a sequence of lines.
|
||||
type lineStats struct {
|
||||
firstLength int
|
||||
lastLength int
|
||||
multiline bool // false implies firstLength == lastLength
|
||||
}
|
||||
|
||||
// expandComposites populates needExpand with the set of composite values
|
||||
// that need to be expanded (i.e., print each member/element on a new line).
|
||||
// This method is pure and does not mutate the AST.
|
||||
func (v *Value) expandComposites(needExpand map[composite]bool) (stats lineStats) {
|
||||
switch v2 := v.Value.(type) {
|
||||
case Literal:
|
||||
stats = lineStats{len(v2), len(v2), false}
|
||||
case composite:
|
||||
// Every object or array is either fully inlined or fully expanded.
|
||||
// This simplifies machine-modification of HuJSON so that the mutation
|
||||
// can easily determine which mode it is currently in.
|
||||
//
|
||||
// If any whitespace after a '{', '[', or ',' or before a '}' or ']'
|
||||
// contains a newline, then we always expand the object or array.
|
||||
var expand bool
|
||||
|
||||
// Keep track of line lengths.
|
||||
var lineLength int
|
||||
var lineLengths []int
|
||||
updateStats := func(s lineStats) {
|
||||
lineLength += s.firstLength
|
||||
if s.multiline {
|
||||
lineLengths = append(lineLengths, lineLength)
|
||||
lineLength = s.lastLength
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all members/elements in an object/array.
|
||||
switch v2 := v2.(type) {
|
||||
case *Object:
|
||||
lineLength += len("{")
|
||||
for i := range v2.Members {
|
||||
name := &v2.Members[i].Name
|
||||
value := &v2.Members[i].Value
|
||||
expand = expand || name.BeforeExtra.hasNewline()
|
||||
updateStats(name.BeforeExtra.lineStats())
|
||||
updateStats(name.expandComposites(needExpand))
|
||||
updateStats(name.AfterExtra.lineStats())
|
||||
lineLength += len(": ")
|
||||
updateStats(value.BeforeExtra.lineStats())
|
||||
updateStats(value.expandComposites(needExpand))
|
||||
updateStats(value.AfterExtra.lineStats())
|
||||
lineLength += len(", ")
|
||||
}
|
||||
lineLength += len("}")
|
||||
|
||||
// Always expand multiline objects with more than 1 member.
|
||||
expand = expand || v2.length() > 1 && stats.multiline
|
||||
case *Array:
|
||||
lineLength += len("[")
|
||||
for i := range v2.Elements {
|
||||
value := &v2.Elements[i]
|
||||
expand = expand || value.BeforeExtra.hasNewline()
|
||||
updateStats(value.BeforeExtra.lineStats())
|
||||
updateStats(value.expandComposites(needExpand))
|
||||
updateStats(value.AfterExtra.lineStats())
|
||||
lineLength += len(", ")
|
||||
}
|
||||
lineLength += len("]")
|
||||
}
|
||||
if last := v2.lastValue(); last != nil {
|
||||
expand = expand || last.AfterExtra.hasNewline()
|
||||
}
|
||||
expand = expand || v2.afterExtra().hasNewline()
|
||||
|
||||
// Update the block statistics.
|
||||
lineLengths = append(lineLengths, lineLength)
|
||||
stats = lineStats{
|
||||
firstLength: lineLengths[0],
|
||||
lastLength: lineLengths[len(lineLengths)-1],
|
||||
multiline: len(lineLengths) > 1,
|
||||
}
|
||||
for i := 0; !expand && i < len(lineLengths); i++ {
|
||||
expand = lineLengths[i] > punchCardWidth
|
||||
}
|
||||
|
||||
if expand {
|
||||
stats = lineStats{len("{"), len("}"), true}
|
||||
stats.firstLength += v2.beforeExtraAt(0).lineStats().firstLength
|
||||
needExpand[v2] = expand
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
func (b Extra) lineStats() (stats lineStats) {
|
||||
// length is the approximate length of the comments.
|
||||
length := func(b []byte) (n int) {
|
||||
for {
|
||||
b = b[consumeWhitespace(b):]
|
||||
switch {
|
||||
case bytes.HasPrefix(b, lineCommentStart):
|
||||
return n + len(" ") + len(b) // line comment must go to the end
|
||||
case bytes.HasPrefix(b, blockCommentStart):
|
||||
nc := consumeComment(b)
|
||||
if nc <= 0 {
|
||||
return n + len(" ") + len(b) // truncated block comment must go to the end
|
||||
}
|
||||
n += len(" ") + nc
|
||||
b = b[nc:]
|
||||
continue
|
||||
default:
|
||||
if n > 0 {
|
||||
n += len(" ") // account for padding space after block comment
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bytes.Contains(b, newline) {
|
||||
n := length(b)
|
||||
return lineStats{n, n, false}
|
||||
} else {
|
||||
first := b[:bytes.IndexByte(b, '\n')]
|
||||
last := b[bytes.LastIndexByte(b, '\n')+len("\n"):]
|
||||
return lineStats{length(first), length(last), true}
|
||||
}
|
||||
}
|
||||
|
||||
// formatWhitespace mutates the AST and formats whitespace to ensure
|
||||
// consistent indentation and expansion of objects and arrays.
|
||||
func (v *Value) formatWhitespace(depth int, needExpand map[composite]bool, standardize bool) {
|
||||
if comp, ok := v.Value.(composite); ok {
|
||||
expand := needExpand[comp]
|
||||
|
||||
// Format all members/elements in an object/array.
|
||||
switch comp := comp.(type) {
|
||||
case *Object:
|
||||
for i := range comp.Members {
|
||||
name := &comp.Members[i].Name
|
||||
value := &comp.Members[i].Value
|
||||
|
||||
// Format extra before name.
|
||||
name.BeforeExtra.format(depth+1, formatOptions{
|
||||
ensureLeadingNewline: expand,
|
||||
removeLeadingEmptyLines: i == 0,
|
||||
appendSpaceIfEmpty: i != 0,
|
||||
})
|
||||
// Format the name.
|
||||
name.formatWhitespace(depth+1, needExpand, standardize)
|
||||
// Format extra after name and before colon.
|
||||
name.AfterExtra.format(depth+2, formatOptions{
|
||||
removeLeadingEmptyLines: true,
|
||||
removeTrailingEmptyLines: true,
|
||||
})
|
||||
// Format extra after colon and before value.
|
||||
value.BeforeExtra.format(depth+2, formatOptions{
|
||||
removeLeadingEmptyLines: true,
|
||||
removeTrailingEmptyLines: true,
|
||||
appendSpaceIfEmpty: true,
|
||||
})
|
||||
// Format the value.
|
||||
depthOffset := 0
|
||||
if expand {
|
||||
depthOffset++
|
||||
}
|
||||
if name.AfterExtra.hasNewline() || value.BeforeExtra.hasNewline() {
|
||||
depthOffset++
|
||||
}
|
||||
value.formatWhitespace(depth+depthOffset, needExpand, standardize)
|
||||
// Format extra after value and before comma.
|
||||
value.AfterExtra.format(depth+2, formatOptions{
|
||||
removeLeadingEmptyLines: true,
|
||||
removeTrailingEmptyLines: true,
|
||||
})
|
||||
}
|
||||
case *Array:
|
||||
for i := range comp.Elements {
|
||||
value := &comp.Elements[i]
|
||||
|
||||
// Format extra before value.
|
||||
value.BeforeExtra.format(depth+1, formatOptions{
|
||||
ensureLeadingNewline: expand,
|
||||
removeLeadingEmptyLines: i == 0,
|
||||
appendSpaceIfEmpty: i != 0,
|
||||
})
|
||||
// Format the value.
|
||||
depthOffset := 0
|
||||
if expand {
|
||||
depthOffset++
|
||||
}
|
||||
value.formatWhitespace(depth+depthOffset, needExpand, standardize)
|
||||
// Format extra after value and before comma.
|
||||
value.AfterExtra.format(depth+2, formatOptions{
|
||||
removeLeadingEmptyLines: true,
|
||||
removeTrailingEmptyLines: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Format the extra before the closing '}' or ']'.
|
||||
comp.afterExtra().format(depth+1, formatOptions{
|
||||
ensureTrailingNewline: expand,
|
||||
removeLeadingEmptyLines: comp.length() == 0,
|
||||
removeTrailingEmptyLines: true,
|
||||
unindentLastLine: true,
|
||||
})
|
||||
|
||||
// Normalize presence of trailing comma.
|
||||
surroundedComma := comp.lastValue() != nil && len(comp.lastValue().AfterExtra) > 0 && len(*comp.afterExtra()) > 0
|
||||
switch {
|
||||
// Avoid a trailing comma for a non-expanded object or array.
|
||||
case !expand && !surroundedComma:
|
||||
setTrailingComma(comp, false)
|
||||
// Otherwise, emit a trailing comma (unless this need to be standard).
|
||||
case expand && !standardize:
|
||||
setTrailingComma(comp, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type formatOptions struct {
|
||||
ensureLeadingNewline bool
|
||||
ensureTrailingNewline bool
|
||||
removeLeadingEmptyLines bool
|
||||
removeTrailingEmptyLines bool
|
||||
unindentLastLine bool
|
||||
appendSpaceIfEmpty bool
|
||||
}
|
||||
|
||||
func (b *Extra) format(depth int, opts formatOptions) {
|
||||
// Remove carriage returns to normalize output across operating systems.
|
||||
if bytes.IndexByte(*b, '\r') >= 0 {
|
||||
*b = bytes.ReplaceAll(*b, endlineWindows, newline)
|
||||
*b = bytes.ReplaceAll(*b, endlineMacOSX, newline)
|
||||
*b = bytes.ReplaceAll(*b, carriageReturn, space)
|
||||
}
|
||||
|
||||
in := *b
|
||||
var out []byte // TODO(dsnet): Cache this in sync.Pool?
|
||||
|
||||
// Inject a leading newline if not present in the input.
|
||||
if opts.ensureLeadingNewline && !in.hasNewline() {
|
||||
out = append(out, '\n')
|
||||
}
|
||||
|
||||
// Iterate over every paragraph in the comment.
|
||||
for len(in) > 0 {
|
||||
// Handle whitespace.
|
||||
if n := consumeWhitespace(in); n > 0 {
|
||||
nl := bytes.Count(in[:n], newline)
|
||||
if nl > 2 {
|
||||
nl = 2 // never allow more than one blank line
|
||||
}
|
||||
for i := 0; i < nl; i++ {
|
||||
out = append(out, '\n')
|
||||
}
|
||||
in = in[n:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle comments.
|
||||
n := consumeComment(in)
|
||||
if n <= 0 {
|
||||
return // invalid comment
|
||||
}
|
||||
|
||||
// Emit leading whitespace.
|
||||
if bytes.HasSuffix(out, newline) {
|
||||
out = appendIndent(out, depth)
|
||||
} else {
|
||||
out = append(out, ' ')
|
||||
}
|
||||
|
||||
// Copy single-line comment to the output verbatim.
|
||||
comment := in[:n]
|
||||
if bytes.HasPrefix(comment, lineCommentStart) || !comment.hasNewline() {
|
||||
comment = bytes.TrimRightFunc(comment, unicode.IsSpace) // trim trailing whitespace
|
||||
if bytes.HasPrefix(comment, lineCommentStart) {
|
||||
n-- // leave newline for next iteration of comment
|
||||
}
|
||||
out = append(out, comment...) // single-line comments preserved verbatim
|
||||
in = in[n:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Format multi-line block comments and copy to the output.
|
||||
lines := bytes.Split(comment, newline) // len(lines) >= 2 since at least one '\n' exists
|
||||
var firstLine []byte // first non-empty line after blockCommentStart
|
||||
var hasEmptyLine bool
|
||||
for i, line := range lines {
|
||||
line = bytes.TrimRightFunc(line, unicode.IsSpace) // trim trailing whitespace
|
||||
if len(firstLine) == 0 && len(line) > 0 && i > 0 {
|
||||
firstLine = line
|
||||
}
|
||||
hasEmptyLine = hasEmptyLine || len(line) == 0
|
||||
lines[i] = line
|
||||
}
|
||||
|
||||
// Compute the longest common prefix
|
||||
commonPrefix := firstLine
|
||||
for i, line := range lines[1:] {
|
||||
if len(line) == 0 {
|
||||
continue // ignore empty lines
|
||||
}
|
||||
|
||||
// If the last line is just "*/" with preceding whitespace, then
|
||||
// ignore any whitespace as part of the common prefix.
|
||||
// Instead, copy the whitespace from the common prefix.
|
||||
isLast := bytes.HasSuffix(line, blockCommentEnd)
|
||||
if isLast && consumeWhitespace(line)+len(blockCommentEnd) == len(line) {
|
||||
prefixLen := consumeWhitespace(commonPrefix)
|
||||
lines[i+1] = append(commonPrefix[:prefixLen:prefixLen], blockCommentEnd...)
|
||||
break
|
||||
}
|
||||
|
||||
// Check for longest common prefix.
|
||||
for i := 0; i < len(line) && i < len(commonPrefix); i++ {
|
||||
if line[i] != commonPrefix[i] {
|
||||
commonPrefix = commonPrefix[:i]
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indent every line and copy to output.
|
||||
prefixLen := consumeWhitespace(commonPrefix)
|
||||
starAligned := !hasEmptyLine && len(commonPrefix) > prefixLen && commonPrefix[prefixLen] == '*'
|
||||
out = append(out, lines[0]...)
|
||||
out = append(out, '\n')
|
||||
for _, line := range lines[1:] {
|
||||
if len(line) > 0 {
|
||||
out = appendIndent(out, depth)
|
||||
if starAligned {
|
||||
out = append(out, ' ')
|
||||
}
|
||||
out = append(out, line[prefixLen:]...)
|
||||
}
|
||||
out = append(out, '\n')
|
||||
}
|
||||
out = bytes.TrimRight(out, "\n")
|
||||
in = in[n:]
|
||||
}
|
||||
|
||||
// Inject a trailing newline if not present in the input.
|
||||
if opts.ensureTrailingNewline && !bytes.HasSuffix(out, newline) {
|
||||
out = append(out, '\n')
|
||||
}
|
||||
// Remove all leading empty lines.
|
||||
for opts.removeLeadingEmptyLines && bytes.HasPrefix(out, twoNewlines) {
|
||||
out = out[1:]
|
||||
}
|
||||
// Remove all trailing empty lines.
|
||||
for opts.removeTrailingEmptyLines && bytes.HasSuffix(out, twoNewlines) {
|
||||
out = out[:len(out)-1]
|
||||
}
|
||||
// If the whitespace ends on a newline, append the necessary indentation.
|
||||
// Otherwise, emit a space if we did not end on a new line.
|
||||
if bytes.HasSuffix(out, newline) {
|
||||
if opts.unindentLastLine {
|
||||
depth--
|
||||
}
|
||||
out = appendIndent(out, depth)
|
||||
} else if len(out) > 0 {
|
||||
out = append(out, ' ')
|
||||
}
|
||||
// Emit a space if the output is empty.
|
||||
if opts.appendSpaceIfEmpty && len(out) == 0 {
|
||||
out = append(out, ' ')
|
||||
}
|
||||
|
||||
// Copy intermediate output to the receiver.
|
||||
if !bytes.Equal(*b, out) {
|
||||
*b = append((*b)[:0], out...)
|
||||
}
|
||||
}
|
||||
|
||||
// alignObjectValues aligns object values by inserting spaces after the name
|
||||
// so that the values are aligned to the same column.
|
||||
//
|
||||
// It always returns true to be compatible with composite.rangeValues.
|
||||
func (v *Value) alignObjectValues() bool {
|
||||
// TODO(dsnet): This is broken for non-monospace, non-narrow characters.
|
||||
// This is hard to fix as even `go fmt` suffers from this problem.
|
||||
// See https://golang.org/issue/8273.
|
||||
if obj, ok := v.Value.(*Object); ok {
|
||||
type row struct {
|
||||
extra *Extra // pointer to extra after colon and before value
|
||||
length int // length from start of name to end of extra
|
||||
}
|
||||
var rows []row
|
||||
alignRows := func() {
|
||||
// TODO(dsnet): Should we break apart rows if the number of spaces
|
||||
// to insert exceeds some threshold?
|
||||
|
||||
// Compute the maximum width.
|
||||
var max int
|
||||
for _, row := range rows {
|
||||
if max < row.length {
|
||||
max = row.length
|
||||
}
|
||||
}
|
||||
// Align every row up to that width.
|
||||
for _, row := range rows {
|
||||
for n := max - row.length; n > 0; n-- {
|
||||
*row.extra = append(*row.extra, ' ')
|
||||
}
|
||||
}
|
||||
// Reset the sequence of rows.
|
||||
rows = rows[:0]
|
||||
}
|
||||
var indentSuffix []byte
|
||||
for i := range obj.Members {
|
||||
name := &obj.Members[i].Name
|
||||
value := &obj.Members[i].Value
|
||||
|
||||
// Whitespace right before name must have a newline and
|
||||
// everything after the name until the comma cannot have newlines.
|
||||
if !name.BeforeExtra.hasNewline() ||
|
||||
name.hasNewline(false) ||
|
||||
name.AfterExtra.hasNewline() ||
|
||||
value.BeforeExtra.hasNewline() ||
|
||||
value.hasNewline(false) ||
|
||||
value.AfterExtra.hasNewline() {
|
||||
alignRows()
|
||||
continue
|
||||
}
|
||||
|
||||
// If there are multiple newlines or the indentSuffix mismatches,
|
||||
// then this is the start of a new block or rows to align.
|
||||
if bytes.Count(name.BeforeExtra, newline) > 1 || !bytes.HasSuffix(name.BeforeExtra, indentSuffix) {
|
||||
alignRows() // flush the current block or rows
|
||||
}
|
||||
|
||||
rows = append(rows, row{
|
||||
extra: &value.BeforeExtra,
|
||||
length: len(name.Value.(Literal)) + len(name.AfterExtra) + len(":") + len(value.BeforeExtra),
|
||||
})
|
||||
}
|
||||
alignRows()
|
||||
}
|
||||
|
||||
// Recursively align all sub-objects.
|
||||
if comp, ok := v.Value.(composite); ok {
|
||||
for v2 := range comp.allValues() {
|
||||
v2.alignObjectValues()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v Value) hasNewline(checkTopLevelExtra bool) bool {
|
||||
if checkTopLevelExtra && (v.BeforeExtra.hasNewline() || v.AfterExtra.hasNewline()) {
|
||||
return true
|
||||
}
|
||||
if comp, ok := v.Value.(composite); ok {
|
||||
for v := range comp.allValues() {
|
||||
if v.hasNewline(true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b Extra) hasNewline() bool {
|
||||
return bytes.IndexByte(b, '\n') >= 0
|
||||
}
|
||||
|
||||
func appendIndent(b []byte, n int) []byte {
|
||||
for i := 0; i < n; i++ {
|
||||
b = append(b, '\t')
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user