jrFlint 2 lat temu
commit
5ff5c92d0d
24 zmienionych plików z 1607 dodań i 0 usunięć
  1. 21 0
      LICENSE
  2. 64 0
      cell.go
  3. 148 0
      cell_formula.go
  4. 70 0
      cell_number.go
  5. 66 0
      cell_object.go
  6. 33 0
      cell_string.go
  7. 57 0
      dropdown.go
  8. 11 0
      export.go
  9. 183 0
      export_exel.go
  10. 36 0
      export_string.go
  11. 10 0
      go.mod
  12. 34 0
      go.sum
  13. 70 0
      import.go
  14. 204 0
      json_map.go
  15. 15 0
      matrix.go
  16. BIN
      resource/img.jpg
  17. 76 0
      row.go
  18. 59 0
      style.go
  19. 130 0
      style_group.go
  20. BIN
      tmp.xlsx
  21. 143 0
      xvdoc.go
  22. 71 0
      z_source (копия).json
  23. 69 0
      z_source.json
  24. 37 0
      z_test.go

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 fcg-xvii
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 64 - 0
cell.go

@@ -0,0 +1,64 @@
+package xvdoc
+
+import (
+	"encoding/json"
+)
+
+type Cell interface {
+	Val() interface{}
+	Type() string
+	Styles() []string
+}
+
+type CellConstructor func(*json.Decoder, json.Token) (Cell, error)
+
+var (
+	cellConstructors []CellConstructor
+)
+
+func AppendCellConstructor(constructor CellConstructor) {
+	cellConstructors = append(cellConstructors, constructor)
+}
+
+func init() {
+	cellConstructors = []CellConstructor{
+		constructorString,
+		constructorNumber,
+		constructorObject,
+	}
+}
+
+func parseCell(dec *json.Decoder, t json.Token) (cell Cell, err error) {
+	for _, constructor := range cellConstructors {
+		if cell, err = constructor(dec, t); cell != nil || err != nil {
+			return
+		}
+	}
+	return
+}
+
+func parseCells(dec *json.Decoder) (res []Cell, err error) {
+	var (
+		t    json.Token
+		cell Cell
+	)
+	if t, err = dec.Token(); err == nil {
+		if _, check := t.(json.Delim); check && t == json.Delim('[') {
+			for err == nil {
+				if t, err = dec.Token(); err == nil {
+					if _, check := t.(json.Delim); check && t == json.Delim(']') {
+						return
+					}
+				}
+				if cell, err = parseCell(dec, t); err == nil && cell != nil {
+					res = append(res, cell)
+				}
+			}
+		} else {
+			if cell, err = parseCell(dec, t); err == nil && cell != nil {
+				res = []Cell{cell}
+			}
+		}
+	}
+	return
+}

+ 148 - 0
cell_formula.go

@@ -0,0 +1,148 @@
+package xvdoc
+
+import (
+	"fmt"
+	"strings"
+)
+
+func constructorFormula(m JSONMap) (res Cell, err error) {
+	if method := m.StringVal("method", ""); len(method) > 0 {
+		var args []CellArg
+		if ai := m.Array("args"); len(ai) > 0 {
+			var arg CellArg
+			args = make([]CellArg, len(ai))
+			for i, v := range ai {
+				if arg, err = argFromIface(v); err == nil {
+					args[i] = arg
+				} else {
+					return
+				}
+			}
+		}
+		res = &CellFormula{
+			method: method,
+			args:   args,
+			style:  m.StringVal("style", ""),
+			styles: m.StringArray("styles"),
+		}
+	} else {
+		err = fmt.Errorf("Formula constructor error: empty method")
+	}
+	return
+}
+
+// CellFormula is cell of formula for excel or other types with formula support
+type CellFormula struct {
+	method string
+	args   []CellArg
+	styles []string
+	style  string
+}
+
+// Val is method of getter formula string (Cell interface)
+func (s *CellFormula) Val() interface{} {
+	return s.method
+}
+
+// Type method is value type getter (formula)
+func (s *CellFormula) Type() string {
+	return "formula"
+}
+
+func (s *CellFormula) String() string {
+	return fmt.Sprintf("{ formula :: %v, args: [ %v ]}", s.method, s.args)
+}
+
+// ReplaceArgs is replace variable templates ($1, $2, etc) to valus from incoming array
+func (s *CellFormula) ReplaceArgs(args []string) (res string) {
+	res = s.method
+	for i, arg := range args {
+		res = strings.ReplaceAll(res, fmt.Sprintf("$%v", i+1), arg)
+	}
+	return res
+}
+
+// Style is getter of cell style
+func (s *CellFormula) Styles() (res []string) {
+	if len(s.styles) > 0 {
+		res = s.styles
+	} else if len(s.style) > 0 {
+		res = []string{s.style}
+	}
+	return
+}
+
+////////////////////////////////////////////
+
+type argType uint8
+
+const (
+	argTypeValue argType = iota
+	argTypeRow
+	argTypeCell
+)
+
+func (s argType) String() string {
+	switch s {
+	case argTypeRow:
+		return "row"
+	case argTypeCell:
+		return "cell"
+	default:
+		return "value"
+	}
+}
+
+func argFromIface(i interface{}) (arg CellArg, err error) {
+	if _, check := i.(map[string]interface{}); !check {
+		err = fmt.Errorf("Cell argument parse Error :: object expected")
+	} else {
+		m := JSONMap(i.(map[string]interface{}))
+		switch m.StringVal("type", "") {
+		case "cell":
+			arg.aType = argTypeCell
+		case "row":
+			arg.aType = argTypeRow
+		default:
+			if m.KeyExists("value") {
+				arg.aType = argTypeValue
+			} else {
+				err = fmt.Errorf("Unexpected argument type (type or value field expected)")
+			}
+		}
+		delete(m, "type")
+		arg.data = m
+	}
+	return
+}
+
+// CellArg is argument object of cell
+type CellArg struct {
+	aType argType
+	data  JSONMap
+}
+
+func (s CellArg) String() string {
+	return fmt.Sprintf("{ %v, data: %v }", s.aType, s.data)
+}
+
+// Value is getter of argument value
+func (s CellArg) Value() interface{} {
+	return s.data["value"]
+}
+
+// Coords calculate real argument coords by current args
+func (s CellArg) Coords(x, y int) (dx, dy int) {
+	dx, dy = x, y
+	if s.data.KeyExists("x") {
+		dx = int(s.data.Int("x", 0))
+	} else if s.data.KeyExists("ox") {
+		dx += int(s.data.Int("ox", 0))
+	}
+	if s.data.KeyExists("y") {
+		dy = int(s.data.Int("y", 0))
+	} else if s.data.KeyExists("oy") {
+		dy += int(s.data.Int("oy", 0))
+	}
+	return
+}

