26 Commits

Author SHA1 Message Date
c578081f64 update golang
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-04-29 17:05:59 +10:00
68096c75ae fix build script
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-01 17:10:03 +10:00
45d90bf252 update to latest version of excelize
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-01 17:07:55 +10:00
d5a803644b do formatting to heading row of empty worksheet
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-06-28 09:19:25 +10:00
bfa2afbb6c add CICD
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-06-28 09:02:31 +10:00
24560ee8bf improve handling of empty data 2024-06-28 08:59:22 +10:00
2a89008a54 generate empty workbook if no data to add 2023-06-15 10:41:33 +10:00
c91d38f96c fix json keys 2023-05-31 11:51:04 +10:00
482e5deb6e updated logging 2023-05-31 10:38:55 +10:00
590d3e3407 allow specifying parent-node for data 2023-05-30 16:25:45 +10:00
d872cb8517 more docs 2023-05-30 13:39:48 +10:00
4d021813f6 fix doc 2023-05-30 13:36:02 +10:00
55f3196c4b update doc 2023-05-30 13:35:35 +10:00
7e3e2a2185 Removed git ignored files 2023-05-30 13:32:24 +10:00
4bffb9cbfc do some error checking before asserting orderedmap 2023-04-18 09:20:36 +10:00
c500216105 add log message 2023-04-17 13:17:45 +10:00
e0c13681a8 remove unnecessary mac files 2023-02-13 13:13:22 +11:00
b1b3b8834b bugfix bold top row 2023-02-13 13:12:12 +11:00
13201c144a fix autofilter range 2023-02-13 13:09:49 +11:00
cee94f2141 code refactor 2023-02-13 13:08:10 +11:00
bc71a24a83 set document properties 2023-02-13 12:47:06 +11:00
061bb83861 add some docs 2023-02-13 12:40:56 +11:00
78ba74c709 intial docs 2023-02-13 08:42:39 +11:00
f2446daff1 add autowidth 2023-02-11 17:15:31 +11:00
6ff19c3c81 remove binary 2023-02-10 15:32:24 +11:00
e4f832ebf2 update 2023-02-10 15:30:46 +11:00
11 changed files with 449 additions and 141 deletions

BIN
.DS_Store vendored

Binary file not shown.

15
.drone.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# disable CGO for cross-compiling
export CGO_ENABLED=0
export now=$(TZ=Australia/Sydney date '+%Y%m%d-%H%M%S')
echo $now
#pwd
#ls -lah ~
#go env
echo "build commences"
go build -ldflags "-X main.sha1ver=`git rev-parse HEAD` -X main.buildTime=$now" -o json2excel ./cmd/main/main.go
echo "build complete"
sha256sum json2excel > json2excel_checksum.txt
ls -lah

30
.drone.yml Normal file
View File

@@ -0,0 +1,30 @@
kind: pipeline
type: docker
name: default
# Docs at https://docs.drone.io/pipeline/exec/overview/
# Also see https://github.com/harness/drone-cli/blob/master/.drone.yml
steps:
- name: build
image: golang
environment:
CGO_ENABLED: 0
commands:
- sh ./.drone.sh
- name: dell-sftp-deploy
image: hypervtechnics/drone-sftp
settings:
host: deft.dell.com
username:
from_secret: DELLFTP_USER
password:
from_secret: DELLFTP_PASS
port: 22
source: ./
filter: json2excel*
clean: false
target: /
overwrite: true
verbose: true

6
.gitignore vendored
View File

@@ -5,4 +5,8 @@
json2excel
# Ignore test data
*.json
*.json
# Ignore Mac DS_Store files
.DS_Store
**/.DS_Store

View File

@@ -0,0 +1,27 @@
# json2excel
This is a basic utility to take json and convert it into an OpenXml format xlsx file, compatible with Excel.
It expects that the json input is formatted as an object containing an array of objects. Column names are generated based on the keys in the first nested object.
## Command line options
| Parameter | Default Value | Description |
|---------------|---------------|---------------------------|
| inputJson | input.json | File path of the input json to process |
| outputFilename | output.xlsx | File path of the output file |
| worksheetName | Sheet1 | Set the name of the worksheet, will be truncated to 31 characters |
| boldTopRow | true | Sets the top row of the worksheet to bold |
| freezeTopRow | true | Freezes the first row of the Excel worksheet |
| autofilter | true | Sets the auto filter on the first row |
| autowidth | true | Automatically set the column width to fit contents |
| overwriteFile | false | Overwrite any existing output file instead of modifying in-place |
## Advanced configuration
Advanced settings can be provided via a top level json key named "config". Here is a table of options that can be set via this config key.
| Key | Example Value | Description |
|---------------|---------------|---------------------------|
| keyOrder | "Column3,Column1,Column2"| Comma separated list of column names in the desired order |
| overwriteFile | true | Boolean indicating whether output file should be overwritten if it already exists |
| parentNode | "results" | Specify an alternate starting key for the spreadsheet data than just the first non-config key. Useful with json structures with multiple top-level keys |

