123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- package toml
- import (
- "bytes"
- "fmt"
- "io"
- "math"
- "math/big"
- "reflect"
- "sort"
- "strconv"
- "strings"
- "time"
- )
- type valueComplexity int
- const (
- valueSimple valueComplexity = iota + 1
- valueComplex
- )
- type sortNode struct {
- key string
- complexity valueComplexity
- }
- // Encodes a string to a TOML-compliant multi-line string value
- // This function is a clone of the existing encodeTomlString function, except that whitespace characters
- // are preserved. Quotation marks and backslashes are also not escaped.
- func encodeMultilineTomlString(value string, commented string) string {
- var b bytes.Buffer
- adjacentQuoteCount := 0
- b.WriteString(commented)
- for i, rr := range value {
- if rr != '"' {
- adjacentQuoteCount = 0
- } else {
- adjacentQuoteCount++
- }
- switch rr {
- case '\b':
- b.WriteString(`\b`)
- case '\t':
- b.WriteString("\t")
- case '\n':
- b.WriteString("\n" + commented)
- case '\f':
- b.WriteString(`\f`)
- case '\r':
- b.WriteString("\r")
- case '"':
- if adjacentQuoteCount >= 3 || i == len(value)-1 {
- adjacentQuoteCount = 0
- b.WriteString(`\"`)
- } else {
- b.WriteString(`"`)
- }
- case '\\':
- b.WriteString(`\`)
- default:
- intRr := uint16(rr)
- if intRr < 0x001F {
- b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
- } else {
- b.WriteRune(rr)
- }
- }
- }
- return b.String()
- }
- // Encodes a string to a TOML-compliant string value
- func encodeTomlString(value string) string {
- var b bytes.Buffer
- for _, rr := range value {
- switch rr {
- case '\b':
- b.WriteString(`\b`)
- case '\t':
- b.WriteString(`\t`)
- case '\n':
- b.WriteString(`\n`)
- case '\f':
- b.WriteString(`\f`)
- case '\r':
- b.WriteString(`\r`)
- case '"':
- b.WriteString(`\"`)
- case '\\':
- b.WriteString(`\\`)
- default:
- intRr := uint16(rr)
- if intRr < 0x001F {
- b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
- } else {
- b.WriteRune(rr)
- }
- }
- }
- return b.String()
- }
- func tomlTreeStringRepresentation(t *Tree, ord MarshalOrder) (string, error) {
- var orderedVals []sortNode
- switch ord {
- case OrderPreserve:
- orderedVals = sortByLines(t)
- default:
- orderedVals = sortAlphabetical(t)
- }
- var values []string
- for _, node := range orderedVals {
- k := node.key
- v := t.values[k]
- repr, err := tomlValueStringRepresentation(v, "", "", ord, false)
- if err != nil {
- return "", err
- }
- values = append(values, quoteKeyIfNeeded(k)+" = "+repr)
- }
- return "{ " + strings.Join(values, ", ") + " }", nil
- }
- func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, arraysOneElementPerLine bool) (string, error) {
- // this interface check is added to dereference the change made in the writeTo function.
- // That change was made to allow this function to see formatting options.
- tv, ok := v.(*tomlValue)
- if ok {
- v = tv.value
- } else {
- tv = &tomlValue{}
- }
- switch value := v.(type) {
- case uint64:
- return strconv.FormatUint(value, 10), nil
- case int64:
- return strconv.FormatInt(value, 10), nil
- case float64:
- // Default bit length is full 64
- bits := 64
- // Float panics if nan is used
- if !math.IsNaN(value) {
- // if 32 bit accuracy is enough to exactly show, use 32
- _, acc := big.NewFloat(value).Float32()
- if acc == big.Exact {
- bits = 32
- }
- }
- if math.Trunc(value) == value {
- return strings.ToLower(strconv.FormatFloat(value, 'f', 1, bits)), nil
- }
- return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
- case string:
- if tv.multiline {
- if tv.literal {
- b := strings.Builder{}
- b.WriteString("'''\n")
- b.Write([]byte(value))
- b.WriteString("\n'''")
- return b.String(), nil
- } else {
- return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil
- }
- }
- return "\"" + encodeTomlString(value) + "\"", nil
- case []byte:
- b, _ := v.([]byte)
- return string(b), nil
- case bool:
- if value {
- return "true", nil
- }
- return "false", nil
- case time.Time:
- return value.Format(time.RFC3339), nil
- case LocalDate:
- return value.String(), nil
- case LocalDateTime:
- return value.String(), nil
- case LocalTime:
- return value.String(), nil
- case *Tree:
- return tomlTreeStringRepresentation(value, ord)
- case nil:
- return "", nil
- }
- rv := reflect.ValueOf(v)
- if rv.Kind() == reflect.Slice {
- var values []string
- for i := 0; i < rv.Len(); i++ {
- item := rv.Index(i).Interface()
- itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine)
- if err != nil {
- return "", err
- }
- values = append(values, itemRepr)
- }
- if arraysOneElementPerLine && len(values) > 1 {
- stringBuffer := bytes.Buffer{}
- valueIndent := indent + ` ` // TODO: move that to a shared encoder state
- stringBuffer.WriteString("[\n")
- for _, value := range values {
- stringBuffer.WriteString(valueIndent)
- stringBuffer.WriteString(commented + value)
- stringBuffer.WriteString(`,`)
- stringBuffer.WriteString("\n")
- }
- stringBuffer.WriteString(indent + commented + "]")
- return stringBuffer.String(), nil
- }
- return "[" + strings.Join(values, ", ") + "]", nil
- }
- return "", fmt.Errorf("unsupported value type %T: %v", v, v)
- }
- func getTreeArrayLine(trees []*Tree) (line int) {
- // Prevent returning 0 for empty trees
- line = int(^uint(0) >> 1)
- // get lowest line number >= 0
- for _, tv := range trees {
- if tv.position.Line < line || line == 0 {
- line = tv.position.Line
- }
- }
- return
- }
- func sortByLines(t *Tree) (vals []sortNode) {
- var (
- line int
- lines []int
- tv *Tree
- tom *tomlValue
- node sortNode
- )
- vals = make([]sortNode, 0)
- m := make(map[int]sortNode)
- for k := range t.values {
- v := t.values[k]
- switch v.(type) {
- case *Tree:
- tv = v.(*Tree)
- line = tv.position.Line
- node = sortNode{key: k, complexity: valueComplex}
- case []*Tree:
- line = getTreeArrayLine(v.([]*Tree))
- node = sortNode{key: k, complexity: valueComplex}
- default:
- tom = v.(*tomlValue)
- line = tom.position.Line
- node = sortNode{key: k, complexity: valueSimple}
- }
- lines = append(lines, line)
- vals = append(vals, node)
- m[line] = node
- }
- sort.Ints(lines)
- for i, line := range lines {
- vals[i] = m[line]
- }
- return vals
- }
- func sortAlphabetical(t *Tree) (vals []sortNode) {
- var (
- node sortNode
- simpVals []string
- compVals []string
- )
- vals = make([]sortNode, 0)
- m := make(map[string]sortNode)
- for k := range t.values {
- v := t.values[k]
- switch v.(type) {
- case *Tree, []*Tree:
- node = sortNode{key: k, complexity: valueComplex}
- compVals = append(compVals, node.key)
- default:
- node = sortNode{key: k, complexity: valueSimple}
- simpVals = append(simpVals, node.key)
- }
- vals = append(vals, node)
- m[node.key] = node
- }
- // Simples first to match previous implementation
- sort.Strings(simpVals)
- i := 0
- for _, key := range simpVals {
- vals[i] = m[key]
- i++
- }
- sort.Strings(compVals)
- for _, key := range compVals {
- vals[i] = m[key]
- i++
- }
- return vals
- }
- func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
- return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, " ", false, false)
- }
- func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord MarshalOrder, indentString string, compactComments, parentCommented bool) (int64, error) {
- var orderedVals []sortNode
- switch ord {
- case OrderPreserve:
- orderedVals = sortByLines(t)
- default:
- orderedVals = sortAlphabetical(t)
- }
- for _, node := range orderedVals {
- switch node.complexity {
- case valueComplex:
- k := node.key
- v := t.values[k]
- combinedKey := quoteKeyIfNeeded(k)
- if keyspace != "" {
- combinedKey = keyspace + "." + combinedKey
- }
- switch node := v.(type) {
- // node has to be of those two types given how keys are sorted above
- case *Tree:
- tv, ok := t.values[k].(*Tree)
- if !ok {
- return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
- }
- if tv.comment != "" {
- comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1)
- start := "# "
- if strings.HasPrefix(comment, "#") {
- start = ""
- }
- writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment)
- bytesCount += int64(writtenBytesCountComment)
- if errc != nil {
- return bytesCount, errc
- }
- }
- var commented string
- if parentCommented || t.commented || tv.commented {
- commented = "# "
- }
- writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
- bytesCount += int64(writtenBytesCount)
- if err != nil {
- return bytesCount, err
- }
- bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || tv.commented)
- if err != nil {
- return bytesCount, err
- }
- case []*Tree:
- for _, subTree := range node {
- var commented string
- if parentCommented || t.commented || subTree.commented {
- commented = "# "
- }
- writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
- bytesCount += int64(writtenBytesCount)
- if err != nil {
- return bytesCount, err
- }
- bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || subTree.commented)
- if err != nil {
- return bytesCount, err
- }
- }
- }
- default: // Simple
- k := node.key
- v, ok := t.values[k].(*tomlValue)
- if !ok {
- return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
- }
- var commented string
- if parentCommented || t.commented || v.commented {
- commented = "# "
- }
- repr, err := tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
- if err != nil {
- return bytesCount, err
- }
- if v.comment != "" {
- comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1)
- start := "# "
- if strings.HasPrefix(comment, "#") {
- start = ""
- }
- if !compactComments {
- writtenBytesCountComment, errc := writeStrings(w, "\n")
- bytesCount += int64(writtenBytesCountComment)
- if errc != nil {
- return bytesCount, errc
- }
- }
- writtenBytesCountComment, errc := writeStrings(w, indent, start, comment, "\n")
- bytesCount += int64(writtenBytesCountComment)
- if errc != nil {
- return bytesCount, errc
- }
- }
- quotedKey := quoteKeyIfNeeded(k)
- writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
- bytesCount += int64(writtenBytesCount)
- if err != nil {
- return bytesCount, err
- }
- }
- }
- return bytesCount, nil
- }
- // quote a key if it does not fit the bare key format (A-Za-z0-9_-)
- // quoted keys use the same rules as strings
- func quoteKeyIfNeeded(k string) string {
- // when encoding a map with the 'quoteMapKeys' option enabled, the tree will contain
- // keys that have already been quoted.
- // not an ideal situation, but good enough of a stop gap.
- if len(k) >= 2 && k[0] == '"' && k[len(k)-1] == '"' {
- return k
- }
- isBare := true
- for _, r := range k {
- if !isValidBareChar(r) {
- isBare = false
- break
- }
- }
- if isBare {
- return k
- }
- return quoteKey(k)
- }
- func quoteKey(k string) string {
- return "\"" + encodeTomlString(k) + "\""
- }
- func writeStrings(w io.Writer, s ...string) (int, error) {
- var n int
- for i := range s {
- b, err := io.WriteString(w, s[i])
- n += b
- if err != nil {
- return n, err
- }
- }
- return n, nil
- }
- // WriteTo encode the Tree as Toml and writes it to the writer w.
- // Returns the number of bytes written in case of success, or an error if anything happened.
- func (t *Tree) WriteTo(w io.Writer) (int64, error) {
- return t.writeTo(w, "", "", 0, false)
- }
- // ToTomlString generates a human-readable representation of the current tree.
- // Output spans multiple lines, and is suitable for ingest by a TOML parser.
- // If the conversion cannot be performed, ToString returns a non-nil error.
- func (t *Tree) ToTomlString() (string, error) {
- b, err := t.Marshal()
- if err != nil {
- return "", err
- }
- return string(b), nil
- }
- // String generates a human-readable representation of the current tree.
- // Alias of ToString. Present to implement the fmt.Stringer interface.
- func (t *Tree) String() string {
- result, _ := t.ToTomlString()
- return result
- }
- // ToMap recursively generates a representation of the tree using Go built-in structures.
- // The following types are used:
- //
- // * bool
- // * float64
- // * int64
- // * string
- // * uint64
- // * time.Time
- // * map[string]interface{} (where interface{} is any of this list)
- // * []interface{} (where interface{} is any of this list)
- func (t *Tree) ToMap() map[string]interface{} {
- result := map[string]interface{}{}
- for k, v := range t.values {
- switch node := v.(type) {
- case []*Tree:
- var array []interface{}
- for _, item := range node {
- array = append(array, item.ToMap())
- }
- result[k] = array
- case *Tree:
- result[k] = node.ToMap()
- case *tomlValue:
- result[k] = tomlValueToGo(node.value)
- }
- }
- return result
- }
- func tomlValueToGo(v interface{}) interface{} {
- if tree, ok := v.(*Tree); ok {
- return tree.ToMap()
- }
- rv := reflect.ValueOf(v)
- if rv.Kind() != reflect.Slice {
- return v
- }
- values := make([]interface{}, rv.Len())
- for i := 0; i < rv.Len(); i++ {
- item := rv.Index(i).Interface()
- values[i] = tomlValueToGo(item)
- }
- return values
- }
|