Skip to content

Commit

Permalink
add encode
Browse files Browse the repository at this point in the history
  • Loading branch information
laktak committed Aug 7, 2016
1 parent 13d460a commit 13bcdf3
Show file tree
Hide file tree
Showing 34 changed files with 1,071 additions and 20 deletions.
330 changes: 330 additions & 0 deletions encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@

package hjson

import (
"bytes"
"errors"
"fmt"
"math"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
)

type EncoderOptions struct {
Eol string
BracesSameLine bool
EmitRootBraces bool
QuoteAlways bool
IndentBy string
AllowMinusZero bool
UnknownAsNull bool
}

func DefaultOptions() EncoderOptions {
opt := EncoderOptions{}
opt.Eol = "\n"
opt.BracesSameLine = false
opt.EmitRootBraces = true
opt.QuoteAlways = false
opt.IndentBy = " "
opt.AllowMinusZero = false
opt.UnknownAsNull = false
return opt
}

type hjsonEncoder struct {
bytes.Buffer // output
EncoderOptions
indent int
}

var needsEscape, needsQuotes, needsQuotes2, needsEscapeML, startsWithKeyword, needsEscapeName *regexp.Regexp

func init() {
var err error
// needsEscape is used to detect and replace characters
needsEscape, err = regexp.Compile(`[\\\"\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
if err != nil { panic(err) }
// like needsEscape but without \\ and \"
needsQuotes, err = regexp.Compile(`[\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
if err != nil { panic(err) }
needsQuotes2, err = regexp.Compile(`^\s|^"|^'''|^#|^/\*|^//|^\{|^\[|\s$`)
if err != nil { panic(err) }
// ''' || (needsQuotes but without \n and \r)
needsEscapeML, err = regexp.Compile(`'''|[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}]`)
if err != nil { panic(err) }
// starts with a keyword and optionally is followed by a comment
startsWithKeyword, err = regexp.Compile(`^(true|false|null)\s*((,|\]|\}|#|//|/\*).*)?$`)
if err != nil { panic(err) }
needsEscapeName, err = regexp.Compile(`[,\{\[\}\]\s:#"]|//|/\*|'''`)
if err != nil { panic(err) }
}

var meta map[byte][]byte = map[byte][]byte {
// table of character substitutions
'\b': []byte("\\b"),
'\t': []byte("\\t"),
'\n': []byte("\\n"),
'\f': []byte("\\f"),
'\r': []byte("\\r"),
'"' : []byte("\\\""),
'\\': []byte("\\\\"),
}

func (e *hjsonEncoder) quoteReplace(text string) (string) {
return string(needsEscape.ReplaceAllFunc([]byte(text), func(a []byte) []byte {
c := meta[a[0]]
if c != nil {
return c
} else {
return []byte(fmt.Sprintf("\\u%04x", c))
}
}))
}

func (e *hjsonEncoder) quote(value string, separator string, isRootObject bool) {

// Check if we can insert this string without quotes
// see hjson syntax (must not parse as true, false, null or number)

if len(value) == 0 {
e.WriteString(separator + `""`)
} else if (e.QuoteAlways ||
needsQuotes.MatchString(value) || needsQuotes2.MatchString(value) ||
startsWithNumber([]byte(value)) ||
startsWithKeyword.MatchString(value)) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we first check if the string can be expressed in multiline
// format or we must replace the offending characters with safe escape
// sequences.

if !needsEscape.MatchString(value) {
e.WriteString(separator + `"` + value + `"`)
} else if !needsEscapeML.MatchString(value) && !isRootObject {
e.mlString(value, separator)
} else {
e.WriteString(separator + `"` + e.quoteReplace(value) + `"`)
}
} else {
// return without quotes
e.WriteString(separator + value)
}
}

func (e *hjsonEncoder) mlString(value string, separator string) {
// wrap the string into the ''' (multiline) format

a := strings.Split(strings.Replace(value, "\r", "", -1), "\n")

if len(a) == 1 {
// The string contains only a single line. We still use the multiline
// format as it avoids escaping the \ character (e.g. when used in a
// regex).
e.WriteString(separator + "'''")
e.WriteString(a[0])
} else {
e.writeIndent(e.indent + 1)
e.WriteString("'''")
for _, v := range a {
indent := e.indent + 1
if len(v) == 0 { indent = 0 }
e.writeIndent(indent)
e.WriteString(v)
}
e.writeIndent(e.indent + 1)
}
e.WriteString("'''")
}

func (e *hjsonEncoder) quoteName(name string) (string) {
if len(name) == 0 {
return `""`
}

// Check if we can insert this name without quotes

if needsEscapeName.MatchString(name) {
if needsEscape.MatchString(name) { name = e.quoteReplace(name) }
return `"` + name + `"`
} else {
// without quotes
return name
}
}

type SortAlpha []reflect.Value

func (s SortAlpha) Len() int {
return len(s)
}
func (s SortAlpha) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s SortAlpha) Less(i, j int) bool {
return s[i].String() < s[j].String()
}

func (e *hjsonEncoder) writeIndent(indent int) {
e.WriteString(e.Eol)
for i := 0; i < indent; i++ {
e.WriteString(e.IndentBy)
}
}