+ 70 - 0
cell_number.go

@@ -0,0 +1,70 @@
+package xvdoc
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+func constructorNumber(dec *json.Decoder, t json.Token) (res Cell, err error) {
+	if _, check := t.(float64); check {
+		var (
+			lInt   int64
+			lFloat float64
+		)
+		lIntType, lFloatType := reflect.TypeOf(lInt), reflect.TypeOf(lFloat)
+		rVal := reflect.ValueOf(t)
+		if rVal.Type().ConvertibleTo(lIntType) && strings.IndexByte(fmt.Sprint(t), '.') < 0 {
+			res = &CellInteger{rVal.Convert(lIntType).Int()}
+		} else if rVal.Type().ConvertibleTo(lFloatType) {
+			res = &CellFloat{rVal.Convert(lFloatType).Float()}
+		}
+	}
+	return
+}
+
+// CellInteger is cell with integer type value
+type CellInteger struct {
+	val int64
+}
+
+// Val method is value getter
+func (s *CellInteger) Val() interface{} {
+	return s.val
+}
+
+// Type method is value type getter
+func (s *CellInteger) Type() string {
+	return "number"
+}
+
+func (s *CellInteger) String() string {
+	return fmt.Sprintf("{ int64: %v }", s.val)
+}
+
+func (s *CellInteger) Styles() []string {
+	return nil
+}
+
+//////////////////////////////////
+
+type CellFloat struct {
+	val float64
+}
+
+func (s *CellFloat) Val() interface{} {
+	return s.val
+}
+
+func (s *CellFloat) Type() string {
+	return "float"
+}
+
+func (s *CellFloat) String() string {
+	return fmt.Sprintf("{ float64: %v }", s.val)
+}
+
+func (s *CellFloat) Styles() []string {
+	return nil
+}

+ 66 - 0
cell_object.go

@@ -0,0 +1,66 @@
+package xvdoc
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type ObjConstructor func(JSONMap) (Cell, error)
+
+var (
+	objConstructors = map[string]ObjConstructor{
+		"formula": constructorFormula,
+	}
+)
+
+func constructorObject(dec *json.Decoder, t json.Token) (res Cell, err error) {
+	m := make(JSONMap)
+	if _, check := t.(json.Delim); check && t == json.Delim('{') {
+		for err == nil {
+			if t, err = dec.Token(); err == nil {
+				if _, check := t.(json.Delim); check {
+					if t == json.Delim('}') {
+						if constructor, check := objConstructors[m.StringVal("type", "")]; check {
+							res, err = constructor(m)
+						} else {
+							res = &CellObj{
+								value:  m.Interface("value"),
+								styles: m.StringArray("styles"),
+							}
+						}
+						return
+					}
+				} else {
+					var i interface{}
+					if err = dec.Decode(&i); err == nil {
+						m[t.(string)] = i
+					}
+				}
+			}
+		}
+	}
+	return
+}
+
+////////////////////////////////////////////
+
+type CellObj struct {
+	value  interface{}
+	styles []string
+}
+
+func (s *CellObj) Val() interface{} {
+	return s.value
+}
+
+func (s *CellObj) Type() string {
+	return ""
+}
+
+func (s *CellObj) String() string {
+	return fmt.Sprintf("{ value :: %v }", s.value)
+}
+
+func (s *CellObj) Styles() (res []string) {
+	return s.styles
+}

+ 33 - 0
cell_string.go

@@ -0,0 +1,33 @@
+package xvdoc
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+func constructorString(dec *json.Decoder, t json.Token) (res Cell, err error) {
+	if str, check := t.(string); check {
+		res = &CellString{str}
+	}
+	return
+}
+
+type CellString struct {
+	val string
+}
+
+func (s *CellString) Val() interface{} {
+	return s.val
+}
+
+func (s *CellString) Type() string {
+	return "string"
+}
+
+func (s *CellString) String() string {
+	return fmt.Sprintf("{ string: %v }", s.val)
+}
+
+func (s *CellString) Styles() []string {
+	return nil
+}

+ 57 - 0
dropdown.go