BIN
cmd/.DS_Store vendored

Binary file not shown.

View File

@@ -6,6 +6,10 @@ import (
"fmt"
"log"
"os"
"reflect"
"strings"
"time"
"unicode/utf8"
"github.com/iancoleman/orderedmap"
"github.com/xuri/excelize/v2"
@@ -13,9 +17,17 @@ import (
// Initial concept from https://stackoverflow.com/q/68621039
type Config struct {
keyOrder map[int]string
parentOverride string
}
var config Config
func main() {
//jsonFile := "test.json"
parentNode := "input"
configNode := "config"
//worksheetName := "Sheet2"
//outputFilename := "test.xlsx"
@@ -26,6 +38,8 @@ func main() {
var boldTopRow bool
var freezeTopRow bool
var autoFilter bool
var autoWidth bool
var overwriteFile bool
// Process command line arguments
flag.StringVar(&inputJson, "inputJson", "./input.json", "Full path to input json data file")
@@ -34,62 +48,21 @@ func main() {
flag.BoolVar(&boldTopRow, "bold-toprow", true, "Sets the top row of the worksheet to bold")
flag.BoolVar(&freezeTopRow, "freeze-toprow", true, "Freezes the first row of the Excel worksheet")
flag.BoolVar(&autoFilter, "autofilter", true, "Sets the auto filter on the first row")
flag.BoolVar(&autoWidth, "autowidth", true, "Automatically set the column width to fit contents")
flag.BoolVar(&overwriteFile, "overwrite", false, "Set to true to overwrite existing file rather than modifying in place")
flag.Parse()
var xlsx *excelize.File
var s []byte
var sheetIndex int
//var sheetIndex int
var err error
var cell string
var row, column int
// TODO - truncate worksheetName to the maximum 31 characters
// Check if xlsx file exists already, and if it does then open and append data
if fileExists(outputFilename) {
fmt.Printf("Output spreadsheet '%s' already exists.\n", outputFilename)
xlsx, err = excelize.OpenFile(outputFilename)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Since we have an existing workbook, check if the sheet we want to write to already exists
sheetFound := false
for index, name := range xlsx.GetSheetMap() {
if name == worksheetName {
fmt.Printf("Found worksheet '%s' at index '%d'\n", worksheetName, index)
sheetFound = true
}
}
if !sheetFound {
// Create the sheet
fmt.Printf("Creating worksheet '%s'\n", worksheetName)
sheetIndex, err = xlsx.NewSheet(worksheetName)
if err != nil {
fmt.Printf("Error creating worksheet '%s' : %s\n", worksheetName, err)
}
// Set active worksheet
fmt.Printf("Setting active sheet to index %d", sheetIndex)
xlsx.SetActiveSheet(sheetIndex)
}
} else {
fmt.Printf("Creating output spreadsheet '%s'\n", outputFilename)
// Create the file
xlsx = excelize.NewFile()
// Rename the default Sheet1 to this worksheet name
if worksheetName != "Sheet1" {
fmt.Printf("Renaming default worksheet to '%s'\n", worksheetName)
err = xlsx.SetSheetName("Sheet1", worksheetName)
if err != nil {
fmt.Printf("Error setting sheet name to '%s': %s\n", worksheetName, err)
}
}
}
// Truncate worksheetName to the maximum 31 characters
worksheetName = TruncateString(worksheetName, 31)
// Read the json input file
if fileExists(inputJson) {
@@ -106,54 +79,178 @@ func main() {
o := orderedmap.New()
err = json.Unmarshal([]byte(s), &o)
if err != nil {
fmt.Printf("JSON Unmarshal error %s\n", err)
error := fmt.Sprintf("JSON Unmarshal error %s\n", err)
panic(error)
}
// Assume that our content is within the first top-level key
topLevel := o.Keys()
fmt.Printf("Detected toplevel json key as: '%s'\n", topLevel[0])
parentNode = topLevel[0]
fmt.Printf("Found %d top-level keys in json data\n", len(topLevel))
for i, key := range topLevel {
fmt.Printf("[%d] : %s\n", i, key)
}
// Check for config embedded in json
if strings.EqualFold(topLevel[0], configNode) && len(topLevel) > 1 {
fmt.Printf("Found configNode as toplevel json key, setting parentNode as '%s'\n", topLevel[1])
parentNode = topLevel[1]
config.keyOrder = make(map[int]string)
// Get a reference to the top level node we specified earlier
configInterface, ok := o.Get(configNode)
if !ok {
fmt.Printf("Missing key for multitype array when reading embedded config")
}
// Get an interface that we can work with to access the sub elements
// This doesn't seem necessary for some reason - maybe because there's only one level of depth to the configNode
//configSlice := configInterface
//fmt.Printf("%v\n", configSlice)
// Get the keys for the first element so we know what config options have been specified
configMap := configInterface.(orderedmap.OrderedMap)
configKeys := configMap.Keys()
// Parse each key into our config struct
for _, key := range configKeys {
if strings.EqualFold(key, "keyOrder") {
fmt.Printf("Found config element for keyOrder\n")
e, _ := configMap.Get(key)
for i, e := range strings.Split(e.(string), ",") {
config.keyOrder[i] = e
}
fmt.Printf("Column order is now : '%v'\n", config.keyOrder)
} else if strings.EqualFold(key, "overwriteFile") {
fmt.Printf("Found config element for overwriting output file\n")
e, _ := configMap.Get(key)
overwriteFile = e.(bool)
} else if strings.EqualFold(key, "parentNode") {
fmt.Printf("Found config element for forcing parent key for spreadsheet data\n")
e, _ := configMap.Get(key)
config.parentOverride = e.(string)
}
}
} else if strings.EqualFold(topLevel[0], configNode) {
error := "Only found config in first level of json keys"
panic(error)
} else {
fmt.Printf("Detected toplevel json key as: '%s'\n", topLevel[0])
parentNode = topLevel[0]
}
if config.parentOverride != "" {
fmt.Printf("Overriding parent node to '%s'\n", config.parentOverride)
parentNode = config.parentOverride
}
// Get a reference to the top level node we specified earlier
vislice, ok := o.Get(parentNode)
if !ok {
fmt.Printf("Missing key for multitype array")
}
// Get an interface that we can work with to access the sub elements
vslice := vislice.([]interface{})
if len(vslice) < 1 {
// There was no data but lets just log that and close off the empty workbook
fmt.Printf("No data found contained in top-level json key '%s', no work to do.\n", parentNode)
// Create new file
xlsx = createWorkbook(worksheetName, outputFilename)
// Run code to add column names to the first row of the workbook based off the keyOrder, if defined
if len(config.keyOrder) > 0 {
var columnHeadings []string
for _, value := range config.keyOrder {
columnHeadings = append(columnHeadings, value)
}
createHeadingRow(xlsx, worksheetName, columnHeadings)
headerStyle, err := xlsx.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
},
})
if err != nil {
fmt.Printf("Error generating header style : '%s'\n", err)
} else {
// Set the style if there was no error generating it
if boldTopRow {
err = xlsx.SetRowStyle(worksheetName, 1, 1, headerStyle)
if err != nil {
fmt.Printf("Error setting header style : '%s'\n", err)
}
}
}
if autoWidth {
err = SetColAutoWidth(xlsx, worksheetName)
if err != nil {
fmt.Printf("Error setting auto width : '%s'\n", err)
}
}
}
// Close off the file
if err := xlsx.SaveAs(outputFilename); err != nil {
log.Fatal(err)
}
return
}
// Check that the first element is what we expected
if _, ok := vslice[0].(orderedmap.OrderedMap); !ok {
error := fmt.Sprintf("Type of first vslice element is not an ordered map. It appears to be '%v'\n", reflect.TypeOf(vslice[0]))
panic(error)
}
// Get the keys for the first element so we know what the column names will be
columnMap := vslice[0].(orderedmap.OrderedMap)
//fmt.Printf("First vslice element is an ordered map\n")
columnNames := columnMap.Keys()
fmt.Printf("Creating excel workbook with following headings : '%v'\n", columnNames)
// Check if xlsx file exists already, and if it does then open and append data
if fileExists(outputFilename) {
if overwriteFile {
// Delete existing file
fmt.Printf("Overwrite flag is set, removing existing file '%s'\n", outputFilename)
err = os.Remove(outputFilename)
if err != nil {
panic(err)
}
// Create new file
xlsx = createWorkbook(worksheetName, outputFilename)
} else {
xlsx = modifyWorkbook(worksheetName, outputFilename)
}
} else {
xlsx = createWorkbook(worksheetName, outputFilename)
}
// Run code to add column names to the first row of the workbook
createHeadingRow(xlsx, worksheetName, columnNames)
// Set the style for the header values
// Just handling bold for now but we can do other styles too as per https://xuri.me/excelize/en/style.html#NewStyle
fmt.Printf("Bolding top row : %v\n", boldTopRow)
headerStyle, err2 := xlsx.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: boldTopRow,
Bold: true,
},
})
if err2 != nil {
fmt.Printf("Error generating header style : '%s'\n", err2)
}
row = 1
column = 1
// Set the style
err = xlsx.SetRowStyle(worksheetName, row, row, headerStyle)
if err != nil {
fmt.Printf("Error setting header style : '%s'\n", err)
}
// Add the header row
for i := 0; i < len(columnNames); i++ {
cell, _ = excelize.CoordinatesToCellName(column, row)
fmt.Printf("Setting cell %s to value %s\n", cell, columnNames[i])
xlsx.SetCellValue(worksheetName, cell, columnNames[i])
//xlsx.SetCellStyle(worksheetName, cell, cell, headerStyle)
column++
} else {
// Set the style if there was no error generating it
if boldTopRow {
err = xlsx.SetRowStyle(worksheetName, 1, 1, headerStyle)
if err != nil {
fmt.Printf("Error setting header style : '%s'\n", err)
}
}
}
// Freeze top row if requested, see https://xuri.me/excelize/en/utils.html#SetPanes
@@ -165,7 +262,7 @@ func main() {
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
Panes: []excelize.PaneOptions{
Selection: []excelize.Selection{
{SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"},
},
})
@@ -176,9 +273,10 @@ func main() {
// Handle autofilter
if autoFilter {
// cell is still a reference to the last cell in the header row
cell, _ = excelize.CoordinatesToCellName(len(columnNames), 1)
filterRange := "A1:" + cell
fmt.Printf("Setting autofilter to range '%s'\n", filterRange)
// As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks)
err = xlsx.AutoFilter(worksheetName, filterRange, nil)
if err != nil {
fmt.Printf("Error setting autofilter : '%s'\n", err)
@@ -201,29 +299,51 @@ func main() {
// Each iteration should start back at column 1
column = 1
// Print each key-value pair contained in this slice
// Get the key-value pairs contained in this slice
vmap := v.(orderedmap.OrderedMap)
k := vmap.Keys()
for j := range k {
//a = string(asciiValue)
//cell = a + strconv.Itoa(2+i)
cell, _ = excelize.CoordinatesToCellName(column, row)
if len(config.keyOrder) > 0 {
// If we have a specified order for the values then use that
e, _ := vmap.Get(k[j])
//fmt.Printf("Setting cell %s to value %v\n", cell, e)
xlsx.SetCellValue(worksheetName, cell, e)
for j := 0; j < len(config.keyOrder); j++ {
cell, _ = excelize.CoordinatesToCellName(column, row)
// Move to the next column
//asciiValue++
column++
e, _ := vmap.Get(config.keyOrder[j])
//fmt.Printf("Setting cell %s to value %v\n", cell, e)
xlsx.SetCellValue(worksheetName, cell, e)
// Move to the next column
column++
}
} else {
// Otherwise use the order the json was in
for j := range k {
cell, _ = excelize.CoordinatesToCellName(column, row)
e, _ := vmap.Get(k[j])
//fmt.Printf("Setting cell %s to value %v\n", cell, e)
xlsx.SetCellValue(worksheetName, cell, e)
// Move to the next column
column++
}
}
//fmt.Printf("k: %v\n", k)
// Move to next row
row++
}
// Perform any post processing now that the data exists
if autoWidth {
err = SetColAutoWidth(xlsx, worksheetName)
if err != nil {
fmt.Printf("Error setting auto width : '%s'\n", err)
}
}
// Close off the file
if err := xlsx.SaveAs(outputFilename); err != nil {
log.Fatal(err)
@@ -259,6 +379,135 @@ func fileExists(filename string) bool {
return !info.IsDir()
}
// From https://dev.to/takakd/go-safe-truncate-string-9h0#comment-1hp14
func TruncateString(str string, length int) string {
if length <= 0 {
return ""
}
if utf8.RuneCountInString(str) < length {
return str
}
return string([]rune(str)[:length])
}
func createWorkbook(worksheetName string, outputFilename string) *excelize.File {
fmt.Printf("Creating output spreadsheet '%s'\n", outputFilename)
var err error
// Create the file
xlsx := excelize.NewFile()
// Rename the default Sheet1 to this worksheet name
if worksheetName != "Sheet1" {
fmt.Printf("Renaming default worksheet to '%s'\n", worksheetName)
err = xlsx.SetSheetName("Sheet1", worksheetName)
if err != nil {
fmt.Printf("Error setting sheet name to '%s': %s\n", worksheetName, err)
}
}
// Set the document properties
err = xlsx.SetDocProps(&excelize.DocProperties{
Creator: "json2excel",
Created: time.Now().Format(time.RFC3339),
})
if err != nil {
fmt.Printf("Error setting document properties: %s\n", err)
}
return xlsx
}
func modifyWorkbook(worksheetName string, outputFilename string) *excelize.File {
fmt.Printf("Output spreadsheet '%s' already exists.\n", outputFilename)
xlsx, err := excelize.OpenFile(outputFilename)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Since we have an existing workbook, check if the sheet we want to write to already exists
sheetFound := false
for index, name := range xlsx.GetSheetMap() {
if name == worksheetName {
fmt.Printf("Found worksheet '%s' at index '%d'\n", worksheetName, index)
sheetFound = true
}
}
if !sheetFound {
// Create the sheet
fmt.Printf("Creating worksheet '%s'\n", worksheetName)
sheetIndex, err := xlsx.NewSheet(worksheetName)
if err != nil {
fmt.Printf("Error creating worksheet '%s' : %s\n", worksheetName, err)
}
// Set active worksheet
fmt.Printf("Setting active sheet to index %d", sheetIndex)
xlsx.SetActiveSheet(sheetIndex)
}
return xlsx
}
func createHeadingRow(xlsx *excelize.File, worksheetName string, columnNames []string) {
var cell string
row := 1
column := 1
if len(config.keyOrder) > 0 {
fmt.Printf("Creating excel workbook with heading order as per config key-order\n")
// Check that the number of specified columns matches input data
if len(config.keyOrder) != len(columnNames) {
error := fmt.Sprintf("Column order specified in json key-order but mismatch found in json data. %d specifed columns does not match %d found columns.", len(config.keyOrder), len(columnNames))
panic(error)
}
// Iterate the map and add the columns as per that order
for i := 0; i < len(config.keyOrder); i++ {
cell, _ = excelize.CoordinatesToCellName(column, row)
fmt.Printf("Setting cell %s to value %s at index %d\n", cell, config.keyOrder[i], i)
xlsx.SetCellValue(worksheetName, cell, config.keyOrder[i])
column++
}
} else {
fmt.Printf("Creating excel workbook with following headings : '%v'\n", columnNames)
// Add the header row
for i := 0; i < len(columnNames); i++ {
cell, _ = excelize.CoordinatesToCellName(column, row)
fmt.Printf("Setting cell %s to value %s\n", cell, columnNames[i])
xlsx.SetCellValue(worksheetName, cell, columnNames[i])
//xlsx.SetCellStyle(worksheetName, cell, cell, headerStyle)
column++
}
}
}
// Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446
func SetColAutoWidth(xlsx *excelize.File, sheetName string) error {
// Autofit all columns according to their text content
cols, err := xlsx.GetCols(sheetName)
if err != nil {
return err
}
for idx, col := range cols {
largestWidth := 0
for _, rowCell := range col {
cellWidth := utf8.RuneCountInString(rowCell) + 2 // + 2 for margin
if cellWidth > largestWidth {
largestWidth = cellWidth
}
}
fmt.Printf("SetColAutoWidth calculated largest width for column index '%d' is '%d'\n", idx, largestWidth)
name, err := excelize.ColumnNumberToName(idx + 1)
if err != nil {
return err
}
xlsx.SetColWidth(sheetName, name, name, float64(largestWidth))
}
// No errors at this point
return nil
}
func TestUnmarshalJSON() {
s := `{
"number": 4,

18
go.mod
View File

@@ -1,21 +1,21 @@
module json2excel
go 1.19
go 1.24.2
require (
github.com/iancoleman/orderedmap v0.2.0
github.com/xuri/excelize/v2 v2.7.0
github.com/iancoleman/orderedmap v0.3.0
github.com/xuri/excelize/v2 v2.9.0
)
require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/text v0.6.0 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 // indirect
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/text v0.24.0 // indirect
)
//require internal/ordered v1.0.0

81
go.sum
View File

@@ -1,8 +1,7 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -12,51 +11,35 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.7.0 h1:Hri/czwyRCW6f6zrCDWXcXKshlq4xAZNpNOpdfnFhEw=
github.com/xuri/excelize/v2 v2.7.0/go.mod h1:ebKlRoS+rGyLMyUx3ErBECXs/HNYqyj+PbkkKRK5vSI=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA=
github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U=
github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
internal/.DS_Store vendored

Binary file not shown.

Binary file not shown.