func (e *hjsonEncoder) str(value reflect.Value, noIndent bool, separator string, isRootObject bool) (error) {

// Produce a string from value.

kind := value.Kind()

if kind == reflect.Interface || kind == reflect.Ptr {
if value.IsNil() {
e.WriteString(separator)
e.WriteString("null")
return nil
} else {
value = value.Elem()
kind = value.Kind()
}
}


switch kind {
case reflect.String:
e.quote(value.String(), separator, isRootObject)

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
e.WriteString(separator)
e.WriteString(strconv.FormatInt(value.Int(), 10))

case reflect.Float32, reflect.Float64:
// JSON numbers must be finite. Encode non-finite numbers as null.
e.WriteString(separator)
number := value.Float()
if math.IsInf(number, 0) || math.IsNaN(number) {
e.WriteString("null");
} else if !e.AllowMinusZero && number == -0 {
e.WriteString("0")
} else {
// find shortest representation ('G' does not work)
val := strconv.FormatFloat(number, 'f', -1, 64)
exp := strconv.FormatFloat(number, 'E', -1, 64)
if len(exp) < len(val) { val = strings.ToLower(exp) }
e.WriteString(val)
}

case reflect.Bool:
e.WriteString(separator)
if value.Bool() {
e.WriteString("true")
} else {
e.WriteString("false")
}

case reflect.Slice, reflect.Array:

len := value.Len()
if len == 0 {
e.WriteString(separator)
e.WriteString("[]")
break
}

indent1 := e.indent
e.indent++

if !noIndent && !e.BracesSameLine {
e.writeIndent(indent1)
} else {
e.WriteString(separator)
}
e.WriteString("[")

// Join all of the element texts together, separated with newlines
for i := 0; i < len; i++ {
e.writeIndent(e.indent)
if err := e.str(value.Index(i), true, "", false); err != nil { return err }
}

e.writeIndent(indent1)
e.WriteString("]")

e.indent = indent1

case reflect.Map:

len := value.Len()
if len == 0 {
e.WriteString(separator)
e.WriteString("{}")
break
}

showBraces := !isRootObject || e.EmitRootBraces
indent1 := e.indent
e.indent++

if (showBraces) {
if !noIndent && !e.BracesSameLine {
e.writeIndent(indent1)
} else {
e.WriteString(separator)
}
e.WriteString("{")
}

keys := value.MapKeys()
sort.Sort(SortAlpha(keys))

// Join all of the member texts together, separated with newlines
for i := 0; i < len; i++ {
e.writeIndent(e.indent)
e.WriteString(e.quoteName(keys[i].String()))
e.WriteString(":")
if err := e.str(value.MapIndex(keys[i]), false, " ", false); err != nil { return err }
}

if (showBraces) {
e.writeIndent(indent1)
e.WriteString("}")
}

e.indent = indent1

default:
if e.UnknownAsNull {
// Use null as a placeholder for non-JSON values.
e.WriteString("null")
} else {
return errors.New("Unsupported type " + value.Type().String())
}
}
return nil
}

func Marshal(v interface{}) ([]byte, error) {
return MarshalWithOptions(v, DefaultOptions())
}

func MarshalWithOptions(v interface{}, options EncoderOptions) ([]byte, error) {
e := &hjsonEncoder{}
e.indent = 0
e.Eol = options.Eol
e.BracesSameLine = options.BracesSameLine
e.EmitRootBraces = options.EmitRootBraces
e.QuoteAlways = options.QuoteAlways
e.IndentBy = options.IndentBy

err := e.str(reflect.ValueOf(v), true, "", true)
if err != nil {
return nil, err
}
return e.Bytes(), nil
}
14 changes: 7 additions & 7 deletions hjson-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ func fixJson(data []byte) ([]byte) {
data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
data = bytes.Replace(data, []byte("\\u0008"), []byte("\\b"), -1)
data = bytes.Replace(data, []byte("\\u000c"), []byte("\\f"), -1)
return data
}

Expand Down Expand Up @@ -52,8 +54,6 @@ func main() {
}
if err != nil { panic(err) }

// fmt.Print(string(data))

var value interface{}

if err := hjson.Unmarshal(data, &value); err != nil {
Expand All @@ -62,15 +62,15 @@ func main() {

var out []byte
if *showCompact {
out, _ = json.Marshal(value)
out, err = json.Marshal(value)
if err != nil { panic(err) }
out = fixJson(out)
} else if *showJson {
out, _ = json.MarshalIndent(value, "", " ")
out, err = json.MarshalIndent(value, "", " ")
if err != nil { panic(err) }
out = fixJson(out)
} else {
out, _ = json.MarshalIndent(value, "", " ")
out = fixJson(out)
//out, _ = hjson.MarshalIndent(value, "", " ")
out, err = hjson.Marshal(value)
}

fmt.Println(string(out))
Expand Down
10 changes: 9 additions & 1 deletion parseNumber.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ func (p *parseNumber) next() (bool) {
}
}

func startsWithNumber(text []byte) (bool) {
if _, err := tryParseNumber(text, true); err == nil {
return true
} else {
return false
}
}

func tryParseNumber(text []byte, stopAtNext bool) (float64, error) {
// Parse a number value.

Expand Down Expand Up @@ -74,7 +82,7 @@ func tryParseNumber(text []byte, stopAtNext bool) (float64, error) {
p.ch == '#' || p.ch == '/' && (p.data[p.at] == '/' || p.data[p.at] == '*') { p.ch = 0 }
}

if p.ch > 0 || leadingZeros > 0 {
if p.ch > 0 || leadingZeros != 0 {
return 0, errors.New("Invalid number")
}
if number, err := strconv.ParseFloat(string(p.data[0:end-1]), 64); err != nil {
Expand Down
Loading

0 comments on commit 13bcdf3

Please sign in to comment.