@@ -0,0 +1,57 @@
+package xvdoc
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+)
+
+type dropDown struct {
+	values []string
+	x      int
+	y      int
+	dx     int
+	dy     int
+}
+
+func (s *dropDown) UnmarshalJSON(src []byte) (err error) {
+	s.y, s.x, s.dy, s.dx = -1, -1, -1, -1
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var empty interface{}
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "dx":
+					err = dec.Decode(&s.dx)
+				case "dy":
+					err = dec.Decode(&s.dy)
+				case "x":
+					err = dec.Decode(&s.x)
+				case "y":
+					err = dec.Decode(&s.y)
+				case "values":
+					err = dec.Decode(&s.values)
+				default:
+					dec.Decode(&empty)
+				}
+			}
+		}
+	}
+	if s.x == -1 {
+		err = fmt.Errorf("xvdoc dropdown list parse error :: expected x field")
+		return
+	}
+	if s.y == -1 {
+		err = fmt.Errorf("xvdoc dropdown list parse error :: expected y field")
+		return
+	}
+	if s.dx == -1 {
+		s.dx = s.x
+	}
+	if s.dy == -1 {
+		s.dy = -1
+	}
+	return
+}

+ 11 - 0
export.go

@@ -0,0 +1,11 @@
+package xvdoc
+
+type ExportMethod func(matrix []Matrix) ([]byte, error)
+
+var (
+	exportMethods = map[string]ExportMethod{
+		"string": exportString,
+		"csv":    exportCSV,
+		"xlsx":   exportXLSX,
+	}
+)

+ 183 - 0
export_exel.go

@@ -0,0 +1,183 @@
+package xvdoc
+
+import (
+	"bytes"
+	"fmt"
+	_ "image/gif"
+	_ "image/jpeg"
+	_ "image/png"
+
+	"github.com/xuri/excelize/v2"
+)
+
+func exportXLSX(mList []Matrix) (res []byte, err error) {
+	if len(mList) == 0 {
+		return nil, fmt.Errorf("XVDoc export xlsx error :: expexcted not empty matrix list")
+	}
+
+	// create excelize file and setup sheet name
+	f := excelize.NewFile()
+	for index, matrix := range mList {
+		sName := matrix.Name
+		if len(sName) == 0 {
+			sName = fmt.Sprintf("Sheet%v", index)
+		}
+		if index == 0 {
+			f.SetSheetName("Sheet1", sName)
+		} else {
+			f.NewSheet(sName)
+		}
+		////////////////
+
+		// setup global styles
+		var sID int
+		xStyles := make(map[string]int)
+		for name, style := range matrix.Styles {
+			var styleRes JSONMap
+			if styleRes, err = style.Result(matrix.Styles, name); err != nil {
+				return
+			}
+			//fmt.Println("!!!!!!!!!!!!!!", name, string(styleRes.JSON()))
+			if sID, err = f.NewStyle(string(styleRes.JSON())); err != nil {
+				return
+			}
+			xStyles[name] = sID
+		}
+		////////////////
+
+		// setup cells values
+		var coords, cellName string
+		for rIndex, row := range matrix.Cells {
+			for cIndex, cell := range row {
+				if coords, err = excelize.CoordinatesToCellName(cIndex+1, rIndex+1); err == nil {
+					if cell != nil {
+						if cell.Type() == "formula" {
+							formula := cell.(*CellFormula)
+							args := make([]string, len(formula.args))
+							for i, arg := range formula.args {
+								if arg.aType != argTypeValue {
+									x, y := arg.Coords(cIndex, rIndex)
+									if cellName, err = excelize.CoordinatesToCellName(int(x)+1, int(y)+1); err != nil {
+										return
+									} else {
+										args[i] = cellName
+									}
+								} else {
+									args[i] = fmt.Sprint(arg.Value())
+								}
+							}
+							if err = f.SetCellFormula(sName, coords, formula.ReplaceArgs(args)); err != nil {
+								return
+							}
+						} else {
+							f.SetCellValue(sName, coords, cell.Val())
+						}
+
+						// setup cell styles
+						if styles := cell.Styles(); len(styles) > 0 {
+							//fmt.Println("&&&&", styles)
+							for _, style := range styles {
+								if sID, check := xStyles[style]; check {
+									if err = f.SetCellStyle(sName, coords, coords, sID); err != nil {
+										return
+									}
+								}
+							}
+						}
+					}
+				} else {
+					return nil, err
+				}
+			}
+			if height, check := matrix.RowsHeight[rIndex]; check {
+				f.SetRowHeight(sName, rIndex+1, height)
+			}
+		}
+
+		var c, dc string
+
+		// setup rect styles
+		for _, rect := range matrix.StyleRects {
+			if c, err = excelize.CoordinatesToCellName(rect.x+1, rect.y+1); err != nil {
+				return
+			}
+			if dc, err = excelize.CoordinatesToCellName(rect.dx+1, rect.dy+1); err != nil {
+				return
+			}
+			for _, style := range rect.styles {
+				if sID, check := xStyles[style]; check {
+					if err = f.SetCellStyle(sName, c, dc, sID); err != nil {
+						return
+					}
+				}
+			}
+		}
+		///////////////////////////////////
+
+		// setup images
+		for _, image := range matrix.Images {
+			if c, err = excelize.CoordinatesToCellName(image.x+1, image.y+1); err != nil {
+				return
+			}
+			format := ""
+			if image.format != nil {
+				format = string(image.format.JSON())
+			}
+			if err = f.AddPicture(sName, c, image.path, format); err != nil {
+				return
+			}
+		}
+		///////////////////////////////////
+
+		// setup columns width
+		for _, cWidth := range matrix.ColumnsWidth {
+			if c, err = excelize.ColumnNumberToName(cWidth.x + 1); err != nil {
+				return
+			}
+			if dc, err = excelize.ColumnNumberToName(cWidth.dx + 1); err != nil {
+				return
+			}
+			if err = f.SetColWidth(sName, c, dc, cWidth.width); err != nil {
+				return
+			}
+		}
+
+		// setup merges
+		for _, merge := range matrix.Merges {
+			if c, err = excelize.CoordinatesToCellName(merge.x+1, merge.y+1); err != nil {
+				return
+			}
+			if dc, err = excelize.CoordinatesToCellName(merge.dx+1, merge.dy+1); err != nil {
+				return
+			}
+			if err = f.MergeCell(sName, c, dc); err != nil {
+				return
+			}
+		}
+
+		for _, dropdown := range matrix.Dropdowns {
+			if c, err = excelize.CoordinatesToCellName(dropdown.x+1, dropdown.y+1); err != nil {
+				return
+			}
+			if dc, err = excelize.CoordinatesToCellName(dropdown.dx+1, dropdown.dy+1); err != nil {
+				return
+			}
+			r := excelize.NewDataValidation(true)
+			r.Sqref = "A5:A7"
+			r.Sqref = fmt.Sprintf("%v:%v", c, dc)
+			r.SetDropList(dropdown.values)
+			f.AddDataValidation(sName, r)
+		}
+		///////////////////////////////////
+	}
+
+	//r := excelize.NewDataValidation(true)
+	//r.Sqref = "A5:A7"
+	//r.SetDropList([]string{"10", "30", "50"})
+	//f.AddDataValidation("xv-test", r)
+
+	// setup result data
+	var buf bytes.Buffer
+	f.Write(&buf)
+	return buf.Bytes(), nil
+}

+ 36 - 0
export_string.go

@@ -0,0 +1,36 @@
+package xvdoc
+
+import (
+	"bytes"
+	"fmt"
+)
+
+func exportString(matrix []Matrix) ([]byte, error) {
+	if len(matrix) == 0 {
+		return nil, fmt.Errorf("XVDoc export strings error :: expected not empty martix array")
+	}
+	m := matrix[0]
+	var buf bytes.Buffer
+	for i, v := range m.Cells {
+		buf.Write([]byte(fmt.Sprintf("%v: %v\n", i, v)))
+	}
+	return buf.Bytes(), nil
+}
+
+func exportCSV(matrix []Matrix) ([]byte, error) {
+	if len(matrix) == 0 {
+		return nil, fmt.Errorf("XVDoc export CSV error :: expected not empty martix array")
+	}
+	m := matrix[0]
+	var buf bytes.Buffer
+	for _, v := range m.Cells {
+		for i, k := range v {
+			buf.Write([]byte(fmt.Sprint(k.Val())))
+			if i < len(v)-1 {
+				buf.WriteByte(';')
+			}
+		}
+		buf.WriteByte('\n')
+	}
+	return buf.Bytes(), nil
+}

+ 10 - 0
go.mod

@@ -0,0 +1,10 @@
+module git.ali33.ru/fcg-xvii/xvdoc
+
+go 1.16
+
+require (
+	github.com/xuri/excelize/v2 v2.4.1
+	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
+	golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
+	golang.org/x/text v0.3.7 // indirect
+)

+ 34 - 0
go.sum

@@ -0,0 +1,34 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI=
+github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o=
+github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.4.1 h1:veeeFLAJwsNEBPBlDepzPIYS1eLyBVcXNZUW79exZ1E=
+github.com/xuri/excelize/v2 v2.4.1/go.mod h1:rSu0C3papjzxQA3sdK8cU544TebhrPUoTOaGPIh0Q1A=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
+golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 70 - 0
import.go

@@ -0,0 +1,70 @@
+package xvdoc
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/xuri/excelize/v2"
+)
+
+func ImportXLSXFromFile(fileName string) (res []*Sheet, err error) {
+	var f *os.File
+	if f, err = os.Open(fileName); err == nil {
+		res, err = ImportXLSX(f)
+		f.Close()
+	}
+	return
+}
+
+func ImportXLSX(r io.Reader) (res []*Sheet, err error) {
+	var f *excelize.File
+	if f, err = excelize.OpenReader(r); err != nil {
+		return
+	}
+	res = make([]*Sheet, 0, f.SheetCount)
+	for _, sheetName := range f.GetSheetMap() {
+		sheet := Sheet{name: sheetName}
+		if sheet.rows, err = f.GetRows(sheet.name); err != nil {
+			return
+		}
+		res = append(res, &sheet)
+	}
+	return
+}
+
+func ImportXLSXToJSON(r io.Reader) (res []byte, err error) {
+	var sheets []*Sheet
+	if sheets, err = ImportXLSX(r); err != nil {
+		return
+	}
+	res, err = json.Marshal(sheets)
+	return
+}
+
+func ImportXLSXToJSONFromFile(fileName string) (res []byte, err error) {
+	var f *os.File
+	if f, err = os.Open(fileName); err == nil {
+		res, err = ImportXLSXToJSON(f)
+		f.Close()
+	}
+	return
+}
+
+type Sheet struct {
+	name string
+	rows [][]string
+}
+
+func (s *Sheet) String() string {
+	return fmt.Sprint(s.name, s.rows)
+}
+
+func (s *Sheet) MarshalJSON() ([]byte, error) {
+	m := map[string]interface{}{
+		"name": s.name,
+		"rows": s.rows,
+	}
+	return json.Marshal(m)
+}

+ 204 - 0
json_map.go

@@ -0,0 +1,204 @@
+package xvdoc
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strconv"
+)
+
+type JSONMap map[string]interface{}
+
+func (s JSONMap) Bool(key string, defaultVal bool) bool {
+	if res, check := s[key].(bool); check {
+		return res
+	}
+	return defaultVal
+}
+
+func (s JSONMap) Int(key string, defaultVal int64) int64 {
+	if iface, check := s[key]; check {
+		rVal := reflect.ValueOf(iface)
+		switch rVal.Kind() {
+		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			return rVal.Int()
+		case reflect.Float32, reflect.Float64:
+			return int64(rVal.Float())
+		}
+	}
+	return defaultVal
+}
+
+func (s JSONMap) Value(key string, defaultVal interface{}) interface{} {
+	if iface, check := s[key]; check {
+		dVal := reflect.ValueOf(defaultVal)
+		if !dVal.IsValid() {
+			return iface
+		} else {
+			lVal := reflect.ValueOf(iface)
+			if !lVal.IsValid() {
+				return defaultVal
+			} else if lVal.Kind() == dVal.Kind() {
+				return iface
+			} else if lVal.Type().ConvertibleTo(dVal.Type()) {
+				return lVal.Convert(dVal.Type()).Interface()
+			} else {
+				if lVal.Kind() == reflect.String {
+					switch dVal.Kind() {
+					case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+						if val, err := strconv.Atoi(lVal.String()); err != nil {
+							return defaultVal
+						} else {
+							lVal = reflect.ValueOf(val)
+							return lVal.Convert(dVal.Type())
+						}
+					case reflect.Float32, reflect.Float64:
+						if val, err := strconv.ParseFloat(lVal.String(), 64); err != nil {
+							return defaultVal
+						} else {
+							lVal = reflect.ValueOf(val)
+							return lVal.Convert(dVal.Type())
+						}
+					}
+				}
+			}
+		}
+	}
+	return defaultVal
+}
+
+func (s JSONMap) StringVal(key, defaultVal string) string {
+	if iface, check := s[key]; check {
+		return fmt.Sprint(iface)
+	}
+	return defaultVal
+}
+
+func (s JSONMap) Interface(key string) interface{} {
+	return s[key]
+}
+
+func (s JSONMap) StringArray(key string) (res []string) {
+	if arr, check := s[key].([]interface{}); check {
+		res = make([]string, len(arr))
+		for i, v := range arr {
+			res[i] = fmt.Sprint(v)
+		}
+	}
+	return
+}
+
+func (s JSONMap) Array(key string) (res []interface{}) {
+	if arr, check := s[key].([]interface{}); check {
+		res = arr
+	}
+	return
+}
+
+func (s JSONMap) JSONMap(key string) (res JSONMap) {
+	if m, check := s[key].(map[string]interface{}); check {
+		res = JSONMap(m)
+	}
+	return
+}
+
+func (s JSONMap) KeyExists(key string) (check bool) {
+	_, check = s[key]
+	return check
+}
+
+func (s JSONMap) Copy() JSONMap {
+	res := make(JSONMap)
+	for key, val := range s {
+		res[key] = val
+	}
+	return res
+}
+
+func (s JSONMap) JSON() (res []byte) {
+	res, _ = json.Marshal(&s)
+	return
+}
+
+func (s JSONMap) Map() map[string]interface{} {
+	return map[string]interface{}(s)
+}
+
+func copyMap(m map[string]interface{}) (res map[string]interface{}) {
+	res = make(map[string]interface{})
+	for key, val := range m {
+		switch val.(type) {
+		case []interface{}:
+			res[key] = copySlice(val.([]interface{}))
+		case map[string]interface{}:
+			res[key] = copyMap(val.(map[string]interface{}))
+		default:
+			res[key] = val
+		}
+	}
+	return
+}
+
+func copySlice(arr []interface{}) (res []interface{}) {
+	res = make([]interface{}, len(arr))
+	for i, v := range arr {
+		switch v.(type) {
+		case []interface{}:
+			res[i] = copySlice(v.([]interface{}))
+		case map[string]interface{}:
+			res[i] = copyMap(v.(map[string]interface{}))
+		default:
+			res[i] = v
+		}
+	}
+	return
+}
+
+func mergeSlices(aSource, aMerge []interface{}) []interface{} {
+	return append(aSource, aMerge...)
+}
+
+func mergeMaps(mSource, mMerge map[string]interface{}) (res map[string]interface{}) {
+	res = copyMap(mSource)
+
+	// merge result with result map
+	for key, val := range mMerge {
+		if sVal, check := res[key]; check {
+			switch val.(type) {
+			case map[string]interface{}:
+				{
+					if m, check := sVal.(map[string]interface{}); check {
+						res[key] = mergeMaps(m, val.(map[string]interface{}))
+					} else if val == nil {
+						res[key] = val
+					}
+				}
+			case []interface{}:
+				{
+					if arr, check := sVal.([]interface{}); check {
+						res[key] = mergeSlices(arr, val.([]interface{}))
+					} else if val == nil {
+						res[key] = val
+					}
+				}
+			default:
+				res[key] = val
+			}
+		} else {
+			switch val.(type) {
+			case map[string]interface{}:
+				res[key] = copyMap(val.(map[string]interface{}))
+			case []interface{}:
+				res[key] = copySlice(val.([]interface{}))
+			default:
+				res[key] = val
+			}
+		}
+	}
+	return
+}
+
+func mapJSON(m map[string]interface{}) (res []byte) {
+	res, _ = json.Marshal(&m)
+	return
+}

+ 15 - 0
matrix.go

@@ -0,0 +1,15 @@
+package xvdoc
+
+type Matrix struct {
+	Name         string
+	Cells        [][]Cell
+	RowsHeight   map[int]float64
+	Styles       map[string]Style
+	RowsStyle    map[int]JSONMap
+	Style        string
+	StyleRects   []*styleRect
+	Dropdowns    []*dropDown
+	Merges       []*rectMerge
+	Images       []*cellImage
+	ColumnsWidth []*rectWidths
+}

BIN
resource/img.jpg


+ 76 - 0
row.go

@@ -0,0 +1,76 @@
+package xvdoc
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+)
+
+type Row struct {
+	oy      int
+	x       int
+	y       int
+	offsetY bool
+	offsetX bool
+	cells   []Cell
+	style   string
+	styles  []string
+	height  float64
+}
+
+func (s *Row) UnmarshalJSON(src []byte) (err error) {
+	s.y = -1
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "ox":
+					err = dec.Decode(&s.oy)
+					s.offsetX = true
+				case "oy":
+					err = dec.Decode(&s.oy)
+					s.offsetY = true
+				case "x":
+					err = dec.Decode(&s.x)
+				case "y":
+					err = dec.Decode(&s.y)
+				case "cells":
+					s.cells, err = parseCells(dec)
+				case "height":
+					err = dec.Decode(&s.height)
+				case "style":
+					err = dec.Decode(&s.style)
+				case "styles":
+					err = dec.Decode(&s.styles)
+				default:
+					dec.Decode(new(interface{}))
+				}
+			}
+		}
+	}
+	return
+}
+
+func (s *Row) String() (res string) {
+	res = fmt.Sprintf("{\n  x: %v\n  y: %v\n  oy: %v\n  offsetY: %v\n  cells: %v\n}", s.x, s.y, s.oy, s.offsetY, s.cells)
+	return
+}
+
+func (s *Row) IsCustomHeight() bool {
+	return s.height > 0
+}
+
+func (s *Row) Height() float64 {
+	return s.height
+}
+
+func (s *Row) Styles() (res []string) {
+	if len(s.styles) > 0 {
+		res = s.styles
+	} else if len(s.style) > 0 {
+		res = []string{s.style}
+	}
+	return
+}

+ 59 - 0
style.go

@@ -0,0 +1,59 @@
+package xvdoc
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type Style struct {
+	JSONMap
+}
+
+func (s *Style) UnmarshalJSON(src []byte) (err error) {
+	//fmt.Println("!!!!!!!UMJ")
+	var m JSONMap
+	if err = json.Unmarshal(src, &m); err == nil {
+		s.JSONMap = m
+		//fmt.Println(s.JSONMap)
+	}
+	return
+}
+
+func (s *Style) mergeStyle(m map[string]Style, rStyle JSONMap, parentName string, level int) (res JSONMap, err error) {
+	if parent, check := m[parentName]; check {
+		var pm JSONMap
+		if pm, err = parent.Result(m, parentName, level+1); err == nil {
+			res = mergeMaps(pm.Map(), rStyle.Map())
+		}
+	} else {
+		res = rStyle.Copy()
+	}
+	return
+}
+
+func (s *Style) Result(m map[string]Style, childName string, cLevel ...int) (res JSONMap, err error) {
+	//fmt.Println(">>>>>>>>>>>>>>>>>>>>", s, childName, s.JSONMap)
+	level := 0
+	if len(cLevel) > 0 {
+		level = cLevel[0]
+	}
+	if level >= 100 {
+		return nil, fmt.Errorf("Style loop arrived... (%v)", childName)
+	}
+	res = s.Copy()
+	if s.KeyExists("parent") {
+		if res, err = s.mergeStyle(m, res, s.StringVal("parent", ""), level); err != nil {
+			return
+		}
+	}
+	if s.KeyExists("parents") {
+		for _, pName := range s.StringArray("parents") {
+			if res, err = s.mergeStyle(m, res, pName, level); err != nil {
+				return
+			}
+		}
+	}
+	delete(res, "parent")
+	delete(res, "parents")
+	return
+}

+ 130 - 0
style_group.go

@@ -0,0 +1,130 @@
+package xvdoc
+
+import (
+	"bytes"
+	"encoding/json"
+)
+
+type styleRect struct {
+	x, y   int
+	dx, dy int
+	styles []string
+}
+
+func (s *styleRect) UnmarshalJSON(src []byte) (err error) {
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "x":
+					err = dec.Decode(&s.x)
+				case "y":
+					err = dec.Decode(&s.y)
+				case "dx":
+					err = dec.Decode(&s.dx)
+				case "dy":
+					err = dec.Decode(&s.dy)
+				case "styles":
+					err = dec.Decode(&s.styles)
+				default:
+					err = dec.Decode(new(interface{}))
+				}
+			}
+		}
+	}
+	return
+}
+
+//////////////////////////////////////////////////////////////////
+
+type rectMerge struct {
+	x, y   int
+	dx, dy int
+}
+
+func (s *rectMerge) UnmarshalJSON(src []byte) (err error) {
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "x":
+					err = dec.Decode(&s.x)
+				case "y":
+					err = dec.Decode(&s.y)
+				case "dx":
+					err = dec.Decode(&s.dx)
+				case "dy":
+					err = dec.Decode(&s.dy)
+				default:
+					err = dec.Decode(new(interface{}))
+				}
+			}
+		}
+	}
+	return
+}
+
+///////////////////////////////////////////////////////////////
+
+type cellImage struct {
+	x, y   int
+	path   string
+	format JSONMap
+}
+
+func (s *cellImage) UnmarshalJSON(src []byte) (err error) {
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "x":
+					err = dec.Decode(&s.x)
+				case "y":
+					err = dec.Decode(&s.y)
+				case "path":
+					err = dec.Decode(&s.path)
+				case "format":
+					err = dec.Decode(&s.format)
+				default:
+					err = dec.Decode(new(interface{}))
+				}
+			}
+		}
+	}
+	return
+}
+
+//////////////////////////////////////////////////////////////
+
+type rectWidths struct {
+	x, dx int
+	width float64
+}
+
+func (s *rectWidths) UnmarshalJSON(src []byte) (err error) {
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "x":
+					err = dec.Decode(&s.x)
+				case "dx":
+					err = dec.Decode(&s.dx)
+				case "width":
+					err = dec.Decode(&s.width)
+				default:
+					err = dec.Decode(new(interface{}))
+				}
+			}
+		}
+	}
+	return
+}

BIN
tmp.xlsx


+ 143 - 0
xvdoc.go

@@ -0,0 +1,143 @@
+package xvdoc
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+)
+
+func FromJSON(src []byte) (res []*XVDoc, err error) {
+	var xv []*XVDoc
+	if err = json.Unmarshal(src, &xv); err == nil {
+		res = xv
+	}
+	return
+}
+
+type XVDoc struct {
+	name         string
+	style        string
+	rows         []*Row
+	styles       map[string]Style
+	dropdowns    []*dropDown
+	merges       []*rectMerge
+	columnsWidth []*rectWidths
+	styleRects   []*styleRect
+	images       []*cellImage
+}
+
+func (s *XVDoc) UnmarshalJSON(src []byte) (err error) {
+	dec := json.NewDecoder(bytes.NewReader(src))
+	var t json.Token
+	for dec.More() && err == nil {
+		if t, err = dec.Token(); err == nil {
+			if _, check := t.(json.Delim); !check {
+				switch t.(string) {
+				case "name":
+					err = dec.Decode(&s.name)
+				case "style":
+					err = dec.Decode(&s.style)
+				case "rows":
+					err = dec.Decode(&s.rows)
+				case "styles":
+					err = dec.Decode(&s.styles)
+				case "merges":
+					err = dec.Decode(&s.merges)
+				case "style_rects":
+					err = dec.Decode(&s.styleRects)
+				case "columns_width":
+					err = dec.Decode(&s.columnsWidth)
+				case "images":
+					err = dec.Decode(&s.images)
+				case "dropdowns":
+					err = dec.Decode(&s.dropdowns)
+				default:
+					err = dec.Decode(new(interface{}))
+				}
+			}
+		}
+	}
+	return
+}
+
+func (s *XVDoc) String() (res string) {
+	res = fmt.Sprintf("xvDoc %v\n*** rows ***\n", s.name)
+	res = fmt.Sprintf("styles: %v\n", s.styles)
+	for i, v := range s.rows {
+		res = res + fmt.Sprintf("%v: Cells: %v\n", i, v)
+	}
+	res = res + "*********************\n"
+	return res
+}
+
+func (s *XVDoc) matrix() Matrix {
+	var matrix [][]Cell
+	row, rowsHeight := 0, make(map[int]float64)
+
+	// create document matrix
+	for _, v := range s.rows {
+
+		// setup row
+		if v.offsetY {
+			row += v.oy
+		} else if v.y >= 0 {
+			row = v.y
+		}
+
+		// setup custom row height
+		if v.IsCustomHeight() {
+			rowsHeight[row] = v.Height()
+		}
+
+		// fill matrix rows
+		if row >= len(matrix) {
+			tmp := make([][]Cell, row-len(matrix)+1)
+			matrix = append(matrix, tmp...)
+		}
+
+		// get row in matrix
+		r := matrix[row]
+
+		if v.x+len(v.cells) >= len(r) {
+			tmp := make([]Cell, v.x+len(v.cells)-len(r))
+			r = append(r, tmp...)
+		}
+
+		for i := 0; i < len(v.cells); i++ {
+			r[v.x+i] = v.cells[i]
+		}
+
+		// setup row in matrix
+		matrix[row] = r
+
+		// to next row
+		row++
+	}
+
+	res := Matrix{
+		Name:         s.name,
+		Cells:        matrix,
+		Styles:       s.styles,
+		RowsHeight:   rowsHeight,
+		Style:        s.style,
+		StyleRects:   s.styleRects,
+		Merges:       s.merges,
+		Images:       s.images,
+		Dropdowns:    s.dropdowns,
+		ColumnsWidth: s.columnsWidth,
+	}
+
+	return res
+}
+
+func Export(format string, docs []*XVDoc) ([]byte, error) {
+	if method, check := exportMethods[format]; check {
+		m := make([]Matrix, len(docs))
+		for i, v := range docs {
+			m[i] = v.matrix()
+		}
+		return method(m)
+	} else {
+		return nil, fmt.Errorf("xvdoc export error :: unexpected format '%v'", format)
+	}
+}

+ 71 - 0
z_source (копия).json

@@ -0,0 +1,71 @@
+{
+    "name": "xv-test",
+    "style": "main",
+    "styles": {
+        "b-top": {  
+            "border": [{ "type": "top", "color": "#AAAAAA", "style": 2 }]
+        },
+        "b-left": {  
+            "border": [{ "type": "left", "color": "#AAAAAA", "style": 2 }]
+        },
+        "f-size": {
+            "font": { "size": 16, "color": "#FFFFFF" }
+        },
+        "f-bold": { "font": { "bold": true } },
+        "f-color": {
+            "parents": [ "f-size", "f-bold", "b-top", "b-left" ],
+            "font": { "color": "#AAAAAA" }
+        },
+        "fa": {
+			"font": { "color": "#aaaaaa" }
+        },
+        "border-box-lt": {
+			"border": [
+				{ "type": "top", "color": "#AAAAAA", "style": 2 },
+				{ "type": "left", "color": "#AAAAAA", "style": 2 }
+			]
+        },
+        "border-box-rt": {
+			"border": [
+				{ "type": "top", "color": "#AAAAAA", "style": 2 },
+				{ "type": "right", "color": "#AAAAAA", "style": 2 }
+			]
+        },
+        "border-box-l": {	"border": [	{ "type": "left", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-t": {	"border": [	{ "type": "top", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-r": {	"border": [	{ "type": "right", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-b": {	"border": [	{ "type": "bottom", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-lb": {
+					"border": [
+						{ "type": "bottom", "color": "#AAAAAA", "style": 2 },
+						{ "type": "left", "color": "#AAAAAA", "style": 2 }
+					]
+        }
+    },
+    "merges": [
+		{ "x": 2, "y": 2, "dx": 12, "dy": 12 }
+    ],
+    "columns_width": [
+        { "x": 0, "dx": 1, "width": 50 }
+    ],
+    "images": [
+			{ "x": 2, "y": 2, "path": "resource/img.jpg", "format": { "x_offset": 100, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize" } }
+    ],
+    "style_rects": [
+        { "x": 2, "y": 2, "dx": 2, "dy": 2, "styles": [ "border-box-lt" ] },
+        { "x": 2, "y": 11, "dx": 2, "dy": 11, "styles": [ "border-box-lb" ] },
+        { "x": 3, "y": 2, "dx": 5, "dy": 2, "styles": [ "border-box-t" ] },
+        { "x": 2, "y": 3, "dx": 2, "dy": 10, "styles": [ "border-box-l" ] },
+        { "x": 5, "y": 3, "dx": 5, "dy": 10, "styles": [ "border-box-r" ] }
+    ],
+    "rows": [
+        {
+			"height": 25,
+            "cells": [ 
+                { "value": 10, "styles": [ "f-color" ] },
+                { "value": 20 },
+                { "value": 30 }
+             ]
+        }
+    ]
+}

+ 69 - 0
z_source.json

@@ -0,0 +1,69 @@
+[{
+    "name": "xv-test",
+    "style": "main",
+    "styles": {
+        "b-top": {  
+            "border": [{ "type": "top", "color": "#AAAAAA", "style": 2 }]
+        },
+        "b-left": {  
+            "border": [{ "type": "left", "color": "#AAAAAA", "style": 2 }]
+        },
+        "f-size": {
+            "font": { "size": 16, "color": "#FFFFFF" }
+        },
+        "f-bold": { "font": { "bold": true } },
+        "f-color": {
+            "parents": [ "f-size", "f-bold", "b-top", "b-left" ],
+            "font": { "color": "#AAAAAA" }
+        },
+        "fa": {
+			"font": { "color": "#aaaaaa" }
+        },
+        "border-box-lt": {
+			"border": [
+				{ "type": "top", "color": "#AAAAAA", "style": 2 },
+				{ "type": "left", "color": "#AAAAAA", "style": 2 }
+			]
+        },
+        "border-box-rt": {
+			"border": [
+				{ "type": "top", "color": "#AAAAAA", "style": 2 },
+				{ "type": "right", "color": "#AAAAAA", "style": 2 }
+			]
+        },
+        "border-box-l": {	"border": [	{ "type": "left", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-t": {	"border": [	{ "type": "top", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-r": {	"border": [	{ "type": "right", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-b": {	"border": [	{ "type": "bottom", "color": "#AAAAAA", "style": 2 } ] },
+        "border-box-lb": {
+					"border": [
+						{ "type": "bottom", "color": "#AAAAAA", "style": 2 },
+						{ "type": "left", "color": "#AAAAAA", "style": 2 }
+					]
+        }
+    },
+    "merges": [
+		{ "x": 2, "y": 2, "dx": 12, "dy": 12 }
+    ],
+    "columns_width": [
+        { "x": 0, "dx": 1, "width": 50 }
+    ],
+    "images": [
+			{ "x": 2, "y": 2, "path": "resource/img.jpg", "format": { "x_offset": 100, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize" } }
+    ],
+    "style_rects": [
+        { "x": 2, "y": 2, "dx": 2, "dy": 2, "styles": [ "border-box-lt" ] },
+        { "x": 2, "y": 11, "dx": 2, "dy": 11, "styles": [ "border-box-lb" ] },
+        { "x": 3, "y": 2, "dx": 5, "dy": 2, "styles": [ "border-box-t" ] },
+        { "x": 2, "y": 3, "dx": 2, "dy": 10, "styles": [ "border-box-l" ] },
+        { "x": 5, "y": 3, "dx": 5, "dy": 10, "styles": [ "border-box-r" ] }
+    ],
+    "dropdowns": [
+        { "x": 0, "y": 5, "dy": 10, "values": [ "Val 1", "Val 2", "Val 3" ] }
+    ],
+    "rows": [
+        { "cells": [ 10 ] },
+        { "cells": [ 20 ] },
+        { "cells": [ 30 ] }
+    ]
+}]

+ 37 - 0
z_test.go

@@ -0,0 +1,37 @@
+package xvdoc
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"testing"
+)
+
+func TestXVDoc(t *testing.T) {
+	if src, err := ioutil.ReadFile("z_source.json"); err == nil {
+		if docs, err := FromJSON(src); err == nil {
+			log.Println("!!!!!!!", len(docs), docs)
+			log.Println("=============================")
+			if exp, err := Export("xlsx", docs); err == nil {
+				ioutil.WriteFile("tmp.xlsx", exp, 0660)
+			} else {
+				log.Println(err)
+			}
+
+		} else {
+			t.Error(err)
+		}
+	} else {
+		t.Error(err)
+	}
+}
+
+func TestXLSXImport(t *testing.T) {
+	sheets, err := ImportXLSXFromFile("tmp.xlsx")
+	fmt.Println(sheets, "|", err)
+}
+
+func TestXLSXImportToJSON(t *testing.T) {
+	src, err := ImportXLSXToJSONFromFile("tmp.xlsx")
+	fmt.Println(string(src), "|", err)
+}