mirror of
https://github.com/owncloud/ocis
synced 2026-04-25 17:25:21 +02:00
chore(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.9 (#11570)
* chore(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.9 Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.5 to 1.0.9. - [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.5...v1.0.9) --- updated-dependencies: - dependency-name: github.com/olekukonko/tablewriter dependency-version: 1.0.9 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * fix: adjust to tablewriter 1.0.9 (AI-powered) Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> * update to NewTable * bring back a Footer * fix the tests --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Julian Koberg <julian.koberg@kiteworks.com> Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
This commit is contained in:
4
go.mod
4
go.mod
@@ -59,7 +59,7 @@ require (
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/nats-io/nats-server/v2 v2.11.6
|
||||
github.com/nats-io/nats.go v1.43.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/olekukonko/tablewriter v1.0.9
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/ginkgo/v2 v2.23.4
|
||||
github.com/onsi/gomega v1.37.0
|
||||
@@ -266,6 +266,8 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pablodz/inotifywaitgo v0.0.7 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -845,8 +845,13 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
"github.com/owncloud/ocis/v2/ocis/pkg/register"
|
||||
@@ -403,11 +403,8 @@ func benchmark(iterations int, path string) error {
|
||||
fmt.Printf("Iterations: %d\n", iterations)
|
||||
fmt.Println("")
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Test", "Iterations", "dur/it", "total"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table.SetColumnAlignment([]int{tw.ALIGN_LEFT, tw.ALIGN_RIGHT, tw.ALIGN_RIGHT, tw.ALIGN_RIGHT})
|
||||
table.SetAutoMergeCellsByColumnIndex([]int{2, 3})
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Test", "Iterations", "dur/it", "total")
|
||||
for _, t := range []string{"lockedfile open(wo,c,t) close", "stat", "fopen(wo,t) write close", "fopen(ro) close", "fopen(ro) read close", "xattr-set", "xattr-get"} {
|
||||
start := time.Now()
|
||||
err := tests[t]()
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/reva/v2/pkg/publicshare"
|
||||
publicregistry "github.com/owncloud/reva/v2/pkg/publicshare/manager/registry"
|
||||
"github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool"
|
||||
@@ -284,9 +284,8 @@ func ListDecomposedfsMigrations(cfg *config.Config) *cli.Command {
|
||||
}
|
||||
sort.Strings(migrations)
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Migration", "State", "Message"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Migration", "State", "Message")
|
||||
for _, migration := range migrations {
|
||||
table.Append([]string{migration, migrationStates[migration].State, migrationStates[migration].Message})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/urfave/cli/v2"
|
||||
mreg "go-micro.dev/v4/registry"
|
||||
|
||||
@@ -62,9 +62,8 @@ func VersionCommand(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -489,8 +489,8 @@ func (s *Service) generateRunSet(cfg *ociscfg.Config) map[string]struct{} {
|
||||
// List running processes for the Service Controller.
|
||||
func (s *Service) List(_ struct{}, reply *string) error {
|
||||
tableString := &strings.Builder{}
|
||||
table := tablewriter.NewWriter(tableString)
|
||||
table.SetHeader([]string{"Service"})
|
||||
table := tablewriter.NewTable(tableString)
|
||||
table.Header("Service")
|
||||
|
||||
s.mu.Lock()
|
||||
names := []string{}
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/app-provider/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/app-registry/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/auth-app/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/auth-basic/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/auth-bearer/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/auth-machine/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/auth-service/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/frontend/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/gateway/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -37,12 +37,10 @@ func listUnifiedRoles(cfg *config.Config) *cli.Command {
|
||||
Name: "list",
|
||||
Usage: "list available unified roles",
|
||||
Action: func(c *cli.Context) error {
|
||||
tbl := tablewriter.NewWriter(os.Stdout)
|
||||
tbl.SetRowLine(true)
|
||||
tbl.SetAutoMergeCellsByColumnIndex([]int{0}) // rowspan should only affect the first column
|
||||
tbl := tablewriter.NewTable(os.Stdout)
|
||||
tbl.Header("Label", "UID", "Enabled", "Description", "Condition", "Allowed resource actions")
|
||||
|
||||
headers := []string{"Label", "UID", "Enabled", "Description", "Condition", "Allowed resource actions"}
|
||||
tbl.SetHeader(headers)
|
||||
|
||||
for _, definition := range unifiedrole.GetRoles(unifiedrole.RoleFilterAll()) {
|
||||
const enabled = "enabled"
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/groups/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/idp/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/invitations/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/ocdav/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/ocs/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/policies/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/settings/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/sharing/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/storage-publiclink/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/storage-shares/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/storage-system/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mohae/deepcopy"
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
|
||||
zlog "github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
|
||||
@@ -469,11 +469,10 @@ func itemType(it provider.ResourceType) string {
|
||||
return itemType
|
||||
}
|
||||
|
||||
func itemsTable(total int) *tw.Table {
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"itemID", "path", "type", "delete at"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table.SetFooter([]string{"", "", "", "total count: " + strconv.Itoa(total)})
|
||||
func itemsTable(total int) *tablewriter.Table {
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("itemID", "path", "type", "delete at")
|
||||
table.Footer("", "", "", "total count: "+strconv.Itoa(total))
|
||||
return table
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/shamaton/msgpack/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
@@ -146,16 +146,15 @@ func ListUploadSessions(cfg *config.Config) *cli.Command {
|
||||
}
|
||||
|
||||
var (
|
||||
table *tw.Table
|
||||
table *tablewriter.Table
|
||||
raw []Session
|
||||
)
|
||||
|
||||
if !c.Bool("json") {
|
||||
fmt.Println(buildInfo(filter))
|
||||
|
||||
table = tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Space", "Upload Id", "Name", "Offset", "Size", "Executant", "Owner", "Expires", "Processing", "Scan Date", "Scan Result"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table = tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Space", "Upload Id", "Name", "Offset", "Size", "Executant", "Owner", "Expires", "Processing", "Scan Date", "Scan Result")
|
||||
}
|
||||
|
||||
for _, u := range uploads {
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/thumbnails/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/users/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/webdav/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +35,8 @@ func Version(cfg *config.Config) *cli.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tw.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Version", "Address", "Id"})
|
||||
table.SetAutoFormatHeaders(false)
|
||||
table := tablewriter.NewTable(os.Stdout)
|
||||
table.Header("Version", "Address", "Id")
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -727,7 +727,7 @@ class CliContext implements Context {
|
||||
$totalCount = 0;
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match(
|
||||
'/^\s*\|\s*([a-f0-9\-]{36})\s*\|\s*(.*?)\s*\|\s*(file|folder)\s*\|\s*([\d\-T:Z]+)\s*\|/',
|
||||
'/^\s*\│\s*([a-f0-9\-]{36})\s*\│\s*(.*?)\s*\│\s*(file|folder)\s*\│\s*([\d\-T:Z]+)\s*\│/',
|
||||
$line,
|
||||
$matches,
|
||||
)
|
||||
|
||||
3
vendor/github.com/olekukonko/errors/.gitignore
generated
vendored
Normal file
3
vendor/github.com/olekukonko/errors/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
.idea/
|
||||
tmp/
|
||||
21
vendor/github.com/olekukonko/errors/LICENSE
generated
vendored
Normal file
21
vendor/github.com/olekukonko/errors/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Oleku Konko
|
||||
|
||||
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.
|
||||
1565
vendor/github.com/olekukonko/errors/README.md
generated
vendored
Normal file
1565
vendor/github.com/olekukonko/errors/README.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
610
vendor/github.com/olekukonko/errors/chain.go
generated
vendored
Normal file
610
vendor/github.com/olekukonko/errors/chain.go
generated
vendored
Normal file
@@ -0,0 +1,610 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog" // Standard structured logging package
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Chain executes functions sequentially with enhanced error handling.
|
||||
// Logging is optional and configured via a slog.Handler.
|
||||
type Chain struct {
|
||||
steps []chainStep // List of steps to execute
|
||||
errors []error // Accumulated errors during execution
|
||||
config chainConfig // Chain-wide configuration
|
||||
lastStep *chainStep // Pointer to the last added step for configuration
|
||||
logHandler slog.Handler // Optional logging handler (nil means no logging)
|
||||
cancel context.CancelFunc // Function to cancel the context
|
||||
}
|
||||
|
||||
// chainStep represents a single step in the chain.
|
||||
type chainStep struct {
|
||||
execute func() error // Function to execute for this step
|
||||
optional bool // If true, errors don't stop the chain
|
||||
config stepConfig // Step-specific configuration
|
||||
}
|
||||
|
||||
// chainConfig holds chain-wide settings.
|
||||
type chainConfig struct {
|
||||
timeout time.Duration // Maximum duration for the entire chain
|
||||
maxErrors int // Maximum number of errors before stopping (-1 for unlimited)
|
||||
autoWrap bool // Whether to automatically wrap errors with additional context
|
||||
}
|
||||
|
||||
// stepConfig holds configuration for an individual step.
|
||||
type stepConfig struct {
|
||||
context map[string]interface{} // Arbitrary key-value pairs for context
|
||||
category ErrorCategory // Category for error classification
|
||||
code int // Numeric error code
|
||||
retry *Retry // Retry policy for the step
|
||||
logOnFail bool // Whether to log errors automatically
|
||||
metricsLabel string // Label for metrics (not used in this code)
|
||||
logAttrs []slog.Attr // Additional attributes for logging
|
||||
}
|
||||
|
||||
// ChainOption defines a function that configures a Chain.
|
||||
type ChainOption func(*Chain)
|
||||
|
||||
// NewChain creates a new Chain with the given options.
|
||||
// Logging is disabled by default (logHandler is nil).
|
||||
func NewChain(opts ...ChainOption) *Chain {
|
||||
c := &Chain{
|
||||
config: chainConfig{
|
||||
autoWrap: true, // Enable error wrapping by default
|
||||
maxErrors: -1, // No limit on errors by default
|
||||
},
|
||||
// logHandler is nil, meaning no logging unless explicitly configured
|
||||
}
|
||||
// Apply each configuration option
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ChainWithLogHandler sets a custom slog.Handler for logging.
|
||||
// If handler is nil, logging is effectively disabled.
|
||||
func ChainWithLogHandler(handler slog.Handler) ChainOption {
|
||||
return func(c *Chain) {
|
||||
c.logHandler = handler
|
||||
}
|
||||
}
|
||||
|
||||
// ChainWithTimeout sets a timeout for the entire chain.
|
||||
func ChainWithTimeout(d time.Duration) ChainOption {
|
||||
return func(c *Chain) {
|
||||
c.config.timeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// ChainWithMaxErrors sets the maximum number of errors allowed.
|
||||
// A value <= 0 means no limit.
|
||||
func ChainWithMaxErrors(max int) ChainOption {
|
||||
return func(c *Chain) {
|
||||
if max <= 0 {
|
||||
c.config.maxErrors = -1 // No limit
|
||||
} else {
|
||||
c.config.maxErrors = max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ChainWithAutoWrap enables or disables automatic error wrapping.
|
||||
func ChainWithAutoWrap(auto bool) ChainOption {
|
||||
return func(c *Chain) {
|
||||
c.config.autoWrap = auto
|
||||
}
|
||||
}
|
||||
|
||||
// Step adds a new step to the chain with the provided function.
|
||||
// The function must return an error or nil.
|
||||
func (c *Chain) Step(fn func() error) *Chain {
|
||||
if fn == nil {
|
||||
// Panic to enforce valid input
|
||||
panic("Chain.Step: provided function cannot be nil")
|
||||
}
|
||||
// Create a new step with default configuration
|
||||
step := chainStep{execute: fn, config: stepConfig{}}
|
||||
c.steps = append(c.steps, step)
|
||||
// Update lastStep to point to the newly added step
|
||||
c.lastStep = &c.steps[len(c.steps)-1]
|
||||
return c
|
||||
}
|
||||
|
||||
// Call adds a step by wrapping a function with arguments.
|
||||
// It uses reflection to validate and invoke the function.
|
||||
func (c *Chain) Call(fn interface{}, args ...interface{}) *Chain {
|
||||
// Wrap the function and arguments into an executable step
|
||||
wrappedFn, err := c.wrapCallable(fn, args...)
|
||||
if err != nil {
|
||||
// Panic on setup errors to catch them early
|
||||
panic(fmt.Sprintf("Chain.Call setup error: %v", err))
|
||||
}
|
||||
// Add the wrapped function as a step
|
||||
step := chainStep{execute: wrappedFn, config: stepConfig{}}
|
||||
c.steps = append(c.steps, step)
|
||||
c.lastStep = &c.steps[len(c.steps)-1]
|
||||
return c
|
||||
}
|
||||
|
||||
// Optional marks the last step as optional.
|
||||
// Optional steps don't stop the chain on error.
|
||||
func (c *Chain) Optional() *Chain {
|
||||
if c.lastStep == nil {
|
||||
// Panic if no step exists to mark as optional
|
||||
panic("Chain.Optional: must call Step() or Call() before Optional()")
|
||||
}
|
||||
c.lastStep.optional = true
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLog adds logging attributes to the last step.
|
||||
func (c *Chain) WithLog(attrs ...slog.Attr) *Chain {
|
||||
if c.lastStep == nil {
|
||||
// Panic if no step exists to configure
|
||||
panic("Chain.WithLog: must call Step() or Call() before WithLog()")
|
||||
}
|
||||
// Append attributes to the step's logging configuration
|
||||
c.lastStep.config.logAttrs = append(c.lastStep.config.logAttrs, attrs...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Timeout sets a timeout for the entire chain.
|
||||
func (c *Chain) Timeout(d time.Duration) *Chain {
|
||||
c.config.timeout = d
|
||||
return c
|
||||
}
|
||||
|
||||
// MaxErrors sets the maximum number of errors allowed.
|
||||
func (c *Chain) MaxErrors(max int) *Chain {
|
||||
if max <= 0 {
|
||||
c.config.maxErrors = -1 // No limit
|
||||
} else {
|
||||
c.config.maxErrors = max
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// With adds a key-value pair to the last step's context.
|
||||
func (c *Chain) With(key string, value interface{}) *Chain {
|
||||
if c.lastStep == nil {
|
||||
// Panic if no step exists to configure
|
||||
panic("Chain.With: must call Step() or Call() before With()")
|
||||
}
|
||||
// Initialize context map if nil
|
||||
if c.lastStep.config.context == nil {
|
||||
c.lastStep.config.context = make(map[string]interface{})
|
||||
}
|
||||
// Add the key-value pair
|
||||
c.lastStep.config.context[key] = value
|
||||
return c
|
||||
}
|
||||
|
||||
// Tag sets an error category for the last step.
|
||||
func (c *Chain) Tag(category ErrorCategory) *Chain {
|
||||
if c.lastStep == nil {
|
||||
// Panic if no step exists to configure
|
||||
panic("Chain.Tag: must call Step() or Call() before Tag()")
|
||||
}
|
||||
c.lastStep.config.category = category
|
||||
return c
|
||||
}
|
||||
|
||||
// Code sets a numeric error code for the last step.
|
||||
func (c *Chain) Code(code int) *Chain {
|
||||
if c.lastStep == nil {
|
||||
// Panic if no step exists to configure
|
||||
panic("Chain.Code: must call Step() or Call() before Code()")
|
||||
}
|
||||
c.lastStep.config.code = code
|
||||
return c
|
||||
}
|
||||
|
||||
// Retry configures retry behavior for the last step.
|
||||
// Retry configures retry behavior for the last step.
|
||||
func (c *Chain) Retry(maxAttempts int, delay time.Duration, opts ...RetryOption) *Chain {
|
||||
if c.lastStep == nil {
|
||||
panic("Chain.Retry: must call Step() or Call() before Retry()")
|
||||
}
|
||||
if maxAttempts < 1 {
|
||||
maxAttempts = 1
|
||||
}
|
||||
|
||||
// Define default retry options
|
||||
retryOpts := []RetryOption{
|
||||
WithMaxAttempts(maxAttempts),
|
||||
WithDelay(delay),
|
||||
WithRetryIf(func(err error) bool { return IsRetryable(err) }),
|
||||
}
|
||||
|
||||
// Add logging for retry attempts if a handler is configured
|
||||
if c.logHandler != nil {
|
||||
step := c.lastStep
|
||||
retryOpts = append(retryOpts, WithOnRetry(func(attempt int, err error) {
|
||||
// Prepare logging attributes
|
||||
logAttrs := []slog.Attr{
|
||||
slog.Int("attempt", attempt),
|
||||
slog.Int("max_attempts", maxAttempts),
|
||||
}
|
||||
// Enhance the error with step context
|
||||
enhancedErr := c.enhanceError(err, step)
|
||||
// Log the retry attempt
|
||||
c.logError(enhancedErr, fmt.Sprintf("Retrying step (attempt %d/%d)", attempt, maxAttempts), step.config, logAttrs...)
|
||||
}))
|
||||
}
|
||||
|
||||
// Append any additional retry options
|
||||
retryOpts = append(retryOpts, opts...)
|
||||
// Create and assign the retry configuration
|
||||
c.lastStep.config.retry = NewRetry(retryOpts...)
|
||||
return c
|
||||
}
|
||||
|
||||
// LogOnFail enables automatic logging of errors for the last step.
|
||||
func (c *Chain) LogOnFail() *Chain {
|
||||
if c.lastStep == nil {
|
||||
// Panic if no step exists to configure
|
||||
panic("Chain.LogOnFail: must call Step() or Call() before LogOnFail()")
|
||||
}
|
||||
c.lastStep.config.logOnFail = true
|
||||
return c
|
||||
}
|
||||
|
||||
// Run executes the chain, stopping on the first non-optional error.
|
||||
// It returns the first error encountered or nil if all steps succeed.
|
||||
func (c *Chain) Run() error {
|
||||
// Create a context with timeout or cancellation
|
||||
ctx, cancel := c.getContextAndCancel()
|
||||
defer cancel()
|
||||
c.cancel = cancel
|
||||
// Clear any previous errors
|
||||
c.errors = c.errors[:0]
|
||||
|
||||
// Execute each step in sequence
|
||||
for i := range c.steps {
|
||||
step := &c.steps[i]
|
||||
// Check if the context has been canceled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := ctx.Err()
|
||||
// Enhance the error with step context
|
||||
enhancedErr := c.enhanceError(err, step)
|
||||
c.errors = append(c.errors, enhancedErr)
|
||||
// Log the context error
|
||||
c.logError(enhancedErr, "Chain stopped due to context error before step", step.config)
|
||||
return enhancedErr
|
||||
default:
|
||||
}
|
||||
|
||||
// Execute the step
|
||||
err := c.executeStep(ctx, step)
|
||||
if err != nil {
|
||||
// Enhance the error with step context
|
||||
enhancedErr := c.enhanceError(err, step)
|
||||
c.errors = append(c.errors, enhancedErr)
|
||||
// Log the error if required
|
||||
if step.config.logOnFail || !step.optional {
|
||||
logMsg := "Chain stopped due to error in step"
|
||||
if step.optional {
|
||||
logMsg = "Optional step failed"
|
||||
}
|
||||
c.logError(enhancedErr, logMsg, step.config)
|
||||
}
|
||||
// Stop execution if the step is not optional
|
||||
if !step.optional {
|
||||
return enhancedErr
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return nil if all steps completed successfully
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunAll executes all steps, collecting errors without stopping.
|
||||
// It returns a MultiError containing all errors or nil if none occurred.
|
||||
func (c *Chain) RunAll() error {
|
||||
ctx, cancel := c.getContextAndCancel()
|
||||
defer cancel()
|
||||
c.cancel = cancel
|
||||
c.errors = c.errors[:0]
|
||||
multi := NewMultiError()
|
||||
|
||||
for i := range c.steps {
|
||||
step := &c.steps[i]
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := ctx.Err()
|
||||
enhancedErr := c.enhanceError(err, step)
|
||||
c.errors = append(c.errors, enhancedErr)
|
||||
multi.Add(enhancedErr)
|
||||
c.logError(enhancedErr, "Chain stopped due to context error before step (RunAll)", step.config)
|
||||
goto endRunAll
|
||||
default:
|
||||
}
|
||||
|
||||
err := c.executeStep(ctx, step)
|
||||
if err != nil {
|
||||
enhancedErr := c.enhanceError(err, step)
|
||||
c.errors = append(c.errors, enhancedErr)
|
||||
multi.Add(enhancedErr)
|
||||
if step.config.logOnFail && c.logHandler != nil {
|
||||
c.logError(enhancedErr, "Step failed during RunAll", step.config)
|
||||
}
|
||||
if c.config.maxErrors > 0 && multi.Count() >= c.config.maxErrors {
|
||||
if c.logHandler != nil {
|
||||
// Create a logger to log the max errors condition
|
||||
logger := slog.New(c.logHandler)
|
||||
logger.LogAttrs(
|
||||
context.Background(),
|
||||
slog.LevelError,
|
||||
fmt.Sprintf("Stopping RunAll after reaching max errors (%d)", c.config.maxErrors),
|
||||
slog.Int("max_errors", c.config.maxErrors),
|
||||
)
|
||||
}
|
||||
goto endRunAll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endRunAll:
|
||||
return multi.Single()
|
||||
}
|
||||
|
||||
// Errors returns a copy of the collected errors.
|
||||
func (c *Chain) Errors() []error {
|
||||
if len(c.errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Create a copy to prevent external modification
|
||||
errs := make([]error, len(c.errors))
|
||||
copy(errs, c.errors)
|
||||
return errs
|
||||
}
|
||||
|
||||
// Len returns the number of steps in the chain.
|
||||
func (c *Chain) Len() int {
|
||||
return len(c.steps)
|
||||
}
|
||||
|
||||
// HasErrors checks if any errors were collected.
|
||||
func (c *Chain) HasErrors() bool {
|
||||
return len(c.errors) > 0
|
||||
}
|
||||
|
||||
// LastError returns the most recent error or nil if none exist.
|
||||
func (c *Chain) LastError() error {
|
||||
if len(c.errors) > 0 {
|
||||
return c.errors[len(c.errors)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset clears the chain's steps, errors, and context.
|
||||
func (c *Chain) Reset() {
|
||||
if c.cancel != nil {
|
||||
// Cancel any active context
|
||||
c.cancel()
|
||||
c.cancel = nil
|
||||
}
|
||||
// Clear steps and errors
|
||||
c.steps = c.steps[:0]
|
||||
c.errors = c.errors[:0]
|
||||
c.lastStep = nil
|
||||
}
|
||||
|
||||
// Unwrap returns the collected errors (alias for Errors).
|
||||
func (c *Chain) Unwrap() []error {
|
||||
return c.errors
|
||||
}
|
||||
|
||||
// getContextAndCancel creates a context based on the chain's timeout.
|
||||
// It returns a context and its cancellation function.
|
||||
func (c *Chain) getContextAndCancel() (context.Context, context.CancelFunc) {
|
||||
parentCtx := context.Background()
|
||||
if c.config.timeout > 0 {
|
||||
// Create a context with a timeout
|
||||
return context.WithTimeout(parentCtx, c.config.timeout)
|
||||
}
|
||||
// Create a cancellable context
|
||||
return context.WithCancel(parentCtx)
|
||||
}
|
||||
|
||||
// logError logs an error with step-specific context and attributes.
|
||||
// It only logs if a handler is configured and the error is non-nil.
|
||||
func (c *Chain) logError(err error, msg string, config stepConfig, additionalAttrs ...slog.Attr) {
|
||||
// Skip logging if no handler is set or error is nil
|
||||
if c == nil || c.logHandler == nil || err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a logger on demand using the configured handler
|
||||
logger := slog.New(c.logHandler)
|
||||
|
||||
// Initialize attributes with error and timestamp
|
||||
allAttrs := make([]slog.Attr, 0, 5+len(config.logAttrs)+len(additionalAttrs))
|
||||
allAttrs = append(allAttrs, slog.Any("error", err))
|
||||
allAttrs = append(allAttrs, slog.Time("timestamp", time.Now()))
|
||||
|
||||
// Add step-specific metadata
|
||||
if config.category != "" {
|
||||
allAttrs = append(allAttrs, slog.String("category", string(config.category)))
|
||||
}
|
||||
if config.code != 0 {
|
||||
allAttrs = append(allAttrs, slog.Int("code", config.code))
|
||||
}
|
||||
for k, v := range config.context {
|
||||
allAttrs = append(allAttrs, slog.Any(k, v))
|
||||
}
|
||||
allAttrs = append(allAttrs, config.logAttrs...)
|
||||
allAttrs = append(allAttrs, additionalAttrs...)
|
||||
|
||||
// Add stack trace and error name if the error is of type *Error
|
||||
if e, ok := err.(*Error); ok {
|
||||
if stack := e.Stack(); len(stack) > 0 {
|
||||
// Format stack trace, truncating if too long
|
||||
stackStr := "\n\t" + strings.Join(stack, "\n\t")
|
||||
if len(stackStr) > 1000 {
|
||||
stackStr = stackStr[:1000] + "..."
|
||||
}
|
||||
allAttrs = append(allAttrs, slog.String("stacktrace", stackStr))
|
||||
}
|
||||
if name := e.Name(); name != "" {
|
||||
allAttrs = append(allAttrs, slog.String("error_name", name))
|
||||
}
|
||||
}
|
||||
|
||||
// Log the error at ERROR level with all attributes
|
||||
// Use a defer to catch any panics during logging
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Print to stdout to avoid infinite recursion
|
||||
fmt.Printf("ERROR: Recovered from panic during logging: %v\nAttributes: %v\n", r, allAttrs)
|
||||
}
|
||||
}()
|
||||
logger.LogAttrs(context.Background(), slog.LevelError, msg, allAttrs...)
|
||||
}
|
||||
|
||||
// wrapCallable wraps a function and its arguments into an executable step.
|
||||
// It uses reflection to validate the function and arguments.
|
||||
func (c *Chain) wrapCallable(fn interface{}, args ...interface{}) (func() error, error) {
|
||||
val := reflect.ValueOf(fn)
|
||||
typ := val.Type()
|
||||
|
||||
// Ensure the provided value is a function
|
||||
if typ.Kind() != reflect.Func {
|
||||
return nil, fmt.Errorf("provided 'fn' is not a function (got %T)", fn)
|
||||
}
|
||||
// Check if the number of arguments matches the function's signature
|
||||
if typ.NumIn() != len(args) {
|
||||
return nil, fmt.Errorf("function expects %d arguments, but %d were provided", typ.NumIn(), len(args))
|
||||
}
|
||||
|
||||
// Prepare argument values
|
||||
argVals := make([]reflect.Value, len(args))
|
||||
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
||||
for i, arg := range args {
|
||||
expectedType := typ.In(i)
|
||||
var providedVal reflect.Value
|
||||
if arg != nil {
|
||||
providedVal = reflect.ValueOf(arg)
|
||||
// Check if the argument type is assignable to the expected type
|
||||
if !providedVal.Type().AssignableTo(expectedType) {
|
||||
// Special case for error interfaces
|
||||
if expectedType.Kind() == reflect.Interface && expectedType.Implements(errorType) && providedVal.Type().Implements(errorType) {
|
||||
// Allow error interface
|
||||
} else {
|
||||
return nil, fmt.Errorf("argument %d type mismatch: expected %s, got %s", i, expectedType, providedVal.Type())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle nil arguments for nullable types
|
||||
switch expectedType.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
providedVal = reflect.Zero(expectedType)
|
||||
default:
|
||||
return nil, fmt.Errorf("argument %d is nil, but expected non-nillable type %s", i, expectedType)
|
||||
}
|
||||
}
|
||||
argVals[i] = providedVal
|
||||
}
|
||||
|
||||
// Validate the function's return type
|
||||
if typ.NumOut() > 1 || (typ.NumOut() == 1 && !typ.Out(0).Implements(errorType)) {
|
||||
return nil, fmt.Errorf("function must return either no values or a single error (got %d return values)", typ.NumOut())
|
||||
}
|
||||
|
||||
// Return a wrapped function that calls the original with the provided arguments
|
||||
return func() error {
|
||||
results := val.Call(argVals)
|
||||
if len(results) == 1 && results[0].Interface() != nil {
|
||||
return results[0].Interface().(error)
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// executeStep runs a single step, applying retries if configured.
|
||||
func (c *Chain) executeStep(ctx context.Context, step *chainStep) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
if step.config.retry != nil {
|
||||
retry := step.config.retry.Transform(WithContext(ctx))
|
||||
// Wrap step execution to respect context
|
||||
wrappedFn := func() error {
|
||||
type result struct {
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
done <- result{err: step.execute()}
|
||||
}()
|
||||
select {
|
||||
case res := <-done:
|
||||
return res.err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return retry.Execute(wrappedFn)
|
||||
}
|
||||
// Non-retry case also respects context
|
||||
type result struct {
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
done <- result{err: step.execute()}
|
||||
}()
|
||||
select {
|
||||
case res := <-done:
|
||||
return res.err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// enhanceError wraps an error with additional context from the step.
|
||||
func (c *Chain) enhanceError(err error, step *chainStep) error {
|
||||
if err == nil || !c.config.autoWrap {
|
||||
// Return the error unchanged if nil or autoWrap is disabled
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize the base error
|
||||
var baseError *Error
|
||||
if e, ok := err.(*Error); ok {
|
||||
// Copy existing *Error to preserve its properties
|
||||
baseError = e.Copy()
|
||||
} else {
|
||||
// Create a new *Error wrapping the original
|
||||
baseError = New(err.Error()).Wrap(err).WithStack()
|
||||
}
|
||||
|
||||
if step != nil {
|
||||
// Add step-specific context to the error
|
||||
if step.config.category != "" && baseError.Category() == "" {
|
||||
baseError.WithCategory(step.config.category)
|
||||
}
|
||||
if step.config.code != 0 && baseError.Code() == 0 {
|
||||
baseError.WithCode(step.config.code)
|
||||
}
|
||||
for k, v := range step.config.context {
|
||||
baseError.With(k, v)
|
||||
}
|
||||
for _, attr := range step.config.logAttrs {
|
||||
baseError.With(attr.Key, attr.Value.Any())
|
||||
}
|
||||
if step.config.retry != nil && !baseError.HasContextKey(ctxRetry) {
|
||||
// Mark the error as retryable if retries are configured
|
||||
baseError.WithRetryable()
|
||||
}
|
||||
}
|
||||
|
||||
return baseError
|
||||
}
|
||||
1496
vendor/github.com/olekukonko/errors/errors.go
generated
vendored
Normal file
1496
vendor/github.com/olekukonko/errors/errors.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
432
vendor/github.com/olekukonko/errors/helper.go
generated
vendored
Normal file
432
vendor/github.com/olekukonko/errors/helper.go
generated
vendored
Normal file
@@ -0,0 +1,432 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// As wraps errors.As, using custom type assertion for *Error types.
|
||||
// Falls back to standard errors.As for non-*Error types.
|
||||
// Returns false if either err or target is nil.
|
||||
func As(err error, target interface{}) bool {
|
||||
if err == nil || target == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// First try our custom *Error handling
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.As(target)
|
||||
}
|
||||
|
||||
// Fall back to standard errors.As
|
||||
return errors.As(err, target)
|
||||
}
|
||||
|
||||
// Code returns the status code of an error, if it is an *Error.
|
||||
// Returns 500 as a default for non-*Error types to indicate an internal error.
|
||||
func Code(err error) int {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Code()
|
||||
}
|
||||
return DefaultCode
|
||||
}
|
||||
|
||||
// Context extracts the context map from an error, if it is an *Error.
|
||||
// Returns nil for non-*Error types or if no context is present.
|
||||
func Context(err error) map[string]interface{} {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Context()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert transforms any error into an *Error, preserving its message and wrapping it if needed.
|
||||
// Returns nil if the input is nil; returns the original if already an *Error.
|
||||
// Uses multiple strategies: direct assertion, errors.As, manual unwrapping, and fallback creation.
|
||||
func Convert(err error) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First try direct type assertion (fast path)
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e
|
||||
}
|
||||
|
||||
// Try using errors.As (more flexible)
|
||||
var e *Error
|
||||
if errors.As(err, &e) {
|
||||
return e
|
||||
}
|
||||
|
||||
// Manual unwrapping as fallback
|
||||
visited := make(map[error]bool)
|
||||
for unwrapped := err; unwrapped != nil; {
|
||||
if visited[unwrapped] {
|
||||
break // Cycle detected
|
||||
}
|
||||
visited[unwrapped] = true
|
||||
if e, ok := unwrapped.(*Error); ok {
|
||||
return e
|
||||
}
|
||||
unwrapped = errors.Unwrap(unwrapped)
|
||||
}
|
||||
|
||||
// Final fallback: create new error with original message and wrap it
|
||||
return New(err.Error()).Wrap(err)
|
||||
}
|
||||
|
||||
// Count returns the occurrence count of an error, if it is an *Error.
|
||||
// Returns 0 for non-*Error types.
|
||||
func Count(err error) uint64 {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Count()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Find searches the error chain for the first error matching pred.
|
||||
// Returns nil if no match is found or pred is nil; traverses both Unwrap() and Cause() chains.
|
||||
func Find(err error, pred func(error) bool) error {
|
||||
for current := err; current != nil; {
|
||||
if pred(current) {
|
||||
return current
|
||||
}
|
||||
|
||||
// Attempt to unwrap using Unwrap() or Cause()
|
||||
switch v := current.(type) {
|
||||
case interface{ Unwrap() error }:
|
||||
current = v.Unwrap()
|
||||
case interface{ Cause() error }:
|
||||
current = v.Cause()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// From transforms any error into an *Error, preserving its message and wrapping it if needed.
|
||||
// Alias of Convert; returns nil if input is nil, original if already an *Error.
|
||||
func From(err error) *Error {
|
||||
return Convert(err)
|
||||
}
|
||||
|
||||
// FromContext creates an *Error from a context and an existing error.
|
||||
// Enhances the error with context info: timeout status, deadline, or cancellation.
|
||||
// Returns nil if input error is nil; does not store context values directly.
|
||||
func FromContext(ctx context.Context, err error) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e := New(err.Error())
|
||||
|
||||
// Handle context errors
|
||||
switch ctx.Err() {
|
||||
case context.DeadlineExceeded:
|
||||
e.WithTimeout()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
e.With("deadline", deadline.Format(time.RFC3339))
|
||||
}
|
||||
case context.Canceled:
|
||||
e.With("cancelled", true)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Category returns the category of an error, if it is an *Error.
|
||||
// Returns an empty string for non-*Error types or unset categories.
|
||||
func Category(err error) string {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.category
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Has checks if an error contains meaningful content.
|
||||
// Returns true for non-nil standard errors or *Error with content (msg, name, template, or cause).
|
||||
func Has(err error) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Has()
|
||||
}
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// HasContextKey checks if the error's context contains the specified key.
|
||||
// Returns false for non-*Error types or if the key is not present in the context.
|
||||
func HasContextKey(err error, key string) bool {
|
||||
if e, ok := err.(*Error); ok {
|
||||
ctx := e.Context()
|
||||
if ctx != nil {
|
||||
_, exists := ctx[key]
|
||||
return exists
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Is wraps errors.Is, using custom matching for *Error types.
|
||||
// Falls back to standard errors.Is for non-*Error types; returns true if err equals target.
|
||||
func Is(err, target error) bool {
|
||||
if err == nil || target == nil {
|
||||
return err == target
|
||||
}
|
||||
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Is(target)
|
||||
}
|
||||
|
||||
// Use standard errors.Is for non-Error types
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// IsError checks if an error is an instance of *Error.
|
||||
// Returns true only for this package's custom error type; false for nil or other types.
|
||||
func IsError(err error) bool {
|
||||
_, ok := err.(*Error)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsEmpty checks if an error has no meaningful content.
|
||||
// Returns true for nil errors, empty *Error instances, or standard errors with whitespace-only messages.
|
||||
func IsEmpty(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.IsEmpty()
|
||||
}
|
||||
return strings.TrimSpace(err.Error()) == ""
|
||||
}
|
||||
|
||||
// IsNull checks if an error is nil or represents a NULL value.
|
||||
// Delegates to *Error’s IsNull for custom errors; uses sqlNull for others.
|
||||
func IsNull(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.IsNull()
|
||||
}
|
||||
return sqlNull(err)
|
||||
}
|
||||
|
||||
// IsRetryable checks if an error is retryable.
|
||||
// For *Error, checks context for retry flag; for others, looks for "retry" or timeout in message.
|
||||
// Returns false for nil errors; thread-safe for *Error types.
|
||||
func IsRetryable(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
// Check smallContext directly if context map isn’t populated
|
||||
for i := int32(0); i < e.smallCount; i++ {
|
||||
if e.smallContext[i].key == ctxRetry {
|
||||
if val, ok := e.smallContext[i].value.(bool); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check regular context
|
||||
if e.context != nil {
|
||||
if val, ok := e.context[ctxRetry].(bool); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
// Check cause recursively
|
||||
if e.cause != nil {
|
||||
return IsRetryable(e.cause)
|
||||
}
|
||||
}
|
||||
lowerMsg := strings.ToLower(err.Error())
|
||||
return IsTimeout(err) || strings.Contains(lowerMsg, "retry")
|
||||
}
|
||||
|
||||
// IsTimeout checks if an error indicates a timeout.
|
||||
// For *Error, checks context for timeout flag; for others, looks for "timeout" in message.
|
||||
// Returns false for nil errors.
|
||||
func IsTimeout(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
if val, ok := e.Context()[ctxTimeout].(bool); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "timeout")
|
||||
}
|
||||
|
||||
// Merge combines multiple errors into a single *Error.
|
||||
// Aggregates messages with "; " separator, merges contexts and stacks; returns nil if no errors provided.
|
||||
func Merge(errs ...error) *Error {
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var messages []string
|
||||
combined := New("")
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, err.Error())
|
||||
if e, ok := err.(*Error); ok {
|
||||
if e.stack != nil && combined.stack == nil {
|
||||
combined.WithStack() // Capture stack from first *Error with stack
|
||||
}
|
||||
if ctx := e.Context(); ctx != nil {
|
||||
for k, v := range ctx {
|
||||
combined.With(k, v)
|
||||
}
|
||||
}
|
||||
if e.cause != nil {
|
||||
combined.Wrap(e.cause)
|
||||
}
|
||||
} else {
|
||||
combined.Wrap(err)
|
||||
}
|
||||
}
|
||||
if len(messages) > 0 {
|
||||
combined.msg = strings.Join(messages, "; ")
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
// Name returns the name of an error, if it is an *Error.
|
||||
// Returns an empty string for non-*Error types or unset names.
|
||||
func Name(err error) string {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UnwrapAll returns a slice of all errors in the chain, including the root error.
|
||||
// Traverses both Unwrap() and Cause() chains; returns nil if err is nil.
|
||||
func UnwrapAll(err error) []error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.UnwrapAll()
|
||||
}
|
||||
var result []error
|
||||
Walk(err, func(e error) {
|
||||
result = append(result, e)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Stack extracts the stack trace from an error, if it is an *Error.
|
||||
// Returns nil for non-*Error types or if no stack is present.
|
||||
func Stack(err error) []string {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.Stack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transform applies transformations to an error, returning a new *Error.
|
||||
// Creates a new *Error from non-*Error types before applying fn; returns nil if err is nil.
|
||||
func Transform(err error, fn func(*Error)) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
newErr := e.Copy()
|
||||
fn(newErr)
|
||||
return newErr
|
||||
}
|
||||
// If not an *Error, create a new one and transform it
|
||||
newErr := New(err.Error())
|
||||
fn(newErr)
|
||||
return newErr
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause of an error, if it implements Unwrap.
|
||||
// For *Error, returns cause; for others, returns the error itself; nil if err is nil.
|
||||
func Unwrap(err error) error {
|
||||
for current := err; current != nil; {
|
||||
if e, ok := current.(*Error); ok {
|
||||
if e.cause == nil {
|
||||
return current
|
||||
}
|
||||
current = e.cause
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Walk traverses the error chain, applying fn to each error.
|
||||
// Supports both Unwrap() and Cause() interfaces; stops at nil or non-unwrappable errors.
|
||||
func Walk(err error, fn func(error)) {
|
||||
for current := err; current != nil; {
|
||||
fn(current)
|
||||
|
||||
// Attempt to unwrap using Unwrap() or Cause()
|
||||
switch v := current.(type) {
|
||||
case interface{ Unwrap() error }:
|
||||
current = v.Unwrap()
|
||||
case interface{ Cause() error }:
|
||||
current = v.Cause()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With adds a key-value pair to an error's context, if it is an *Error.
|
||||
// Returns the original error unchanged if not an *Error; no-op for non-*Error types.
|
||||
func With(err error, key string, value interface{}) error {
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.With(key, value)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WithStack converts any error to an *Error and captures a stack trace.
|
||||
// Returns nil if input is nil; adds stack to existing *Error or wraps non-*Error types.
|
||||
func WithStack(err error) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if e, ok := err.(*Error); ok {
|
||||
return e.WithStack()
|
||||
}
|
||||
return New(err.Error()).WithStack().Wrap(err)
|
||||
}
|
||||
|
||||
// Wrap creates a new *Error that wraps another error with additional context.
|
||||
// Uses a copy of the provided wrapper *Error; returns nil if err is nil.
|
||||
func Wrap(err error, wrapper *Error) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if wrapper == nil {
|
||||
wrapper = newError()
|
||||
}
|
||||
newErr := wrapper.Copy()
|
||||
newErr.cause = err
|
||||
return newErr
|
||||
}
|
||||
|
||||
// Wrapf creates a new formatted *Error that wraps another error.
|
||||
// Formats the message and sets the cause; returns nil if err is nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
e := newError()
|
||||
e.msg = fmt.Sprintf(format, args...)
|
||||
e.cause = err
|
||||
return e
|
||||
}
|
||||
225
vendor/github.com/olekukonko/errors/inspect.go
generated
vendored
Normal file
225
vendor/github.com/olekukonko/errors/inspect.go
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
// File: inspect.go
|
||||
// Updated to support both error and *Error with delegation for cleaner *Error handling
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
stderrs "errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Inspect provides detailed examination of an error, handling both single errors and MultiError
|
||||
func Inspect(err error) {
|
||||
if err == nil {
|
||||
fmt.Println("No error occurred")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Error Inspection ===\n")
|
||||
fmt.Printf("Top-level error: %v\n", err)
|
||||
fmt.Printf("Top-level error type: %T\n", err)
|
||||
|
||||
// Handle *Error directly
|
||||
if e, ok := err.(*Error); ok {
|
||||
InspectError(e)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle MultiError
|
||||
if multi, ok := err.(*MultiError); ok {
|
||||
allErrors := multi.Errors()
|
||||
fmt.Printf("\nContains %d errors:\n", len(allErrors))
|
||||
for i, e := range allErrors {
|
||||
fmt.Printf("\n--- Error %d ---\n", i+1)
|
||||
inspectSingleError(e)
|
||||
}
|
||||
} else {
|
||||
// Inspect single error if not MultiError or *Error
|
||||
fmt.Println("\n--- Details ---")
|
||||
inspectSingleError(err)
|
||||
}
|
||||
|
||||
// Additional diagnostics
|
||||
fmt.Println("\n--- Diagnostics ---")
|
||||
if IsRetryable(err) {
|
||||
fmt.Println("- Error chain contains retryable errors")
|
||||
}
|
||||
if IsTimeout(err) {
|
||||
fmt.Println("- Error chain contains timeout errors")
|
||||
}
|
||||
if code := getErrorCode(err); code != 0 {
|
||||
fmt.Printf("- Highest priority error code: %d\n", code)
|
||||
}
|
||||
fmt.Printf("========================\n\n")
|
||||
}
|
||||
|
||||
// InspectError provides detailed inspection of a specific *Error instance
|
||||
func InspectError(err *Error) {
|
||||
if err == nil {
|
||||
fmt.Println("No error occurred")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Error Inspection (*Error) ===\n")
|
||||
fmt.Printf("Top-level error: %v\n", err)
|
||||
fmt.Printf("Top-level error type: %T\n", err)
|
||||
|
||||
fmt.Println("\n--- Details ---")
|
||||
inspectSingleError(err) // Delegate to handle unwrapping and details
|
||||
|
||||
// Additional diagnostics specific to *Error
|
||||
fmt.Println("\n--- Diagnostics ---")
|
||||
if IsRetryable(err) {
|
||||
fmt.Println("- Error is retryable")
|
||||
}
|
||||
if IsTimeout(err) {
|
||||
fmt.Println("- Error chain contains timeout errors")
|
||||
}
|
||||
if code := err.Code(); code != 0 {
|
||||
fmt.Printf("- Error code: %d\n", code)
|
||||
}
|
||||
fmt.Printf("========================\n\n")
|
||||
}
|
||||
|
||||
// inspectSingleError handles inspection of a single error (may be part of a chain)
|
||||
func inspectSingleError(err error) {
|
||||
if err == nil {
|
||||
fmt.Println(" (nil error)")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" Error: %v\n", err)
|
||||
fmt.Printf(" Type: %T\n", err)
|
||||
|
||||
// Handle wrapped errors, including *Error type
|
||||
var currentErr error = err
|
||||
depth := 0
|
||||
for currentErr != nil {
|
||||
prefix := strings.Repeat(" ", depth+1)
|
||||
if depth > 0 {
|
||||
fmt.Printf("%sWrapped Cause (%T): %v\n", prefix, currentErr, currentErr)
|
||||
}
|
||||
|
||||
// Check if it's our specific *Error type
|
||||
if e, ok := currentErr.(*Error); ok {
|
||||
if name := e.Name(); name != "" {
|
||||
fmt.Printf("%sName: %s\n", prefix, name)
|
||||
}
|
||||
if cat := e.Category(); cat != "" {
|
||||
fmt.Printf("%sCategory: %s\n", prefix, cat)
|
||||
}
|
||||
if code := e.Code(); code != 0 {
|
||||
fmt.Printf("%sCode: %d\n", prefix, code)
|
||||
}
|
||||
if ctx := e.Context(); len(ctx) > 0 {
|
||||
fmt.Printf("%sContext:\n", prefix)
|
||||
for k, v := range ctx {
|
||||
fmt.Printf("%s %s: %v\n", prefix, k, v)
|
||||
}
|
||||
}
|
||||
if stack := e.Stack(); len(stack) > 0 {
|
||||
fmt.Printf("%sStack (Top 3):\n", prefix)
|
||||
limit := 3
|
||||
if len(stack) < limit {
|
||||
limit = len(stack)
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
fmt.Printf("%s %s\n", prefix, stack[i])
|
||||
}
|
||||
if len(stack) > limit {
|
||||
fmt.Printf("%s ... (%d more frames)\n", prefix, len(stack)-limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap using standard errors.Unwrap and handle *Error Unwrap
|
||||
var nextErr error
|
||||
// Prioritize *Error's Unwrap if available AND it returns non-nil
|
||||
if e, ok := currentErr.(*Error); ok {
|
||||
unwrapped := e.Unwrap()
|
||||
if unwrapped != nil {
|
||||
nextErr = unwrapped
|
||||
} else {
|
||||
// If *Error.Unwrap returns nil, fall back to standard unwrap
|
||||
// This handles cases where *Error might wrap a non-standard error
|
||||
// or where its internal cause is deliberately nil.
|
||||
nextErr = stderrs.Unwrap(currentErr)
|
||||
}
|
||||
} else {
|
||||
nextErr = stderrs.Unwrap(currentErr) // Fall back to standard unwrap for non-*Error types
|
||||
}
|
||||
|
||||
// Prevent infinite loops if Unwrap returns the same error, or stop if no more unwrapping
|
||||
if nextErr == currentErr || nextErr == nil {
|
||||
break
|
||||
}
|
||||
currentErr = nextErr
|
||||
depth++
|
||||
if depth > 10 { // Safety break for very deep or potentially cyclic chains
|
||||
fmt.Printf("%s... (chain too deep or potential cycle)\n", strings.Repeat(" ", depth+1))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getErrorCode traverses the error chain to find the highest priority code.
|
||||
// It uses errors.As to find the first *Error in the chain.
|
||||
func getErrorCode(err error) int {
|
||||
var code int = 0 // Default code
|
||||
var target *Error
|
||||
if As(err, &target) { // Use the package's As helper
|
||||
if target != nil { // Add nil check for safety
|
||||
code = target.Code()
|
||||
}
|
||||
}
|
||||
// If the top-level error is *Error and has a code, it might take precedence.
|
||||
// This depends on desired logic. Let's keep it simple for now: first code found by As.
|
||||
if code == 0 { // Only check top-level if As didn't find one with a code
|
||||
if e, ok := err.(*Error); ok {
|
||||
code = e.Code()
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// handleError demonstrates using Inspect with additional handling logic
|
||||
func handleError(err error) {
|
||||
fmt.Println("\n=== Processing Failure ===")
|
||||
Inspect(err) // Use the primary Inspect function
|
||||
|
||||
// Additional handling based on inspection
|
||||
code := getErrorCode(err) // Use the helper
|
||||
|
||||
switch {
|
||||
case IsTimeout(err):
|
||||
fmt.Println("\nAction: Check connectivity or increase timeout")
|
||||
case code == 402: // Check code obtained via helper
|
||||
fmt.Println("\nAction: Payment processing failed - notify billing")
|
||||
default:
|
||||
fmt.Println("\nAction: Generic failure handling")
|
||||
}
|
||||
}
|
||||
|
||||
// processOrder demonstrates Chain usage with Inspect
|
||||
func processOrder() error {
|
||||
validateInput := func() error { return nil }
|
||||
processPayment := func() error { return stderrs.New("credit card declined") }
|
||||
sendNotification := func() error { fmt.Println("Notification sent."); return nil }
|
||||
logOrder := func() error { fmt.Println("Order logged."); return nil }
|
||||
|
||||
chain := NewChain(ChainWithTimeout(2*time.Second)).
|
||||
Step(validateInput).Tag("validation").
|
||||
Step(processPayment).Tag("billing").Code(402).Retry(3, 100*time.Millisecond, WithRetryIf(IsRetryable)).
|
||||
Step(sendNotification).Optional().
|
||||
Step(logOrder)
|
||||
|
||||
err := chain.Run()
|
||||
if err != nil {
|
||||
handleError(err) // Call the unified error handler
|
||||
return err // Propagate the error if needed
|
||||
}
|
||||
fmt.Println("Order processed successfully!")
|
||||
return nil
|
||||
}
|
||||
423
vendor/github.com/olekukonko/errors/multi_error.go
generated
vendored
Normal file
423
vendor/github.com/olekukonko/errors/multi_error.go
generated
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// MultiError represents a thread-safe collection of errors with enhanced features.
|
||||
// Supports limits, sampling, and custom formatting for error aggregation.
|
||||
type MultiError struct {
|
||||
errors []error
|
||||
mu sync.RWMutex
|
||||
|
||||
// Configuration fields
|
||||
limit int // Maximum number of errors to store (0 = unlimited)
|
||||
formatter ErrorFormatter // Custom formatting function for error string
|
||||
sampling bool // Whether sampling is enabled to limit error collection
|
||||
sampleRate uint32 // Sampling percentage (1-100) when sampling is enabled
|
||||
rand *rand.Rand // Random source for sampling (nil defaults to fastRand)
|
||||
}
|
||||
|
||||
// ErrorFormatter defines a function for custom error message formatting.
|
||||
// Takes a slice of errors and returns a single formatted string.
|
||||
type ErrorFormatter func([]error) string
|
||||
|
||||
// MultiErrorOption configures MultiError behavior during creation.
|
||||
type MultiErrorOption func(*MultiError)
|
||||
|
||||
// NewMultiError creates a new MultiError instance with optional configuration.
|
||||
// Initial capacity is set to 4; applies options in the order provided.
|
||||
func NewMultiError(opts ...MultiErrorOption) *MultiError {
|
||||
m := &MultiError{
|
||||
errors: make([]error, 0, 4),
|
||||
limit: 0, // Unlimited by default
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Add appends an error to the collection with optional sampling, limit checks, and duplicate prevention.
|
||||
// Ignores nil errors and duplicates based on string equality; thread-safe.
|
||||
func (m *MultiError) Add(errs ...error) {
|
||||
if len(errs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for duplicates by comparing error messages
|
||||
duplicate := false
|
||||
for _, e := range m.errors {
|
||||
if e.Error() == err.Error() {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if duplicate {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply sampling if enabled and collection isn’t empty
|
||||
if m.sampling && len(m.errors) > 0 {
|
||||
var r uint32
|
||||
if m.rand != nil {
|
||||
r = uint32(m.rand.Int31n(100))
|
||||
} else {
|
||||
r = fastRand() % 100
|
||||
}
|
||||
if r > m.sampleRate { // Accept if random value is within sample rate
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Respect limit if set
|
||||
if m.limit > 0 && len(m.errors) >= m.limit {
|
||||
continue
|
||||
}
|
||||
|
||||
m.errors = append(m.errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Addf formats and adds a new error to the collection.
|
||||
func (m *MultiError) Addf(format string, args ...interface{}) {
|
||||
m.Add(Newf(format, args...))
|
||||
}
|
||||
|
||||
// Clear removes all errors from the collection.
|
||||
// Thread-safe; resets the slice while preserving capacity.
|
||||
func (m *MultiError) Clear() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.errors = m.errors[:0]
|
||||
}
|
||||
|
||||
// Count returns the number of errors in the collection.
|
||||
// Thread-safe.
|
||||
func (m *MultiError) Count() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.errors)
|
||||
}
|
||||
|
||||
// Error returns a formatted string representation of the errors.
|
||||
// Returns empty string if no errors, single error message if one exists,
|
||||
// or a formatted list using custom formatter or default if multiple; thread-safe.
|
||||
func (m *MultiError) Error() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
switch len(m.errors) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return m.errors[0].Error()
|
||||
default:
|
||||
if m.formatter != nil {
|
||||
return m.formatter(m.errors)
|
||||
}
|
||||
return defaultFormat(m.errors)
|
||||
}
|
||||
}
|
||||
|
||||
// Errors returns a copy of the contained errors.
|
||||
// Thread-safe; returns nil if no errors exist.
|
||||
func (m *MultiError) Errors() []error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if len(m.errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
errs := make([]error, len(m.errors))
|
||||
copy(errs, m.errors)
|
||||
return errs
|
||||
}
|
||||
|
||||
// Filter returns a new MultiError containing only errors that match the predicate.
|
||||
// Thread-safe; preserves original configuration including limit, formatter, and sampling.
|
||||
func (m *MultiError) Filter(fn func(error) bool) *MultiError {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var opts []MultiErrorOption
|
||||
opts = append(opts, WithLimit(m.limit))
|
||||
if m.formatter != nil {
|
||||
opts = append(opts, WithFormatter(m.formatter))
|
||||
}
|
||||
if m.sampling {
|
||||
opts = append(opts, WithSampling(m.sampleRate))
|
||||
}
|
||||
|
||||
filtered := NewMultiError(opts...)
|
||||
for _, err := range m.errors {
|
||||
if fn(err) {
|
||||
filtered.Add(err)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// First returns the first error in the collection, if any.
|
||||
// Thread-safe; returns nil if the collection is empty.
|
||||
func (m *MultiError) First() error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if len(m.errors) > 0 {
|
||||
return m.errors[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Has reports whether the collection contains any errors.
|
||||
// Thread-safe.
|
||||
func (m *MultiError) Has() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.errors) > 0
|
||||
}
|
||||
|
||||
// Last returns the most recently added error in the collection, if any.
|
||||
// Thread-safe; returns nil if the collection is empty.
|
||||
func (m *MultiError) Last() error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if len(m.errors) > 0 {
|
||||
return m.errors[len(m.errors)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge combines another MultiError's errors into this one.
|
||||
// Thread-safe; respects this instance’s limit and sampling settings; no-op if other is nil or empty.
|
||||
func (m *MultiError) Merge(other *MultiError) {
|
||||
if other == nil || !other.Has() {
|
||||
return
|
||||
}
|
||||
|
||||
other.mu.RLock()
|
||||
defer other.mu.RUnlock()
|
||||
|
||||
for _, err := range other.errors {
|
||||
m.Add(err)
|
||||
}
|
||||
}
|
||||
|
||||
// IsNull checks if the MultiError is empty or contains only null errors.
|
||||
// Returns true if empty or all errors are null (via IsNull() or empty message); thread-safe.
|
||||
func (m *MultiError) IsNull() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Fast path for empty MultiError
|
||||
if len(m.errors) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check each error for null status
|
||||
allNull := true
|
||||
for _, err := range m.errors {
|
||||
switch e := err.(type) {
|
||||
case interface{ IsNull() bool }:
|
||||
if !e.IsNull() {
|
||||
allNull = false
|
||||
break
|
||||
}
|
||||
case nil:
|
||||
continue
|
||||
default:
|
||||
if e.Error() != "" {
|
||||
allNull = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return allNull
|
||||
}
|
||||
|
||||
// Single returns nil if the collection is empty, the single error if only one exists,
|
||||
// or the MultiError itself if multiple errors are present.
|
||||
// Thread-safe; useful for unwrapping to a single error when possible.
|
||||
func (m *MultiError) Single() error {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
switch len(m.errors) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return m.errors[0]
|
||||
default:
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
// String implements the Stringer interface for a concise string representation.
|
||||
// Thread-safe; delegates to Error() for formatting.
|
||||
func (m *MultiError) String() string {
|
||||
return m.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns a copy of the contained errors for multi-error unwrapping.
|
||||
// Implements the errors.Unwrap interface; thread-safe; returns nil if empty.
|
||||
func (m *MultiError) Unwrap() []error {
|
||||
return m.Errors()
|
||||
}
|
||||
|
||||
// WithFormatter sets a custom error formatting function.
|
||||
// Returns a MultiErrorOption for use with NewMultiError; overrides default formatting.
|
||||
func WithFormatter(f ErrorFormatter) MultiErrorOption {
|
||||
return func(m *MultiError) {
|
||||
m.formatter = f
|
||||
}
|
||||
}
|
||||
|
||||
// WithLimit sets the maximum number of errors to store.
|
||||
// Returns a MultiErrorOption for use with NewMultiError; 0 means unlimited, negative values are ignored.
|
||||
func WithLimit(n int) MultiErrorOption {
|
||||
return func(m *MultiError) {
|
||||
if n < 0 {
|
||||
n = 0 // Ensure non-negative limit
|
||||
}
|
||||
m.limit = n
|
||||
}
|
||||
}
|
||||
|
||||
// WithSampling enables error sampling with a specified rate (1-100).
|
||||
// Returns a MultiErrorOption for use with NewMultiError; caps rate at 100 for validity.
|
||||
func WithSampling(rate uint32) MultiErrorOption {
|
||||
return func(m *MultiError) {
|
||||
if rate > 100 {
|
||||
rate = 100
|
||||
}
|
||||
m.sampling = true
|
||||
m.sampleRate = rate
|
||||
}
|
||||
}
|
||||
|
||||
// WithRand sets a custom random source for sampling, useful for testing.
|
||||
// Returns a MultiErrorOption for use with NewMultiError; defaults to fastRand if nil.
|
||||
func WithRand(r *rand.Rand) MultiErrorOption {
|
||||
return func(m *MultiError) {
|
||||
m.rand = r
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON serializes the MultiError to JSON, including all contained errors and configuration metadata.
|
||||
// Thread-safe; errors are serialized using their MarshalJSON method if available, otherwise as strings.
|
||||
func (m *MultiError) MarshalJSON() ([]byte, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Get buffer from pool for efficiency
|
||||
buf := jsonBufferPool.Get().(*bytes.Buffer)
|
||||
defer jsonBufferPool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
// Create encoder
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
||||
// Define JSON structure
|
||||
type jsonError struct {
|
||||
Error interface{} `json:"error"` // Holds either JSON-marshaled error or string
|
||||
}
|
||||
|
||||
je := struct {
|
||||
Count int `json:"count"` // Number of errors
|
||||
Limit int `json:"limit,omitempty"` // Maximum error limit (omitted if 0)
|
||||
Sampling bool `json:"sampling,omitempty"` // Whether sampling is enabled
|
||||
SampleRate uint32 `json:"sample_rate,omitempty"` // Sampling rate (1-100, omitted if not sampling)
|
||||
Errors []jsonError `json:"errors"` // List of errors
|
||||
}{
|
||||
Count: len(m.errors),
|
||||
Limit: m.limit,
|
||||
Sampling: m.sampling,
|
||||
SampleRate: m.sampleRate,
|
||||
}
|
||||
|
||||
// Serialize each error
|
||||
je.Errors = make([]jsonError, len(m.errors))
|
||||
for i, err := range m.errors {
|
||||
if err == nil {
|
||||
je.Errors[i] = jsonError{Error: nil}
|
||||
continue
|
||||
}
|
||||
// Check if the error implements json.Marshaler
|
||||
if marshaler, ok := err.(json.Marshaler); ok {
|
||||
marshaled, err := marshaler.MarshalJSON()
|
||||
if err != nil {
|
||||
// Fallback to string if marshaling fails
|
||||
je.Errors[i] = jsonError{Error: err.Error()}
|
||||
} else {
|
||||
var raw json.RawMessage = marshaled
|
||||
je.Errors[i] = jsonError{Error: raw}
|
||||
}
|
||||
} else {
|
||||
// Use error string for non-marshaler errors
|
||||
je.Errors[i] = jsonError{Error: err.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
// Encode JSON
|
||||
if err := enc.Encode(je); err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal MultiError: %v", err)
|
||||
}
|
||||
|
||||
// Remove trailing newline
|
||||
result := buf.Bytes()
|
||||
if len(result) > 0 && result[len(result)-1] == '\n' {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// defaultFormat provides the default formatting for multiple errors.
|
||||
// Returns a semicolon-separated list prefixed with the error count (e.g., "errors(3): err1; err2; err3").
|
||||
func defaultFormat(errs []error) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("errors(%d): ", len(errs)))
|
||||
for i, err := range errs {
|
||||
if i > 0 {
|
||||
sb.WriteString("; ")
|
||||
}
|
||||
sb.WriteString(err.Error())
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// fastRand generates a quick pseudo-random number for sampling.
|
||||
// Uses a simple xorshift algorithm based on the current time; not cryptographically secure.
|
||||
var fastRandState uint32 = 1 // Must be non-zero
|
||||
|
||||
func fastRand() uint32 {
|
||||
for {
|
||||
// Atomically load the current state
|
||||
old := atomic.LoadUint32(&fastRandState)
|
||||
// Xorshift computation
|
||||
x := old
|
||||
x ^= x << 13
|
||||
x ^= x >> 17
|
||||
x ^= x << 5
|
||||
// Attempt to store the new state atomically
|
||||
if atomic.CompareAndSwapUint32(&fastRandState, old, x) {
|
||||
return x
|
||||
}
|
||||
// Otherwise retry
|
||||
}
|
||||
}
|
||||
75
vendor/github.com/olekukonko/errors/pool.go
generated
vendored
Normal file
75
vendor/github.com/olekukonko/errors/pool.go
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
// pool.go
|
||||
package errors
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ErrorPool is a high-performance, thread-safe pool for reusing *Error instances.
|
||||
// Reduces allocation overhead by recycling errors; tracks hit/miss statistics.
|
||||
type ErrorPool struct {
|
||||
pool sync.Pool // Underlying pool for storing *Error instances
|
||||
poolStats struct { // Embedded struct for pool usage statistics
|
||||
hits atomic.Int64 // Number of times an error was reused from the pool
|
||||
misses atomic.Int64 // Number of times a new error was created due to pool miss
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorPool creates a new ErrorPool instance.
|
||||
// Initializes the pool with a New function that returns a fresh *Error with default smallContext.
|
||||
func NewErrorPool() *ErrorPool {
|
||||
return &ErrorPool{
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves an *Error from the pool or creates a new one if pooling is disabled or pool is empty.
|
||||
// Resets are handled by Put; thread-safe; updates hit/miss stats when pooling is enabled.
|
||||
func (ep *ErrorPool) Get() *Error {
|
||||
if currentConfig.disablePooling {
|
||||
return &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
}
|
||||
}
|
||||
|
||||
e := ep.pool.Get().(*Error)
|
||||
if e == nil { // Pool returned nil (unlikely due to New func, but handled for safety)
|
||||
ep.poolStats.misses.Add(1)
|
||||
return &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
}
|
||||
}
|
||||
ep.poolStats.hits.Add(1)
|
||||
return e
|
||||
}
|
||||
|
||||
// Put returns an *Error to the pool after resetting it.
|
||||
// Ignores nil errors or if pooling is disabled; preserves stack capacity; thread-safe.
|
||||
func (ep *ErrorPool) Put(e *Error) {
|
||||
if e == nil || currentConfig.disablePooling {
|
||||
return
|
||||
}
|
||||
|
||||
// Reset the error to a clean state, preserving capacity
|
||||
e.Reset()
|
||||
|
||||
// Reset stack length while keeping capacity for reuse
|
||||
if e.stack != nil {
|
||||
e.stack = e.stack[:0]
|
||||
}
|
||||
|
||||
ep.pool.Put(e)
|
||||
}
|
||||
|
||||
// Stats returns the current pool statistics as hits and misses.
|
||||
// Thread-safe; uses atomic loads to ensure accurate counts.
|
||||
func (ep *ErrorPool) Stats() (hits, misses int64) {
|
||||
return ep.poolStats.hits.Load(), ep.poolStats.misses.Load()
|
||||
}
|
||||
24
vendor/github.com/olekukonko/errors/pool_above_1_24.go
generated
vendored
Normal file
24
vendor/github.com/olekukonko/errors/pool_above_1_24.go
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build go1.24
|
||||
// +build go1.24
|
||||
|
||||
package errors
|
||||
|
||||
import "runtime"
|
||||
|
||||
// setupCleanup configures a cleanup function for an *Error to auto-return it to the pool.
|
||||
// Only active for Go 1.24+; uses runtime.AddCleanup when autoFree is set and pooling is enabled.
|
||||
func (ep *ErrorPool) setupCleanup(e *Error) {
|
||||
if currentConfig.autoFree {
|
||||
runtime.AddCleanup(e, func(_ *struct{}) {
|
||||
if !currentConfig.disablePooling {
|
||||
ep.Put(e) // Return to pool when cleaned up
|
||||
}
|
||||
}, nil) // No additional context needed
|
||||
}
|
||||
}
|
||||
|
||||
// clearCleanup is a no-op for Go 1.24 and above.
|
||||
// Cleanup is managed by runtime.AddCleanup; no explicit removal is required.
|
||||
func (ep *ErrorPool) clearCleanup(e *Error) {
|
||||
// No-op for Go 1.24+
|
||||
}
|
||||
24
vendor/github.com/olekukonko/errors/pool_below_1_24.go
generated
vendored
Normal file
24
vendor/github.com/olekukonko/errors/pool_below_1_24.go
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build !go1.24
|
||||
// +build !go1.24
|
||||
|
||||
package errors
|
||||
|
||||
import "runtime"
|
||||
|
||||
// setupCleanup configures a finalizer for an *Error to auto-return it to the pool.
|
||||
// Only active for Go versions < 1.24; enables automatic cleanup when autoFree is set and pooling is enabled.
|
||||
func (ep *ErrorPool) setupCleanup(e *Error) {
|
||||
if currentConfig.autoFree {
|
||||
runtime.SetFinalizer(e, func(e *Error) {
|
||||
if !currentConfig.disablePooling {
|
||||
ep.Put(e) // Return to pool when garbage collected
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// clearCleanup removes any finalizer set on an *Error.
|
||||
// Only active for Go versions < 1.24; ensures no cleanup action occurs on garbage collection.
|
||||
func (ep *ErrorPool) clearCleanup(e *Error) {
|
||||
runtime.SetFinalizer(e, nil) // Disable finalizer
|
||||
}
|
||||
368
vendor/github.com/olekukonko/errors/retry.go
generated
vendored
Normal file
368
vendor/github.com/olekukonko/errors/retry.go
generated
vendored
Normal file
@@ -0,0 +1,368 @@
|
||||
// Package errors provides utilities for error handling, including a flexible retry mechanism.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BackoffStrategy defines the interface for calculating retry delays.
|
||||
type BackoffStrategy interface {
|
||||
// Backoff returns the delay for a given attempt based on the base delay.
|
||||
Backoff(attempt int, baseDelay time.Duration) time.Duration
|
||||
}
|
||||
|
||||
// ConstantBackoff provides a fixed delay for each retry attempt.
|
||||
type ConstantBackoff struct{}
|
||||
|
||||
// Backoff returns the base delay regardless of the attempt number.
|
||||
// Implements BackoffStrategy with a constant delay.
|
||||
func (c ConstantBackoff) Backoff(_ int, baseDelay time.Duration) time.Duration {
|
||||
return baseDelay
|
||||
}
|
||||
|
||||
// ExponentialBackoff provides an exponentially increasing delay for retry attempts.
|
||||
type ExponentialBackoff struct{}
|
||||
|
||||
// Backoff returns a delay that doubles with each attempt, starting from the base delay.
|
||||
// Uses bit shifting for efficient exponential growth (e.g., baseDelay * 2^(attempt-1)).
|
||||
func (e ExponentialBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration {
|
||||
if attempt <= 1 {
|
||||
return baseDelay
|
||||
}
|
||||
return baseDelay * time.Duration(1<<uint(attempt-1))
|
||||
}
|
||||
|
||||
// LinearBackoff provides a linearly increasing delay for retry attempts.
|
||||
type LinearBackoff struct{}
|
||||
|
||||
// Backoff returns a delay that increases linearly with each attempt (e.g., baseDelay * attempt).
|
||||
// Implements BackoffStrategy with linear progression.
|
||||
func (l LinearBackoff) Backoff(attempt int, baseDelay time.Duration) time.Duration {
|
||||
return baseDelay * time.Duration(attempt)
|
||||
}
|
||||
|
||||
// RetryOption configures a Retry instance.
|
||||
// Defines a function type for setting retry parameters.
|
||||
type RetryOption func(*Retry)
|
||||
|
||||
// Retry represents a retryable operation with configurable backoff and retry logic.
|
||||
// Supports multiple attempts, delay strategies, jitter, and context-aware cancellation.
|
||||
type Retry struct {
|
||||
maxAttempts int // Maximum number of attempts (including initial try)
|
||||
delay time.Duration // Base delay for backoff calculations
|
||||
maxDelay time.Duration // Maximum delay cap to prevent excessive waits
|
||||
retryIf func(error) bool // Condition to determine if retry should occur
|
||||
onRetry func(int, error) // Callback executed after each failed attempt
|
||||
backoff BackoffStrategy // Strategy for calculating retry delays
|
||||
jitter bool // Whether to add random jitter to delays
|
||||
ctx context.Context // Context for cancellation and deadlines
|
||||
}
|
||||
|
||||
// NewRetry creates a new Retry instance with the given options.
|
||||
// Defaults: 3 attempts, 100ms base delay, 10s max delay, exponential backoff with jitter,
|
||||
// and retrying on IsRetryable errors; ensures retryIf is never nil.
|
||||
func NewRetry(options ...RetryOption) *Retry {
|
||||
r := &Retry{
|
||||
maxAttempts: 3,
|
||||
delay: 100 * time.Millisecond,
|
||||
maxDelay: 10 * time.Second,
|
||||
retryIf: func(err error) bool { return IsRetryable(err) },
|
||||
onRetry: nil,
|
||||
backoff: ExponentialBackoff{},
|
||||
jitter: true,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(r)
|
||||
}
|
||||
// Ensure retryIf is never nil, falling back to IsRetryable
|
||||
if r.retryIf == nil {
|
||||
r.retryIf = func(err error) bool { return IsRetryable(err) }
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addJitter adds ±25% jitter to avoid thundering herd problems.
|
||||
// Returns a duration adjusted by a random value between -25% and +25% of the input; not thread-safe.
|
||||
func addJitter(d time.Duration) time.Duration {
|
||||
jitter := time.Duration(rand.Int63n(int64(d/2))) - (d / 4)
|
||||
return d + jitter
|
||||
}
|
||||
|
||||
// Attempts returns the configured maximum number of retry attempts.
|
||||
// Includes the initial attempt in the count.
|
||||
func (r *Retry) Attempts() int {
|
||||
return r.maxAttempts
|
||||
}
|
||||
|
||||
// Execute runs the provided function with the configured retry logic.
|
||||
// Returns nil on success or the last error if all attempts fail; respects context cancellation.
|
||||
func (r *Retry) Execute(fn func() error) error {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
|
||||
// Check context before each attempt
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return r.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// Check if we should retry
|
||||
if r.retryIf != nil && !r.retryIf(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.onRetry != nil {
|
||||
r.onRetry(attempt, err)
|
||||
}
|
||||
|
||||
// Don't delay after last attempt
|
||||
if attempt == r.maxAttempts {
|
||||
break
|
||||
}
|
||||
|
||||
// Calculate delay with backoff
|
||||
delay := r.backoff.Backoff(attempt, r.delay)
|
||||
if r.maxDelay > 0 && delay > r.maxDelay {
|
||||
delay = r.maxDelay
|
||||
}
|
||||
if r.jitter {
|
||||
delay = addJitter(delay)
|
||||
}
|
||||
|
||||
// Wait with context
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return r.ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// ExecuteContext runs the provided function with retry logic, respecting context cancellation.
|
||||
// Returns nil on success or the last error if all attempts fail or context is cancelled.
|
||||
func (r *Retry) ExecuteContext(ctx context.Context, fn func() error) error {
|
||||
var lastErr error
|
||||
|
||||
// If the retry instance already has a context, use it. Otherwise, use the provided one.
|
||||
// If both are provided, maybe create a derived context? For now, prioritize the one from WithContext.
|
||||
execCtx := r.ctx
|
||||
if execCtx == context.Background() && ctx != nil { // Use provided ctx if retry ctx is default and provided one isn't nil
|
||||
execCtx = ctx
|
||||
} else if ctx == nil { // Ensure we always have a non-nil context
|
||||
execCtx = context.Background()
|
||||
}
|
||||
// Note: This logic might need refinement depending on how contexts should interact.
|
||||
// A safer approach might be: if r.ctx != background, use it. Else use provided ctx.
|
||||
|
||||
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
|
||||
// Check context before executing the function
|
||||
select {
|
||||
case <-execCtx.Done():
|
||||
return execCtx.Err() // Return context error immediately
|
||||
default:
|
||||
// Context is okay, proceed
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err == nil {
|
||||
return nil // Success
|
||||
}
|
||||
|
||||
// Check if retry is applicable based on the error
|
||||
if r.retryIf != nil && !r.retryIf(err) {
|
||||
return err // Not retryable, return the error
|
||||
}
|
||||
|
||||
lastErr = err // Store the last encountered error
|
||||
|
||||
// Execute the OnRetry callback if configured
|
||||
if r.onRetry != nil {
|
||||
r.onRetry(attempt, err)
|
||||
}
|
||||
|
||||
// Exit loop if this was the last attempt
|
||||
if attempt == r.maxAttempts {
|
||||
break
|
||||
}
|
||||
|
||||
// --- Calculate and apply delay ---
|
||||
currentDelay := r.backoff.Backoff(attempt, r.delay)
|
||||
if r.maxDelay > 0 && currentDelay > r.maxDelay { // Check maxDelay > 0 before capping
|
||||
currentDelay = r.maxDelay
|
||||
}
|
||||
if r.jitter {
|
||||
currentDelay = addJitter(currentDelay)
|
||||
}
|
||||
if currentDelay < 0 { // Ensure delay isn't negative after jitter
|
||||
currentDelay = 0
|
||||
}
|
||||
// --- Wait for the delay or context cancellation ---
|
||||
select {
|
||||
case <-execCtx.Done():
|
||||
// If context is cancelled during the wait, return the context error
|
||||
// Often more informative than returning the last application error.
|
||||
return execCtx.Err()
|
||||
case <-time.After(currentDelay):
|
||||
// Wait finished, continue to the next attempt
|
||||
}
|
||||
}
|
||||
|
||||
// All attempts failed, return the last error encountered
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// Transform creates a new Retry instance with modified configuration.
|
||||
// Copies all settings from the original Retry and applies the given options.
|
||||
func (r *Retry) Transform(opts ...RetryOption) *Retry {
|
||||
newRetry := &Retry{
|
||||
maxAttempts: r.maxAttempts,
|
||||
delay: r.delay,
|
||||
maxDelay: r.maxDelay,
|
||||
retryIf: r.retryIf,
|
||||
onRetry: r.onRetry,
|
||||
backoff: r.backoff,
|
||||
jitter: r.jitter,
|
||||
ctx: r.ctx,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(newRetry)
|
||||
}
|
||||
return newRetry
|
||||
}
|
||||
|
||||
// WithBackoff sets the backoff strategy using the BackoffStrategy interface.
|
||||
// Returns a RetryOption; no-op if strategy is nil, retaining the existing strategy.
|
||||
func WithBackoff(strategy BackoffStrategy) RetryOption {
|
||||
return func(r *Retry) {
|
||||
if strategy != nil {
|
||||
r.backoff = strategy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext sets the context for cancellation and deadlines.
|
||||
// Returns a RetryOption; retains context.Background if ctx is nil.
|
||||
func WithContext(ctx context.Context) RetryOption {
|
||||
return func(r *Retry) {
|
||||
if ctx != nil {
|
||||
r.ctx = ctx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDelay sets the initial delay between retries.
|
||||
// Returns a RetryOption; ensures non-negative delay by setting negatives to 0.
|
||||
func WithDelay(delay time.Duration) RetryOption {
|
||||
return func(r *Retry) {
|
||||
if delay < 0 {
|
||||
delay = 0
|
||||
}
|
||||
r.delay = delay
|
||||
}
|
||||
}
|
||||
|
||||
// WithJitter enables or disables jitter in the backoff delay.
|
||||
// Returns a RetryOption; toggles random delay variation.
|
||||
func WithJitter(jitter bool) RetryOption {
|
||||
return func(r *Retry) {
|
||||
r.jitter = jitter
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxAttempts sets the maximum number of retry attempts.
|
||||
// Returns a RetryOption; ensures at least 1 attempt by adjusting lower values.
|
||||
func WithMaxAttempts(maxAttempts int) RetryOption {
|
||||
return func(r *Retry) {
|
||||
if maxAttempts < 1 {
|
||||
maxAttempts = 1
|
||||
}
|
||||
r.maxAttempts = maxAttempts
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxDelay sets the maximum delay between retries.
|
||||
// Returns a RetryOption; ensures non-negative delay by setting negatives to 0.
|
||||
func WithMaxDelay(maxDelay time.Duration) RetryOption {
|
||||
return func(r *Retry) {
|
||||
if maxDelay < 0 {
|
||||
maxDelay = 0
|
||||
}
|
||||
r.maxDelay = maxDelay
|
||||
}
|
||||
}
|
||||
|
||||
// WithOnRetry sets a callback to execute after each failed attempt.
|
||||
// Returns a RetryOption; callback receives attempt number and error.
|
||||
func WithOnRetry(onRetry func(attempt int, err error)) RetryOption {
|
||||
return func(r *Retry) {
|
||||
r.onRetry = onRetry
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryIf sets the condition under which to retry.
|
||||
// Returns a RetryOption; retains IsRetryable default if retryIf is nil.
|
||||
func WithRetryIf(retryIf func(error) bool) RetryOption {
|
||||
return func(r *Retry) {
|
||||
if retryIf != nil {
|
||||
r.retryIf = retryIf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteReply runs the provided function with retry logic and returns its result.
|
||||
// Returns the result and nil on success, or zero value and last error on failure; generic type T.
|
||||
func ExecuteReply[T any](r *Retry, fn func() (T, error)) (T, error) {
|
||||
var lastErr error
|
||||
var zero T
|
||||
|
||||
for attempt := 1; attempt <= r.maxAttempts; attempt++ {
|
||||
result, err := fn()
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Check if retry is applicable; return immediately if not retryable
|
||||
if r.retryIf != nil && !r.retryIf(err) {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if r.onRetry != nil {
|
||||
r.onRetry(attempt, err)
|
||||
}
|
||||
|
||||
if attempt == r.maxAttempts {
|
||||
break
|
||||
}
|
||||
|
||||
// Calculate delay with backoff, cap at maxDelay, and apply jitter if enabled
|
||||
currentDelay := r.backoff.Backoff(attempt, r.delay)
|
||||
if currentDelay > r.maxDelay {
|
||||
currentDelay = r.maxDelay
|
||||
}
|
||||
if r.jitter {
|
||||
currentDelay = addJitter(currentDelay)
|
||||
}
|
||||
|
||||
// Wait with respect to context cancellation or timeout
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return zero, r.ctx.Err()
|
||||
case <-time.After(currentDelay):
|
||||
}
|
||||
}
|
||||
return zero, lastErr
|
||||
}
|
||||
153
vendor/github.com/olekukonko/errors/utils.go
generated
vendored
Normal file
153
vendor/github.com/olekukonko/errors/utils.go
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
// Package errors provides utility functions for error handling, including stack
|
||||
// trace capture and function name extraction.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// captureStack captures a stack trace with the configured depth.
|
||||
// Skip=0 captures the current call site; skips captureStack and its caller (+2 frames); thread-safe via stackPool.
|
||||
func captureStack(skip int) []uintptr {
|
||||
buf := stackPool.Get().([]uintptr)
|
||||
buf = buf[:cap(buf)]
|
||||
|
||||
// +2 to skip captureStack and the immediate caller
|
||||
n := runtime.Callers(skip+2, buf)
|
||||
if n == 0 {
|
||||
stackPool.Put(buf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new slice to return, avoiding direct use of pooled memory
|
||||
stack := make([]uintptr, n)
|
||||
copy(stack, buf[:n])
|
||||
stackPool.Put(buf)
|
||||
|
||||
return stack
|
||||
}
|
||||
|
||||
// min returns the smaller of two integers.
|
||||
// Simple helper for limiting stack trace size or other comparisons.
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// clearMap removes all entries from a map.
|
||||
// Helper function to reset map contents without reallocating.
|
||||
func clearMap(m map[string]interface{}) {
|
||||
for k := range m {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlNull detects if a value represents a SQL NULL type.
|
||||
// Returns true for nil or invalid sql.Null* types (e.g., NullString, NullInt64); false otherwise.
|
||||
func sqlNull(v interface{}) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
switch val := v.(type) {
|
||||
case sql.NullString:
|
||||
return !val.Valid
|
||||
case sql.NullTime:
|
||||
return !val.Valid
|
||||
case sql.NullInt64:
|
||||
return !val.Valid
|
||||
case sql.NullBool:
|
||||
return !val.Valid
|
||||
case sql.NullFloat64:
|
||||
return !val.Valid
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// getFuncName extracts the function name from an interface, typically a function or method.
|
||||
// Returns "unknown" if the input is nil or invalid; trims leading dots from runtime name.
|
||||
func getFuncName(fn interface{}) string {
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
fullName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
|
||||
return strings.TrimPrefix(fullName, ".")
|
||||
}
|
||||
|
||||
// isInternalFrame determines if a stack frame is considered "internal".
|
||||
// Returns true for frames from runtime, reflect, or this package’s subdirectories if FilterInternal is true.
|
||||
func isInternalFrame(frame runtime.Frame) bool {
|
||||
if strings.HasPrefix(frame.Function, "runtime.") || strings.HasPrefix(frame.Function, "reflect.") {
|
||||
return true
|
||||
}
|
||||
|
||||
suffixes := []string{
|
||||
"errors",
|
||||
"utils",
|
||||
"helper",
|
||||
"retry",
|
||||
"multi",
|
||||
}
|
||||
|
||||
file := frame.File
|
||||
for _, v := range suffixes {
|
||||
if strings.Contains(file, fmt.Sprintf("github.com/olekukonko/errors/%s", v)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FormatError returns a formatted string representation of an error.
|
||||
// Includes message, name, context, stack trace, and cause for *Error types; just message for others; "<nil>" if nil.
|
||||
func FormatError(err error) string {
|
||||
if err == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
var sb strings.Builder
|
||||
if e, ok := err.(*Error); ok {
|
||||
sb.WriteString(fmt.Sprintf("Error: %s\n", e.Error()))
|
||||
if e.name != "" {
|
||||
sb.WriteString(fmt.Sprintf("Name: %s\n", e.name))
|
||||
}
|
||||
if ctx := e.Context(); len(ctx) > 0 {
|
||||
sb.WriteString("Context:\n")
|
||||
for k, v := range ctx {
|
||||
sb.WriteString(fmt.Sprintf("\t%s: %v\n", k, v))
|
||||
}
|
||||
}
|
||||
if stack := e.Stack(); len(stack) > 0 {
|
||||
sb.WriteString("Stack Trace:\n")
|
||||
for _, frame := range stack {
|
||||
sb.WriteString(fmt.Sprintf("\t%s\n", frame))
|
||||
}
|
||||
}
|
||||
if e.cause != nil {
|
||||
sb.WriteString(fmt.Sprintf("Caused by: %s\n", FormatError(e.cause)))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Error: %s\n", err.Error()))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Caller returns the file, line, and function name of the caller at the specified skip level.
|
||||
// Skip=0 returns the caller of this function, 1 returns its caller, etc.; returns "unknown" if no caller found.
|
||||
func Caller(skip int) (file string, line int, function string) {
|
||||
configMu.RLock()
|
||||
defer configMu.RUnlock()
|
||||
var pcs [1]uintptr
|
||||
n := runtime.Callers(skip+2, pcs[:]) // +2 skips Caller and its immediate caller
|
||||
if n == 0 {
|
||||
return "", 0, "unknown"
|
||||
}
|
||||
frame, _ := runtime.CallersFrames(pcs[:n]).Next()
|
||||
return frame.File, frame.Line, frame.Function
|
||||
}
|
||||
5
vendor/github.com/olekukonko/ll/.gitignore
generated
vendored
Normal file
5
vendor/github.com/olekukonko/ll/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.idea
|
||||
lab
|
||||
tmp
|
||||
#_*
|
||||
_test/
|
||||
21
vendor/github.com/olekukonko/ll/LICENSE
generated
vendored
Normal file
21
vendor/github.com/olekukonko/ll/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Oleku Konko
|
||||
|
||||
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.
|
||||
359
vendor/github.com/olekukonko/ll/README.md
generated
vendored
Normal file
359
vendor/github.com/olekukonko/ll/README.md
generated
vendored
Normal file
@@ -0,0 +1,359 @@
|
||||
# ll - A Modern Structured Logging Library for Go
|
||||
|
||||
`ll` is a high-performance, production-ready logging library for Go, designed to provide **hierarchical namespaces**, **structured logging**, **middleware pipelines**, **conditional logging**, and support for multiple output formats, including text, JSON, colorized logs, and compatibility with Go’s `slog`. It’s ideal for applications requiring fine-grained log control, extensibility, and scalability.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Hierarchical Namespaces**: Organize logs with fine-grained control over subsystems (e.g., "app/db").
|
||||
- **Structured Logging**: Add key-value metadata for machine-readable logs.
|
||||
- **Middleware Pipeline**: Customize log processing with error-based rejection.
|
||||
- **Conditional Logging**: Optimize performance by skipping unnecessary log operations.
|
||||
- **Multiple Output Formats**: Support for text, JSON, colorized logs, and `slog` integration.
|
||||
- **Debugging Utilities**: Inspect variables (`Dbg`), binary data (`Dump`), and stack traces (`Stack`).
|
||||
- **Thread-Safe**: Built for concurrent use with mutex-protected state.
|
||||
- **Performance Optimized**: Minimal allocations and efficient namespace caching.
|
||||
|
||||
## Installation
|
||||
|
||||
Install `ll` using Go modules:
|
||||
|
||||
```bash
|
||||
go get github.com/olekukonko/ll
|
||||
```
|
||||
|
||||
Ensure you have Go 1.21 or later for optimal compatibility.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Here’s a quick example to start logging with `ll`:
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a logger with namespace "app"
|
||||
logger := ll.New("")
|
||||
|
||||
// enable output
|
||||
logger.Enable()
|
||||
|
||||
// Basic log
|
||||
logger.Info("Welcome") // Output: [app] INFO: Application started
|
||||
|
||||
logger = logger.Namespace("app")
|
||||
|
||||
// Basic log
|
||||
logger.Info("start at :8080") // Output: [app] INFO: Application started
|
||||
|
||||
//Output
|
||||
//INFO: Welcome
|
||||
//[app] INFO: start at :8080
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/ll/lh"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Chaining
|
||||
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
|
||||
|
||||
// Basic log
|
||||
logger.Info("Application started") // Output: [app] INFO: Application started
|
||||
|
||||
// Structured log with fields
|
||||
logger.Fields("user", "alice", "status", 200).Info("User logged in")
|
||||
// Output: [app] INFO: User logged in [user=alice status=200]
|
||||
|
||||
// Conditional log
|
||||
debugMode := false
|
||||
logger.If(debugMode).Debug("Debug info") // No output (debugMode is false)
|
||||
}
|
||||
```
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Hierarchical Namespaces
|
||||
|
||||
Namespaces allow you to organize logs hierarchically, enabling precise control over logging for different parts of your application. This is especially useful for large systems with multiple components.
|
||||
|
||||
**Benefits**:
|
||||
- **Granular Control**: Enable/disable logs for specific subsystems (e.g., "app/db" vs. "app/api").
|
||||
- **Scalability**: Manage log volume in complex applications.
|
||||
- **Readability**: Clear namespace paths improve traceability.
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
|
||||
|
||||
// Child loggers
|
||||
dbLogger := logger.Namespace("db")
|
||||
apiLogger := logger.Namespace("api").Style(lx.NestedPath)
|
||||
|
||||
// Namespace control
|
||||
logger.NamespaceEnable("app/db") // Enable DB logs
|
||||
logger.NamespaceDisable("app/api") // Disable API logs
|
||||
|
||||
dbLogger.Info("Query executed") // Output: [app/db] INFO: Query executed
|
||||
apiLogger.Info("Request received") // No output
|
||||
```
|
||||
|
||||
### 2. Structured Logging
|
||||
|
||||
Add key-value metadata to logs for machine-readable output, making it easier to query and analyze logs in tools like ELK or Grafana.
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
|
||||
|
||||
// Variadic fields
|
||||
logger.Fields("user", "bob", "status", 200).Info("Request completed")
|
||||
// Output: [app] INFO: Request completed [user=bob status=200]
|
||||
|
||||
// Map-based fields
|
||||
logger.Field(map[string]interface{}{"method": "GET"}).Info("Request")
|
||||
// Output: [app] INFO: Request [method=GET]
|
||||
```
|
||||
|
||||
### 3. Middleware Pipeline
|
||||
|
||||
Customize log processing with a middleware pipeline. Middleware functions can enrich, filter, or transform logs, using an error-based rejection mechanism (non-nil errors stop logging).
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
|
||||
|
||||
// Enrich logs with app metadata
|
||||
logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
|
||||
if e.Fields == nil {
|
||||
e.Fields = make(map[string]interface{})
|
||||
}
|
||||
e.Fields["app"] = "myapp"
|
||||
return nil
|
||||
}))
|
||||
|
||||
// Filter low-level logs
|
||||
logger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
|
||||
if e.Level < lx.LevelWarn {
|
||||
return fmt.Errorf("level too low")
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
|
||||
logger.Info("Ignored") // No output (filtered)
|
||||
logger.Warn("Warning") // Output: [app] WARN: Warning [app=myapp]
|
||||
```
|
||||
|
||||
### 4. Conditional Logging
|
||||
|
||||
Optimize performance by skipping expensive log operations when conditions are false, ideal for production environments.
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
logger := ll.New("app").Enable().Handler(lh.NewTextHandler(os.Stdout))
|
||||
|
||||
featureEnabled := true
|
||||
logger.If(featureEnabled).Fields("action", "update").Info("Feature used")
|
||||
// Output: [app] INFO: Feature used [action=update]
|
||||
|
||||
logger.If(false).Info("Ignored") // No output, no processing
|
||||
```
|
||||
|
||||
### 5. Multiple Output Formats
|
||||
|
||||
`ll` supports various output formats, including human-readable text, colorized logs, JSON, and integration with Go’s `slog` package.
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
logger := ll.New("app").Enable()
|
||||
|
||||
// Text output
|
||||
logger.Handler(lh.NewTextHandler(os.Stdout))
|
||||
logger.Info("Text log") // Output: [app] INFO: Text log
|
||||
|
||||
// JSON output
|
||||
logger.Handler(lh.NewJSONHandler(os.Stdout, time.RFC3339Nano))
|
||||
logger.Info("JSON log") // Output: {"timestamp":"...","level":"INFO","message":"JSON log","namespace":"app"}
|
||||
|
||||
// Slog integration
|
||||
slogText := slog.NewTextHandler(os.Stdout, nil)
|
||||
logger.Handler(lh.NewSlogHandler(slogText))
|
||||
logger.Info("Slog log") // Output: level=INFO msg="Slog log" namespace=app class=Text
|
||||
```
|
||||
|
||||
### 6. Debugging Utilities
|
||||
|
||||
`ll` provides powerful tools for debugging, including variable inspection, binary data dumps, and stack traces.
|
||||
|
||||
#### Core Debugging Methods
|
||||
|
||||
1. **Dbg - Contextual Inspection**
|
||||
Inspects variables with file and line context, preserving variable names and handling all Go types.
|
||||
```go
|
||||
x := 42
|
||||
user := struct{ Name string }{"Alice"}
|
||||
ll.Dbg(x) // Output: [file.go:123] x = 42
|
||||
ll.Dbg(user) // Output: [file.go:124] user = [Name:Alice]
|
||||
```
|
||||
|
||||
2. **Dump - Binary Inspection**
|
||||
Displays a hex/ASCII view of data, optimized for strings, bytes, and complex types (with JSON fallback).
|
||||
```go
|
||||
ll.Handler(lh.NewColorizedHandler(os.Stdout))
|
||||
ll.Dump("hello\nworld") // Output: Hex/ASCII dump (see example/dump.png)
|
||||
```
|
||||
|
||||
3. **Stack - Stack Inspection**
|
||||
Logs a stack trace for debugging critical errors.
|
||||
```go
|
||||
ll.Handler(lh.NewColorizedHandler(os.Stdout))
|
||||
ll.Stack("Critical error") // Output: [app] ERROR: Critical error [stack=...] (see example/stack.png)
|
||||
```
|
||||
|
||||
#### Performance Tracking
|
||||
Measure execution time for performance analysis.
|
||||
```go
|
||||
// Automatic measurement
|
||||
defer ll.Measure(func() { time.Sleep(time.Millisecond) })()
|
||||
// Output: [app] INFO: function executed [duration=~1ms]
|
||||
|
||||
// Explicit benchmarking
|
||||
start := time.Now()
|
||||
time.Sleep(time.Millisecond)
|
||||
ll.Benchmark(start) // Output: [app] INFO: benchmark [start=... end=... duration=...]
|
||||
```
|
||||
|
||||
**Performance Notes**:
|
||||
- `Dbg` calls are disabled at compile-time when not enabled.
|
||||
- `Dump` optimizes for primitive types, strings, and bytes with zero-copy paths.
|
||||
- Stack traces are configurable via `StackSize`.
|
||||
|
||||
## Real-World Example: Web Server
|
||||
|
||||
A practical example of using `ll` in a web server with structured logging, middleware, and `slog` integration:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/ll/lh"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize logger with slog handler
|
||||
slogHandler := slog.NewJSONHandler(os.Stdout, nil)
|
||||
logger := ll.New("server").Enable().Handler(lh.NewSlogHandler(slogHandler))
|
||||
|
||||
// HTTP child logger
|
||||
httpLogger := logger.Namespace("http").Style(lx.NestedPath)
|
||||
|
||||
// Middleware for request ID
|
||||
httpLogger.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
|
||||
if e.Fields == nil {
|
||||
e.Fields = make(map[string]interface{})
|
||||
}
|
||||
e.Fields["request_id"] = "req-" + time.Now().String()
|
||||
return nil
|
||||
}))
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
httpLogger.Fields("method", r.Method, "path", r.URL.Path).Info("Request received")
|
||||
w.Write([]byte("Hello, world!"))
|
||||
httpLogger.Fields("duration_ms", time.Since(start).Milliseconds()).Info("Request completed")
|
||||
})
|
||||
|
||||
logger.Info("Starting server on :8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
```
|
||||
|
||||
**Sample Output (JSON via slog)**:
|
||||
```json
|
||||
{"level":"INFO","msg":"Starting server on :8080","namespace":"server"}
|
||||
{"level":"INFO","msg":"Request received","namespace":"server/http","class":"Text","method":"GET","path":"/","request_id":"req-..."}
|
||||
{"level":"INFO","msg":"Request completed","namespace":"server/http","class":"Text","duration_ms":1,"request_id":"req-..."}
|
||||
```
|
||||
|
||||
## Why Choose `ll`?
|
||||
|
||||
- **Granular Control**: Hierarchical namespaces for precise log management.
|
||||
- **Performance**: Conditional logging and optimized concatenation reduce overhead.
|
||||
- **Extensibility**: Middleware pipeline for custom log processing.
|
||||
- **Structured Output**: Machine-readable logs with key-value metadata.
|
||||
- **Flexible Formats**: Text, JSON, colorized, and `slog` support.
|
||||
- **Debugging Power**: Advanced tools like `Dbg`, `Dump`, and `Stack` for deep inspection.
|
||||
- **Thread-Safe**: Safe for concurrent use in high-throughput applications.
|
||||
|
||||
## Comparison with Other Libraries
|
||||
|
||||
| Feature | `ll` | `log` (stdlib) | `slog` (stdlib) | `zap` |
|
||||
|--------------------------|--------------------------|----------------|-----------------|-------------------|
|
||||
| Hierarchical Namespaces | ✅ | ❌ | ❌ | ❌ |
|
||||
| Structured Logging | ✅ (Fields, Context) | ❌ | ✅ | ✅ |
|
||||
| Middleware Pipeline | ✅ | ❌ | ❌ | ✅ (limited) |
|
||||
| Conditional Logging | ✅ (If, IfOne, IfAny) | ❌ | ❌ | ❌ |
|
||||
| Slog Compatibility | ✅ | ❌ | ✅ (native) | ❌ |
|
||||
| Debugging (Dbg, Dump) | ✅ | ❌ | ❌ | ❌ |
|
||||
| Performance (disabled logs) | High (conditional) | Low | Medium | High |
|
||||
| Output Formats | Text, JSON, Color, Slog | Text | Text, JSON | JSON, Text |
|
||||
|
||||
## Benchmarks
|
||||
|
||||
`ll` is optimized for performance, particularly for disabled logs and structured logging:
|
||||
- **Disabled Logs**: 30% faster than `slog` due to efficient conditional checks.
|
||||
- **Structured Logging**: 2x faster than `log` with minimal allocations.
|
||||
- **Namespace Caching**: Reduces overhead for hierarchical lookups.
|
||||
|
||||
See `ll_bench_test.go` for detailed benchmarks on namespace creation, cloning, and field building.
|
||||
|
||||
## Testing and Stability
|
||||
|
||||
The `ll` library includes a comprehensive test suite (`ll_test.go`) covering:
|
||||
- Logger configuration, namespaces, and conditional logging.
|
||||
- Middleware, rate limiting, and sampling.
|
||||
- Handler output formats (text, JSON, slog).
|
||||
- Debugging utilities (`Dbg`, `Dump`, `Stack`).
|
||||
|
||||
Recent improvements:
|
||||
- Fixed sampling middleware for reliable behavior at edge cases (0.0 and 1.0 rates).
|
||||
- Enhanced documentation across `conditional.go`, `field.go`, `global.go`, `ll.go`, `lx.go`, and `ns.go`.
|
||||
- Added `slog` compatibility via `lh.SlogHandler`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! To contribute:
|
||||
1. Fork the repository: `github.com/olekukonko/ll`.
|
||||
2. Create a feature branch: `git checkout -b feature/your-feature`.
|
||||
3. Commit changes: `git commit -m "Add your feature"`.
|
||||
4. Push to the branch: `git push origin feature/your-feature`.
|
||||
5. Open a pull request with a clear description.
|
||||
|
||||
Please include tests in `ll_test.go` and update documentation as needed. Follow the Go coding style and run `go test ./...` before submitting.
|
||||
|
||||
## License
|
||||
|
||||
`ll` is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Resources
|
||||
|
||||
- **Source Code**: [github.com/olekukonko/ll](https://github.com/olekukonko/ll)
|
||||
- **Issue Tracker**: [github.com/olekukonko/ll/issues](https://github.com/olekukonko/ll/issues)
|
||||
- **GoDoc**: [pkg.go.dev/github.com/olekukonko/ll](https://pkg.go.dev/github.com/olekukonko/ll)
|
||||
421
vendor/github.com/olekukonko/ll/concat.go
generated
vendored
Normal file
421
vendor/github.com/olekukonko/ll/concat.go
generated
vendored
Normal file
@@ -0,0 +1,421 @@
|
||||
package ll
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRecursionDepth = 20 // Maximum depth for recursive type handling to prevent stack overflow
|
||||
nilString = "<nil>" // String representation for nil values
|
||||
unexportedString = "<?>" // String representation for unexported fields
|
||||
)
|
||||
|
||||
// Concat efficiently concatenates values without a separator using the default logger.
|
||||
// It converts each argument to a string and joins them directly, optimizing for performance
|
||||
// in logging scenarios. Thread-safe as it does not modify shared state.
|
||||
// Example:
|
||||
//
|
||||
// msg := ll.Concat("Hello", 42, true) // Returns "Hello42true"
|
||||
func Concat(args ...any) string {
|
||||
return concat(args...)
|
||||
}
|
||||
|
||||
// ConcatSpaced concatenates values with a space separator using the default logger.
|
||||
// It converts each argument to a string and joins them with spaces, suitable for log message
|
||||
// formatting. Thread-safe as it does not modify shared state.
|
||||
// Example:
|
||||
//
|
||||
// msg := ll.ConcatSpaced("Hello", 42, true) // Returns "Hello 42 true"
|
||||
func ConcatSpaced(args ...any) string {
|
||||
return concatSpaced(args...)
|
||||
}
|
||||
|
||||
// ConcatAll concatenates elements with a separator, prefix, and suffix using the default logger.
|
||||
// It combines before, main, and after arguments with the specified separator, optimizing memory
|
||||
// allocation for logging. Thread-safe as it does not modify shared state.
|
||||
// Example:
|
||||
//
|
||||
// msg := ll.ConcatAll(",", []any{"prefix"}, []any{"suffix"}, "main")
|
||||
// // Returns "prefix,main,suffix"
|
||||
func ConcatAll(sep string, before, after []any, args ...any) string {
|
||||
return concatenate(sep, before, after, args...)
|
||||
}
|
||||
|
||||
// concat efficiently concatenates values without a separator.
|
||||
// It converts each argument to a string and joins them directly, optimizing for performance
|
||||
// in logging scenarios. Used internally by Concat and other logging functions.
|
||||
// Example:
|
||||
//
|
||||
// msg := concat("Hello", 42, true) // Returns "Hello42true"
|
||||
func concat(args ...any) string {
|
||||
return concatWith("", args...)
|
||||
}
|
||||
|
||||
// concatSpaced concatenates values with a space separator.
|
||||
// It converts each argument to a string and joins them with spaces, suitable for formatting
|
||||
// log messages. Used internally by ConcatSpaced.
|
||||
// Example:
|
||||
//
|
||||
// msg := concatSpaced("Hello", 42, true) // Returns "Hello 42 true"
|
||||
func concatSpaced(args ...any) string {
|
||||
return concatWith(lx.Space, args...)
|
||||
}
|
||||
|
||||
// concatWith concatenates values with a specified separator using optimized type handling.
|
||||
// It builds a string from arguments, handling various types efficiently (strings, numbers,
|
||||
// structs, etc.), and is used by concat and concatSpaced for log message construction.
|
||||
// Thread-safe as it does not modify shared state.
|
||||
// Example:
|
||||
//
|
||||
// msg := concatWith(",", "Hello", 42, true) // Returns "Hello,42,true"
|
||||
func concatWith(sep string, args ...any) string {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return concatToString(args[0])
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(concatEstimateArgs(sep, args))
|
||||
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
b.WriteString(sep)
|
||||
}
|
||||
concatWriteValue(&b, arg, 0)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// concatenate concatenates elements with separators, prefixes, and suffixes efficiently.
|
||||
// It combines before, main, and after arguments with the specified separator, optimizing
|
||||
// memory allocation for complex log message formatting. Used internally by ConcatAll.
|
||||
// Example:
|
||||
//
|
||||
// msg := concatenate(",", []any{"prefix"}, []any{"suffix"}, "main")
|
||||
// // Returns "prefix,main,suffix"
|
||||
func concatenate(sep string, before []any, after []any, args ...any) string {
|
||||
totalLen := len(before) + len(after) + len(args)
|
||||
switch totalLen {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
switch {
|
||||
case len(before) > 0:
|
||||
return concatToString(before[0])
|
||||
case len(args) > 0:
|
||||
return concatToString(args[0])
|
||||
default:
|
||||
return concatToString(after[0])
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(concatEstimateTotal(sep, before, after, args))
|
||||
|
||||
// Write before elements
|
||||
concatWriteGroup(&b, sep, before)
|
||||
|
||||
// Write main arguments
|
||||
if len(before) > 0 && len(args) > 0 {
|
||||
b.WriteString(sep)
|
||||
}
|
||||
concatWriteGroup(&b, sep, args)
|
||||
|
||||
// Write after elements
|
||||
if len(after) > 0 && (len(before) > 0 || len(args) > 0) {
|
||||
b.WriteString(sep)
|
||||
}
|
||||
concatWriteGroup(&b, sep, after)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// concatWriteGroup writes a group of arguments to a strings.Builder with a separator.
|
||||
// It handles each argument by converting it to a string, used internally by concatenate
|
||||
// to process before, main, or after groups in log message construction.
|
||||
// Example:
|
||||
//
|
||||
// var b strings.Builder
|
||||
// concatWriteGroup(&b, ",", []any{"a", 42}) // Writes "a,42" to b
|
||||
func concatWriteGroup(b *strings.Builder, sep string, group []any) {
|
||||
for i, arg := range group {
|
||||
if i > 0 {
|
||||
b.WriteString(sep)
|
||||
}
|
||||
concatWriteValue(b, arg, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// concatToString converts a single argument to a string efficiently.
|
||||
// It handles common types (string, []byte, fmt.Stringer) with minimal overhead and falls
|
||||
// back to fmt.Sprint for other types. Used internally by concat and concatenate.
|
||||
// Example:
|
||||
//
|
||||
// s := concatToString("Hello") // Returns "Hello"
|
||||
// s := concatToString([]byte{65, 66}) // Returns "AB"
|
||||
func concatToString(arg any) string {
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return *(*string)(unsafe.Pointer(&v))
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
case error:
|
||||
return v.Error()
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
|
||||
// concatEstimateTotal estimates the total string length for concatenate.
|
||||
// It calculates the expected size of the concatenated string, including before, main, and
|
||||
// after arguments with separators, to preallocate the strings.Builder capacity.
|
||||
// Example:
|
||||
//
|
||||
// size := concatEstimateTotal(",", []any{"prefix"}, []any{"suffix"}, "main")
|
||||
// // Returns estimated length for "prefix,main,suffix"
|
||||
func concatEstimateTotal(sep string, before, after, args []any) int {
|
||||
size := 0
|
||||
if len(before) > 0 {
|
||||
size += concatEstimateArgs(sep, before)
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if size > 0 {
|
||||
size += len(sep)
|
||||
}
|
||||
size += concatEstimateArgs(sep, args)
|
||||
}
|
||||
if len(after) > 0 {
|
||||
if size > 0 {
|
||||
size += len(sep)
|
||||
}
|
||||
size += concatEstimateArgs(sep, after)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// concatEstimateArgs estimates the string length for a group of arguments.
|
||||
// It sums the estimated sizes of each argument plus separators, used by concatEstimateTotal
|
||||
// and concatWith to optimize memory allocation for log message construction.
|
||||
// Example:
|
||||
//
|
||||
// size := concatEstimateArgs(",", []any{"hello", 42}) // Returns estimated length for "hello,42"
|
||||
func concatEstimateArgs(sep string, args []any) int {
|
||||
if len(args) == 0 {
|
||||
return 0
|
||||
}
|
||||
size := len(sep) * (len(args) - 1)
|
||||
for _, arg := range args {
|
||||
size += concatEstimateSize(arg)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// concatEstimateSize estimates the string length for a single argument.
|
||||
// It provides size estimates for various types (strings, numbers, booleans, etc.) to
|
||||
// optimize strings.Builder capacity allocation in logging functions.
|
||||
// Example:
|
||||
//
|
||||
// size := concatEstimateSize("hello") // Returns 5
|
||||
// size := concatEstimateSize(42) // Returns ~2
|
||||
func concatEstimateSize(arg any) int {
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
return len(v)
|
||||
case []byte:
|
||||
return len(v)
|
||||
case int:
|
||||
return concatNumLen(int64(v))
|
||||
case int64:
|
||||
return concatNumLen(v)
|
||||
case int32:
|
||||
return concatNumLen(int64(v))
|
||||
case int16:
|
||||
return concatNumLen(int64(v))
|
||||
case int8:
|
||||
return concatNumLen(int64(v))
|
||||
case uint:
|
||||
return concatNumLen(uint64(v))
|
||||
case uint64:
|
||||
return concatNumLen(v)
|
||||
case uint32:
|
||||
return concatNumLen(uint64(v))
|
||||
case uint16:
|
||||
return concatNumLen(uint64(v))
|
||||
case uint8:
|
||||
return concatNumLen(uint64(v))
|
||||
case float64:
|
||||
return 24 // Max digits for float64
|
||||
case float32:
|
||||
return 16 // Max digits for float32
|
||||
case bool:
|
||||
if v {
|
||||
return 4 // "true"
|
||||
}
|
||||
return 5 // "false"
|
||||
case fmt.Stringer:
|
||||
return 16 // Conservative estimate
|
||||
default:
|
||||
return 16 // Default estimate
|
||||
}
|
||||
}
|
||||
|
||||
// concatNumLen estimates the string length for a signed or unsigned integer.
|
||||
// It returns a conservative estimate (20 digits) for int64 or uint64 values, including
|
||||
// a sign for negative numbers, used by concatEstimateSize for memory allocation.
|
||||
// Example:
|
||||
//
|
||||
// size := concatNumLen(int64(-123)) // Returns 20
|
||||
// size := concatNumLen(uint64(123)) // Returns 20
|
||||
func concatNumLen[T int64 | uint64](v T) int {
|
||||
if v < 0 {
|
||||
return 20 // Max digits for int64 + sign
|
||||
}
|
||||
return 20 // Max digits for uint64
|
||||
}
|
||||
|
||||
// concatWriteValue writes a formatted value to a strings.Builder with recursion depth tracking.
|
||||
// It handles various types (strings, numbers, structs, slices, etc.) and prevents infinite
|
||||
// recursion by limiting depth. Used internally by concatWith and concatWriteGroup for log
|
||||
// message formatting.
|
||||
// Example:
|
||||
//
|
||||
// var b strings.Builder
|
||||
// concatWriteValue(&b, "hello", 0) // Writes "hello" to b
|
||||
// concatWriteValue(&b, []int{1, 2}, 0) // Writes "[1,2]" to b
|
||||
func concatWriteValue(b *strings.Builder, arg any, depth int) {
|
||||
if depth > maxRecursionDepth {
|
||||
b.WriteString("...")
|
||||
return
|
||||
}
|
||||
|
||||
if arg == nil {
|
||||
b.WriteString(nilString)
|
||||
return
|
||||
}
|
||||
|
||||
if s, ok := arg.(fmt.Stringer); ok {
|
||||
b.WriteString(s.String())
|
||||
return
|
||||
}
|
||||
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
b.WriteString(v)
|
||||
case []byte:
|
||||
b.Write(v)
|
||||
case int:
|
||||
b.WriteString(strconv.FormatInt(int64(v), 10))
|
||||
case int64:
|
||||
b.WriteString(strconv.FormatInt(v, 10))
|
||||
case int32:
|
||||
b.WriteString(strconv.FormatInt(int64(v), 10))
|
||||
case int16:
|
||||
b.WriteString(strconv.FormatInt(int64(v), 10))
|
||||
case int8:
|
||||
b.WriteString(strconv.FormatInt(int64(v), 10))
|
||||
case uint:
|
||||
b.WriteString(strconv.FormatUint(uint64(v), 10))
|
||||
case uint64:
|
||||
b.WriteString(strconv.FormatUint(v, 10))
|
||||
case uint32:
|
||||
b.WriteString(strconv.FormatUint(uint64(v), 10))
|
||||
case uint16:
|
||||
b.WriteString(strconv.FormatUint(uint64(v), 10))
|
||||
case uint8:
|
||||
b.WriteString(strconv.FormatUint(uint64(v), 10))
|
||||
case float64:
|
||||
b.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
|
||||
case float32:
|
||||
b.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
|
||||
case bool:
|
||||
if v {
|
||||
b.WriteString("true")
|
||||
} else {
|
||||
b.WriteString("false")
|
||||
}
|
||||
default:
|
||||
val := reflect.ValueOf(arg)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
b.WriteString(nilString)
|
||||
return
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
concatFormatSlice(b, val, depth)
|
||||
case reflect.Struct:
|
||||
concatFormatStruct(b, val, depth)
|
||||
default:
|
||||
fmt.Fprint(b, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// concatFormatSlice formats a slice or array for logging.
|
||||
// It writes the elements in a bracketed, comma-separated format, handling nested types
|
||||
// recursively with depth tracking. Used internally by concatWriteValue for log message formatting.
|
||||
// Example:
|
||||
//
|
||||
// var b strings.Builder
|
||||
// val := reflect.ValueOf([]int{1, 2})
|
||||
// concatFormatSlice(&b, val, 0) // Writes "[1,2]" to b
|
||||
func concatFormatSlice(b *strings.Builder, val reflect.Value, depth int) {
|
||||
b.WriteByte('[')
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
if i > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
concatWriteValue(b, val.Index(i).Interface(), depth+1)
|
||||
}
|
||||
b.WriteByte(']')
|
||||
}
|
||||
|
||||
// concatFormatStruct formats a struct for logging.
|
||||
// It writes the struct’s exported fields in a bracketed, name:value format, handling nested
|
||||
// types recursively with depth tracking. Unexported fields are represented as "<?>".
|
||||
// Used internally by concatWriteValue for log message formatting.
|
||||
// Example:
|
||||
//
|
||||
// var b strings.Builder
|
||||
// val := reflect.ValueOf(struct{ Name string }{Name: "test"})
|
||||
// concatFormatStruct(&b, val, 0) // Writes "[Name:test]" to b
|
||||
func concatFormatStruct(b *strings.Builder, val reflect.Value, depth int) {
|
||||
typ := val.Type()
|
||||
b.WriteByte('[')
|
||||
|
||||
first := true
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
fieldValue := val.Field(i)
|
||||
|
||||
if !first {
|
||||
b.WriteString("; ")
|
||||
}
|
||||
first = false
|
||||
|
||||
b.WriteString(field.Name)
|
||||
b.WriteByte(':')
|
||||
|
||||
if !fieldValue.CanInterface() {
|
||||
b.WriteString(unexportedString)
|
||||
continue
|
||||
}
|
||||
|
||||
concatWriteValue(b, fieldValue.Interface(), depth+1)
|
||||
}
|
||||
|
||||
b.WriteByte(']')
|
||||
}
|
||||
340
vendor/github.com/olekukonko/ll/conditional.go
generated
vendored
Normal file
340
vendor/github.com/olekukonko/ll/conditional.go
generated
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
package ll
|
||||
|
||||
// Conditional enables conditional logging based on a boolean condition.
|
||||
// It wraps a logger with a condition that determines whether logging operations are executed,
|
||||
// optimizing performance by skipping expensive operations (e.g., field computation, message formatting)
|
||||
// when the condition is false. The struct supports fluent chaining for adding fields and logging.
|
||||
type Conditional struct {
|
||||
logger *Logger // Associated logger instance for logging operations
|
||||
condition bool // Whether logging is allowed (true to log, false to skip)
|
||||
}
|
||||
|
||||
// If creates a conditional logger that logs only if the condition is true.
|
||||
// It returns a Conditional struct that wraps the logger, enabling conditional logging methods.
|
||||
// This method is typically called on a Logger instance to start a conditional chain.
|
||||
// Thread-safe via the underlying logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Info("Logged") // Output: [app] INFO: Logged
|
||||
// logger.If(false).Info("Ignored") // No output
|
||||
func (l *Logger) If(condition bool) *Conditional {
|
||||
return &Conditional{logger: l, condition: condition}
|
||||
}
|
||||
|
||||
// IfOne creates a conditional logger that logs only if all conditions are true.
|
||||
// It evaluates a variadic list of boolean conditions, setting the condition to true only if
|
||||
// all are true (logical AND). Returns a new Conditional with the result. Thread-safe via the
|
||||
// underlying logger.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.IfOne(true, true).Info("Logged") // Output: [app] INFO: Logged
|
||||
// logger.IfOne(true, false).Info("Ignored") // No output
|
||||
func (cl *Conditional) IfOne(conditions ...bool) *Conditional {
|
||||
result := true
|
||||
// Check each condition; set result to false if any is false
|
||||
for _, cond := range conditions {
|
||||
if !cond {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return &Conditional{logger: cl.logger, condition: result}
|
||||
}
|
||||
|
||||
// IfAny creates a conditional logger that logs only if at least one condition is true.
|
||||
// It evaluates a variadic list of boolean conditions, setting the condition to true if any
|
||||
// is true (logical OR). Returns a new Conditional with the result. Thread-safe via the
|
||||
// underlying logger.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.IfAny(false, true).Info("Logged") // Output: [app] INFO: Logged
|
||||
// logger.IfAny(false, false).Info("Ignored") // No output
|
||||
func (cl *Conditional) IfAny(conditions ...bool) *Conditional {
|
||||
result := false
|
||||
// Check each condition; set result to true if any is true
|
||||
for _, cond := range conditions {
|
||||
if cond {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return &Conditional{logger: cl.logger, condition: result}
|
||||
}
|
||||
|
||||
// Fields starts a fluent chain for adding fields using variadic key-value pairs, if the condition is true.
|
||||
// It returns a FieldBuilder to attach fields, skipping field processing if the condition is false
|
||||
// to optimize performance. Thread-safe via the FieldBuilder’s logger.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Fields("user", "alice").Info("Logged") // Output: [app] INFO: Logged [user=alice]
|
||||
// logger.If(false).Fields("user", "alice").Info("Ignored") // No output, no field processing
|
||||
func (cl *Conditional) Fields(pairs ...any) *FieldBuilder {
|
||||
// Skip field processing if condition is false
|
||||
if !cl.condition {
|
||||
return &FieldBuilder{logger: cl.logger, fields: nil}
|
||||
}
|
||||
// Delegate to logger’s Fields method
|
||||
return cl.logger.Fields(pairs...)
|
||||
}
|
||||
|
||||
// Field starts a fluent chain for adding fields from a map, if the condition is true.
|
||||
// It returns a FieldBuilder to attach fields from a map, skipping processing if the condition
|
||||
// is false. Thread-safe via the FieldBuilder’s logger.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Field(map[string]interface{}{"user": "alice"}).Info("Logged") // Output: [app] INFO: Logged [user=alice]
|
||||
// logger.If(false).Field(map[string]interface{}{"user": "alice"}).Info("Ignored") // No output
|
||||
func (cl *Conditional) Field(fields map[string]interface{}) *FieldBuilder {
|
||||
// Skip field processing if condition is false
|
||||
if !cl.condition {
|
||||
return &FieldBuilder{logger: cl.logger, fields: nil}
|
||||
}
|
||||
// Delegate to logger’s Field method
|
||||
return cl.logger.Field(fields)
|
||||
}
|
||||
|
||||
// Info logs a message at Info level with variadic arguments if the condition is true.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s Info method if the
|
||||
// condition is true. Skips processing if false, optimizing performance. Thread-safe via the
|
||||
// logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Info("Action", "started") // Output: [app] INFO: Action started
|
||||
// logger.If(false).Info("Action", "ignored") // No output
|
||||
func (cl *Conditional) Info(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Info method
|
||||
cl.logger.Info(args...)
|
||||
}
|
||||
|
||||
// Infof logs a message at Info level with a format string if the condition is true.
|
||||
// It formats the message using the provided format string and arguments, delegating to the
|
||||
// logger’s Infof method if the condition is true. Skips processing if false, optimizing performance.
|
||||
// Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Infof("Action %s", "started") // Output: [app] INFO: Action started
|
||||
// logger.If(false).Infof("Action %s", "ignored") // No output
|
||||
func (cl *Conditional) Infof(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Infof method
|
||||
cl.logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs a message at Debug level with variadic arguments if the condition is true.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s Debug method if the
|
||||
// condition is true. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable().Level(lx.LevelDebug)
|
||||
// logger.If(true).Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode
|
||||
// logger.If(false).Debug("Debugging", "ignored") // No output
|
||||
func (cl *Conditional) Debug(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Debug method
|
||||
cl.logger.Debug(args...)
|
||||
}
|
||||
|
||||
// Debugf logs a message at Debug level with a format string if the condition is true.
|
||||
// It formats the message and delegates to the logger’s Debugf method if the condition is true.
|
||||
// Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable().Level(lx.LevelDebug)
|
||||
// logger.If(true).Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode
|
||||
// logger.If(false).Debugf("Debug %s", "ignored") // No output
|
||||
func (cl *Conditional) Debugf(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Debugf method
|
||||
cl.logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs a message at Warn level with variadic arguments if the condition is true.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s Warn method if the
|
||||
// condition is true. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Warn("Warning", "issued") // Output: [app] WARN: Warning issued
|
||||
// logger.If(false).Warn("Warning", "ignored") // No output
|
||||
func (cl *Conditional) Warn(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Warn method
|
||||
cl.logger.Warn(args...)
|
||||
}
|
||||
|
||||
// Warnf logs a message at Warn level with a format string if the condition is true.
|
||||
// It formats the message and delegates to the logger’s Warnf method if the condition is true.
|
||||
// Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued
|
||||
// logger.If(false).Warnf("Warning %s", "ignored") // No output
|
||||
func (cl *Conditional) Warnf(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Warnf method
|
||||
cl.logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error logs a message at Error level with variadic arguments if the condition is true.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s Error method if the
|
||||
// condition is true. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Error("Error", "occurred") // Output: [app] ERROR: Error occurred
|
||||
// logger.If(false).Error("Error", "ignored") // No output
|
||||
func (cl *Conditional) Error(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Error method
|
||||
cl.logger.Error(args...)
|
||||
}
|
||||
|
||||
// Errorf logs a message at Error level with a format string if the condition is true.
|
||||
// It formats the message and delegates to the logger’s Errorf method if the condition is true.
|
||||
// Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred
|
||||
// logger.If(false).Errorf("Error %s", "ignored") // No output
|
||||
func (cl *Conditional) Errorf(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Errorf method
|
||||
cl.logger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Stack logs a message at Error level with a stack trace and variadic arguments if the condition is true.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s Stack method if the
|
||||
// condition is true. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Stack("Critical", "error") // Output: [app] ERROR: Critical error [stack=...]
|
||||
// logger.If(false).Stack("Critical", "ignored") // No output
|
||||
func (cl *Conditional) Stack(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Stack method
|
||||
cl.logger.Stack(args...)
|
||||
}
|
||||
|
||||
// Stackf logs a message at Error level with a stack trace and a format string if the condition is true.
|
||||
// It formats the message and delegates to the logger’s Stackf method if the condition is true.
|
||||
// Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [stack=...]
|
||||
// logger.If(false).Stackf("Critical %s", "ignored") // No output
|
||||
func (cl *Conditional) Stackf(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Stackf method
|
||||
cl.logger.Stackf(format, args...)
|
||||
}
|
||||
|
||||
// Fatal logs a message at Error level with a stack trace and variadic arguments if the condition is true,
|
||||
// then exits. It concatenates the arguments with spaces and delegates to the logger’s Fatal method
|
||||
// if the condition is true, terminating the program with exit code 1. Skips processing if false.
|
||||
// Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits
|
||||
// logger.If(false).Fatal("Fatal", "ignored") // No output, no exit
|
||||
func (cl *Conditional) Fatal(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Fatal method
|
||||
cl.logger.Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted message at Error level with a stack trace if the condition is true, then exits.
|
||||
// It formats the message and delegates to the logger’s Fatalf method if the condition is true,
|
||||
// terminating the program with exit code 1. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [stack=...], then exits
|
||||
// logger.If(false).Fatalf("Fatal %s", "ignored") // No output, no exit
|
||||
func (cl *Conditional) Fatalf(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Fatalf method
|
||||
cl.logger.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// Panic logs a message at Error level with a stack trace and variadic arguments if the condition is true,
|
||||
// then panics. It concatenates the arguments with spaces and delegates to the logger’s Panic method
|
||||
// if the condition is true, triggering a panic. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Panic("Panic", "error") // Output: [app] ERROR: Panic error [stack=...], then panics
|
||||
// logger.If(false).Panic("Panic", "ignored") // No output, no panic
|
||||
func (cl *Conditional) Panic(args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Panic method
|
||||
cl.logger.Panic(args...)
|
||||
}
|
||||
|
||||
// Panicf logs a formatted message at Error level with a stack trace if the condition is true, then panics.
|
||||
// It formats the message and delegates to the logger’s Panicf method if the condition is true,
|
||||
// triggering a panic. Skips processing if false. Thread-safe via the logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.If(true).Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [stack=...], then panics
|
||||
// logger.If(false).Panicf("Panic %s", "ignored") // No output, no panic
|
||||
func (cl *Conditional) Panicf(format string, args ...any) {
|
||||
// Skip logging if condition is false
|
||||
if !cl.condition {
|
||||
return
|
||||
}
|
||||
// Delegate to logger’s Panicf method
|
||||
cl.logger.Panicf(format, args...)
|
||||
}
|
||||
374
vendor/github.com/olekukonko/ll/field.go
generated
vendored
Normal file
374
vendor/github.com/olekukonko/ll/field.go
generated
vendored
Normal file
@@ -0,0 +1,374 @@
|
||||
package ll
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FieldBuilder enables fluent addition of fields before logging.
|
||||
// It acts as a builder pattern to attach key-value pairs (fields) to log entries,
|
||||
// supporting structured logging with metadata. The builder allows chaining to add fields
|
||||
// and log messages at various levels (Info, Debug, Warn, Error, etc.) in a single expression.
|
||||
type FieldBuilder struct {
|
||||
logger *Logger // Associated logger instance for logging operations
|
||||
fields map[string]interface{} // Fields to include in the log entry as key-value pairs
|
||||
}
|
||||
|
||||
// Logger creates a new logger with the builder’s fields embedded in its context.
|
||||
// It clones the parent logger and copies the builder’s fields into the new logger’s context,
|
||||
// enabling persistent field inclusion in subsequent logs. This method supports fluent chaining
|
||||
// after Fields or Field calls.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// newLogger := logger.Fields("user", "alice").Logger()
|
||||
// newLogger.Info("Action") // Output: [app] INFO: Action [user=alice]
|
||||
func (fb *FieldBuilder) Logger() *Logger {
|
||||
// Clone the parent logger to preserve its configuration
|
||||
newLogger := fb.logger.Clone()
|
||||
// Initialize a new context map to avoid modifying the parent’s context
|
||||
newLogger.context = make(map[string]interface{})
|
||||
// Copy builder’s fields into the new logger’s context
|
||||
for k, v := range fb.fields {
|
||||
newLogger.context[k] = v
|
||||
}
|
||||
return newLogger
|
||||
}
|
||||
|
||||
// Info logs a message at Info level with the builder’s fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s log method,
|
||||
// returning early if fields are nil. This method is used for informational messages.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Info("Action", "started") // Output: [app] INFO: Action started [user=alice]
|
||||
func (fb *FieldBuilder) Info(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Info level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelInfo, lx.ClassText, concatSpaced(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Infof logs a message at Info level with the builder’s fields.
|
||||
// It formats the message using the provided format string and arguments, then delegates
|
||||
// to the logger’s internal log method. If fields are nil, it returns early to avoid logging.
|
||||
// This method is part of the fluent API, typically called after adding fields.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Infof("Action %s", "started") // Output: [app] INFO: Action started [user=alice]
|
||||
func (fb *FieldBuilder) Infof(format string, args ...any) {
|
||||
// Skip logging if fields are nil to prevent invalid log entries
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message using the provided arguments
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Info level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelInfo, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Debug logs a message at Debug level with the builder’s fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s log method,
|
||||
// returning early if fields are nil. This method is used for debugging information.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Debug("Debugging", "mode") // Output: [app] DEBUG: Debugging mode [user=alice]
|
||||
func (fb *FieldBuilder) Debug(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Debug level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelDebug, lx.ClassText, concatSpaced(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Debugf logs a message at Debug level with the builder’s fields.
|
||||
// It formats the message and delegates to the logger’s log method, returning early if
|
||||
// fields are nil. This method is used for debugging information that may be disabled in
|
||||
// production environments.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Debugf("Debug %s", "mode") // Output: [app] DEBUG: Debug mode [user=alice]
|
||||
func (fb *FieldBuilder) Debugf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Debug level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelDebug, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Warn logs a message at Warn level with the builder’s fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s log method,
|
||||
// returning early if fields are nil. This method is used for warning conditions.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Warn("Warning", "issued") // Output: [app] WARN: Warning issued [user=alice]
|
||||
func (fb *FieldBuilder) Warn(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Warn level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelWarn, lx.ClassText, concatSpaced(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Warnf logs a message at Warn level with the builder’s fields.
|
||||
// It formats the message and delegates to the logger’s log method, returning early if
|
||||
// fields are nil. This method is used for warning conditions that do not halt execution.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Warnf("Warning %s", "issued") // Output: [app] WARN: Warning issued [user=alice]
|
||||
func (fb *FieldBuilder) Warnf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Warn level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelWarn, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Error logs a message at Error level with the builder’s fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s log method,
|
||||
// returning early if fields are nil. This method is used for error conditions.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Error("Error", "occurred") // Output: [app] ERROR: Error occurred [user=alice]
|
||||
func (fb *FieldBuilder) Error(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Error level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, false)
|
||||
}
|
||||
|
||||
// Errorf logs a message at Error level with the builder’s fields.
|
||||
// It formats the message and delegates to the logger’s log method, returning early if
|
||||
// fields are nil. This method is used for error conditions that may require attention.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Errorf("Error %s", "occurred") // Output: [app] ERROR: Error occurred [user=alice]
|
||||
func (fb *FieldBuilder) Errorf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Error level with the builder’s fields, no stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, false)
|
||||
}
|
||||
|
||||
// Stack logs a message at Error level with a stack trace and the builder’s fields.
|
||||
// It concatenates the arguments with spaces and delegates to the logger’s log method,
|
||||
// returning early if fields are nil. This method is useful for debugging critical errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Stack("Critical", "error") // Output: [app] ERROR: Critical error [user=alice stack=...]
|
||||
func (fb *FieldBuilder) Stack(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Log at Error level with the builder’s fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, concatSpaced(args...), fb.fields, true)
|
||||
}
|
||||
|
||||
// Stackf logs a message at Error level with a stack trace and the builder’s fields.
|
||||
// It formats the message and delegates to the logger’s log method, returning early if
|
||||
// fields are nil. This method is useful for debugging critical errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Stackf("Critical %s", "error") // Output: [app] ERROR: Critical error [user=alice stack=...]
|
||||
func (fb *FieldBuilder) Stackf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
// Log at Error level with the builder’s fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true)
|
||||
}
|
||||
|
||||
// Fatal logs a message at Error level with a stack trace and the builder’s fields, then exits.
|
||||
// It constructs the message from variadic arguments, logs it with a stack trace, and terminates
|
||||
// the program with exit code 1. Returns early if fields are nil. This method is used for
|
||||
// unrecoverable errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Fatal("Fatal", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits
|
||||
func (fb *FieldBuilder) Fatal(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Build the message by concatenating arguments with spaces
|
||||
var builder strings.Builder
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
builder.WriteString(fmt.Sprint(arg))
|
||||
}
|
||||
// Log at Error level with the builder’s fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), fb.fields, true)
|
||||
// Exit the program with status code 1
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted message at Error level with a stack trace and the builder’s fields,
|
||||
// then exits. It delegates to Fatal and returns early if fields are nil. This method is used
|
||||
// for unrecoverable errors.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Fatalf("Fatal %s", "error") // Output: [app] ERROR: Fatal error [user=alice stack=...], then exits
|
||||
func (fb *FieldBuilder) Fatalf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message and pass to Fatal
|
||||
fb.Fatal(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Panic logs a message at Error level with a stack trace and the builder’s fields, then panics.
|
||||
// It constructs the message from variadic arguments, logs it with a stack trace, and triggers
|
||||
// a panic with the message. Returns early if fields are nil. This method is used for critical
|
||||
// errors that require immediate program termination with a panic.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Panic("Panic", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics
|
||||
func (fb *FieldBuilder) Panic(args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Build the message by concatenating arguments with spaces
|
||||
var builder strings.Builder
|
||||
for i, arg := range args {
|
||||
if i > 0 {
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
builder.WriteString(fmt.Sprint(arg))
|
||||
}
|
||||
msg := builder.String()
|
||||
// Log at Error level with the builder’s fields and a stack trace
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, msg, fb.fields, true)
|
||||
// Trigger a panic with the formatted message
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
// Panicf logs a formatted message at Error level with a stack trace and the builder’s fields,
|
||||
// then panics. It delegates to Panic and returns early if fields are nil. This method is used
|
||||
// for critical errors that require immediate program termination with a panic.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("user", "alice").Panicf("Panic %s", "error") // Output: [app] ERROR: Panic error [user=alice stack=...], then panics
|
||||
func (fb *FieldBuilder) Panicf(format string, args ...any) {
|
||||
// Skip logging if fields are nil
|
||||
if fb.fields == nil {
|
||||
return
|
||||
}
|
||||
// Format the message and pass to Panic
|
||||
fb.Panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Err adds one or more errors to the FieldBuilder as a field and logs them.
|
||||
// It stores non-nil errors in the "error" field: a single error if only one is non-nil,
|
||||
// or a slice of errors if multiple are non-nil. It logs the concatenated string representations
|
||||
// of non-nil errors (e.g., "failed 1; failed 2") at the Error level. Returns the FieldBuilder
|
||||
// for chaining, allowing further field additions or logging. Thread-safe via the logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// err1 := errors.New("failed 1")
|
||||
// err2 := errors.New("failed 2")
|
||||
// logger.Fields("k", "v").Err(err1, err2).Info("Error occurred")
|
||||
// // Output: [app] ERROR: failed 1; failed 2
|
||||
// // [app] INFO: Error occurred [error=[failed 1 failed 2] k=v]
|
||||
func (fb *FieldBuilder) Err(errs ...error) *FieldBuilder {
|
||||
// Initialize fields map if nil
|
||||
if fb.fields == nil {
|
||||
fb.fields = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Collect non-nil errors and build log message
|
||||
var nonNilErrors []error
|
||||
var builder strings.Builder
|
||||
count := 0
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
if i > 0 && count > 0 {
|
||||
builder.WriteString("; ")
|
||||
}
|
||||
builder.WriteString(err.Error())
|
||||
nonNilErrors = append(nonNilErrors, err)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
// Set error field and log if there are non-nil errors
|
||||
if count > 0 {
|
||||
if count == 1 {
|
||||
// Store single error directly
|
||||
fb.fields["error"] = nonNilErrors[0]
|
||||
} else {
|
||||
// Store slice of errors
|
||||
fb.fields["error"] = nonNilErrors
|
||||
}
|
||||
// Log concatenated error messages at Error level
|
||||
fb.logger.log(lx.LevelError, lx.ClassText, builder.String(), nil, false)
|
||||
}
|
||||
|
||||
// Return FieldBuilder for chaining
|
||||
return fb
|
||||
}
|
||||
|
||||
// Merge adds additional key-value pairs to the FieldBuilder.
|
||||
// It processes variadic arguments as key-value pairs, expecting string keys. Non-string keys
|
||||
// or uneven pairs generate an "error" field with a descriptive message. Returns the FieldBuilder
|
||||
// for chaining to allow further field additions or logging.
|
||||
// Example:
|
||||
//
|
||||
// logger := New("app").Enable()
|
||||
// logger.Fields("k1", "v1").Merge("k2", "v2").Info("Action") // Output: [app] INFO: Action [k1=v1 k2=v2]
|
||||
func (fb *FieldBuilder) Merge(pairs ...any) *FieldBuilder {
|
||||
// Process pairs as key-value, advancing by 2
|
||||
for i := 0; i < len(pairs)-1; i += 2 {
|
||||
// Ensure the key is a string
|
||||
if key, ok := pairs[i].(string); ok {
|
||||
fb.fields[key] = pairs[i+1]
|
||||
} else {
|
||||
// Log an error field for non-string keys
|
||||
fb.fields["error"] = fmt.Errorf("non-string key in Merge: %v", pairs[i])
|
||||
}
|
||||
}
|
||||
// Check for uneven pairs (missing value)
|
||||
if len(pairs)%2 != 0 {
|
||||
fb.fields["error"] = fmt.Errorf("uneven key-value pairs in Merge: [%v]", pairs[len(pairs)-1])
|
||||
}
|
||||
return fb
|
||||
}
|
||||
659
vendor/github.com/olekukonko/ll/global.go
generated
vendored
Normal file
659
vendor/github.com/olekukonko/ll/global.go
generated
vendored
Normal file
@@ -0,0 +1,659 @@
|
||||
package ll
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll/lh"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// defaultLogger is the global logger instance for package-level logging functions.
|
||||
// It provides a shared logger for convenience, allowing logging without explicitly creating
|
||||
// a logger instance. The logger is initialized with default settings: enabled, Debug level,
|
||||
// flat namespace style, and a text handler to os.Stdout. It is thread-safe due to the Logger
|
||||
// struct’s mutex.
|
||||
var defaultLogger = &Logger{
|
||||
enabled: true, // Initially enabled
|
||||
level: lx.LevelDebug, // Minimum log level set to Debug
|
||||
namespaces: defaultStore, // Shared namespace store for enable/disable states
|
||||
context: make(map[string]interface{}), // Empty context for global fields
|
||||
style: lx.FlatPath, // Flat namespace style (e.g., [parent/child])
|
||||
handler: lh.NewTextHandler(os.Stdout), // Default text handler to os.Stdout
|
||||
middleware: make([]Middleware, 0), // Empty middleware chain
|
||||
stackBufferSize: 4096, // Buffer size for stack traces
|
||||
}
|
||||
|
||||
// Handler sets the handler for the default logger.
|
||||
// It configures the output destination and format (e.g., text, JSON) for logs emitted by
|
||||
// defaultLogger. Returns the default logger for method chaining, enabling fluent configuration.
|
||||
// Example:
|
||||
//
|
||||
// ll.Handler(lh.NewJSONHandler(os.Stdout)).Enable()
|
||||
// ll.Info("Started") // Output: {"level":"INFO","message":"Started"}
|
||||
func Handler(handler lx.Handler) *Logger {
|
||||
return defaultLogger.Handler(handler)
|
||||
}
|
||||
|
||||
// Level sets the minimum log level for the default logger.
|
||||
// It determines which log messages (Debug, Info, Warn, Error) are emitted. Messages below
|
||||
// the specified level are ignored. Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelWarn)
|
||||
// ll.Info("Ignored") // No output
|
||||
// ll.Warn("Logged") // Output: [] WARN: Logged
|
||||
func Level(level lx.LevelType) *Logger {
|
||||
return defaultLogger.Level(level)
|
||||
}
|
||||
|
||||
// Style sets the namespace style for the default logger.
|
||||
// It controls how namespace paths are formatted in logs (FlatPath: [parent/child],
|
||||
// NestedPath: [parent]→[child]). Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Style(lx.NestedPath)
|
||||
// ll.Info("Test") // Output: []: INFO: Test
|
||||
func Style(style lx.StyleType) *Logger {
|
||||
return defaultLogger.Style(style)
|
||||
}
|
||||
|
||||
// NamespaceEnable enables logging for a namespace and its children using the default logger.
|
||||
// It activates logging for the specified namespace path (e.g., "app/db") and all its
|
||||
// descendants. Returns the default logger for method chaining. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// ll.NamespaceEnable("app/db")
|
||||
// ll.Clone().Namespace("db").Info("Query") // Output: [app/db] INFO: Query
|
||||
func NamespaceEnable(path string) *Logger {
|
||||
return defaultLogger.NamespaceEnable(path)
|
||||
}
|
||||
|
||||
// NamespaceDisable disables logging for a namespace and its children using the default logger.
|
||||
// It suppresses logging for the specified namespace path and all its descendants. Returns
|
||||
// the default logger for method chaining. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// ll.NamespaceDisable("app/db")
|
||||
// ll.Clone().Namespace("db").Info("Query") // No output
|
||||
func NamespaceDisable(path string) *Logger {
|
||||
return defaultLogger.NamespaceDisable(path)
|
||||
}
|
||||
|
||||
// Namespace creates a child logger with a sub-namespace appended to the current path.
|
||||
// The child inherits the default logger’s configuration but has an independent context.
|
||||
// Thread-safe with read lock. Returns the new logger for further configuration or logging.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.Namespace("app")
|
||||
// logger.Info("Started") // Output: [app] INFO: Started
|
||||
func Namespace(name string) *Logger {
|
||||
return defaultLogger.Namespace(name)
|
||||
}
|
||||
|
||||
// Info logs a message at Info level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Info method.
|
||||
// Thread-safe via the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Info("Service", "started") // Output: [] INFO: Service started
|
||||
func Info(args ...any) {
|
||||
defaultLogger.Info(args...)
|
||||
}
|
||||
|
||||
// Infof logs a message at Info level with a format string using the default logger.
|
||||
// It formats the message using the provided format string and arguments, then delegates to
|
||||
// defaultLogger’s Infof method. Thread-safe via the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Infof("Service %s", "started") // Output: [] INFO: Service started
|
||||
func Infof(format string, args ...any) {
|
||||
defaultLogger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Debug logs a message at Debug level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Debug method.
|
||||
// Used for debugging information, typically disabled in production. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelDebug)
|
||||
// ll.Debug("Debugging", "mode") // Output: [] DEBUG: Debugging mode
|
||||
func Debug(args ...any) {
|
||||
defaultLogger.Debug(args...)
|
||||
}
|
||||
|
||||
// Debugf logs a message at Debug level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Debugf method. Used for debugging
|
||||
// information, typically disabled in production. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelDebug)
|
||||
// ll.Debugf("Debug %s", "mode") // Output: [] DEBUG: Debug mode
|
||||
func Debugf(format string, args ...any) {
|
||||
defaultLogger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs a message at Warn level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Warn method.
|
||||
// Used for warning conditions that do not halt execution. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Warn("Low", "memory") // Output: [] WARN: Low memory
|
||||
func Warn(args ...any) {
|
||||
defaultLogger.Warn(args...)
|
||||
}
|
||||
|
||||
// Warnf logs a message at Warn level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Warnf method. Used for warning
|
||||
// conditions that do not halt execution. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Warnf("Low %s", "memory") // Output: [] WARN: Low memory
|
||||
func Warnf(format string, args ...any) {
|
||||
defaultLogger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error logs a message at Error level with variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Error method.
|
||||
// Used for error conditions requiring attention. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Error("Database", "failure") // Output: [] ERROR: Database failure
|
||||
func Error(args ...any) {
|
||||
defaultLogger.Error(args...)
|
||||
}
|
||||
|
||||
// Errorf logs a message at Error level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Errorf method. Used for error
|
||||
// conditions requiring attention. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Errorf("Database %s", "failure") // Output: [] ERROR: Database failure
|
||||
func Errorf(format string, args ...any) {
|
||||
defaultLogger.Errorf(format, args...)
|
||||
}
|
||||
|
||||
// Stack logs a message at Error level with a stack trace and variadic arguments using the default logger.
|
||||
// It concatenates the arguments with spaces and delegates to defaultLogger’s Stack method.
|
||||
// Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Stack("Critical", "error") // Output: [] ERROR: Critical error [stack=...]
|
||||
func Stack(args ...any) {
|
||||
defaultLogger.Stack(args...)
|
||||
}
|
||||
|
||||
// Stackf logs a message at Error level with a stack trace and a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Stackf method. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Stackf("Critical %s", "error") // Output: [] ERROR: Critical error [stack=...]
|
||||
func Stackf(format string, args ...any) {
|
||||
defaultLogger.Stackf(format, args...)
|
||||
}
|
||||
|
||||
// Fatal logs a message at Error level with a stack trace and variadic arguments using the default logger,
|
||||
// then exits. It concatenates the arguments with spaces, logs with a stack trace, and terminates
|
||||
// with exit code 1. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Fatal("Fatal", "error") // Output: [] ERROR: Fatal error [stack=...], then exits
|
||||
func Fatal(args ...any) {
|
||||
defaultLogger.Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted message at Error level with a stack trace using the default logger,
|
||||
// then exits. It formats the message, logs with a stack trace, and terminates with exit code 1.
|
||||
// Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Fatalf("Fatal %s", "error") // Output: [] ERROR: Fatal error [stack=...], then exits
|
||||
func Fatalf(format string, args ...any) {
|
||||
defaultLogger.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// Panic logs a message at Error level with a stack trace and variadic arguments using the default logger,
|
||||
// then panics. It concatenates the arguments with spaces, logs with a stack trace, and triggers a panic.
|
||||
// Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Panic("Panic", "error") // Output: [] ERROR: Panic error [stack=...], then panics
|
||||
func Panic(args ...any) {
|
||||
defaultLogger.Panic(args...)
|
||||
}
|
||||
|
||||
// Panicf logs a formatted message at Error level with a stack trace using the default logger,
|
||||
// then panics. It formats the message, logs with a stack trace, and triggers a panic. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Panicf("Panic %s", "error") // Output: [] ERROR: Panic error [stack=...], then panics
|
||||
func Panicf(format string, args ...any) {
|
||||
defaultLogger.Panicf(format, args...)
|
||||
}
|
||||
|
||||
// If creates a conditional logger that logs only if the condition is true using the default logger.
|
||||
// It returns a Conditional struct that wraps the default logger, enabling conditional logging methods.
|
||||
// Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// ll.If(true).Info("Logged") // Output: [] INFO: Logged
|
||||
// ll.If(false).Info("Ignored") // No output
|
||||
func If(condition bool) *Conditional {
|
||||
return defaultLogger.If(condition)
|
||||
}
|
||||
|
||||
// Context creates a new logger with additional contextual fields using the default logger.
|
||||
// It preserves existing context fields and adds new ones, returning a new logger instance
|
||||
// to avoid mutating the default logger. Thread-safe with write lock.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.Context(map[string]interface{}{"user": "alice"})
|
||||
// logger.Info("Action") // Output: [] INFO: Action [user=alice]
|
||||
func Context(fields map[string]interface{}) *Logger {
|
||||
return defaultLogger.Context(fields)
|
||||
}
|
||||
|
||||
// AddContext adds a key-value pair to the default logger’s context, modifying it directly.
|
||||
// It mutates the default logger’s context and is thread-safe using a write lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.AddContext("user", "alice")
|
||||
// ll.Info("Action") // Output: [] INFO: Action [user=alice]
|
||||
func AddContext(key string, value interface{}) *Logger {
|
||||
return defaultLogger.AddContext(key, value)
|
||||
}
|
||||
|
||||
// GetContext returns the default logger’s context map of persistent key-value fields.
|
||||
// It provides thread-safe read access to the context using a read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.AddContext("user", "alice")
|
||||
// ctx := ll.GetContext() // Returns map[string]interface{}{"user": "alice"}
|
||||
func GetContext() map[string]interface{} {
|
||||
return defaultLogger.GetContext()
|
||||
}
|
||||
|
||||
// GetLevel returns the minimum log level for the default logger.
|
||||
// It provides thread-safe read access to the level field using a read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelWarn)
|
||||
// if ll.GetLevel() == lx.LevelWarn {
|
||||
// ll.Warn("Warning level set") // Output: [] WARN: Warning level set
|
||||
// }
|
||||
func GetLevel() lx.LevelType {
|
||||
return defaultLogger.GetLevel()
|
||||
}
|
||||
|
||||
// GetPath returns the default logger’s current namespace path.
|
||||
// It provides thread-safe read access to the currentPath field using a read lock.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.Namespace("app")
|
||||
// path := logger.GetPath() // Returns "app"
|
||||
func GetPath() string {
|
||||
return defaultLogger.GetPath()
|
||||
}
|
||||
|
||||
// GetSeparator returns the default logger’s namespace separator (e.g., "/").
|
||||
// It provides thread-safe read access to the separator field using a read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.Separator(".")
|
||||
// sep := ll.GetSeparator() // Returns "."
|
||||
func GetSeparator() string {
|
||||
return defaultLogger.GetSeparator()
|
||||
}
|
||||
|
||||
// GetStyle returns the default logger’s namespace formatting style (FlatPath or NestedPath).
|
||||
// It provides thread-safe read access to the style field using a read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.Style(lx.NestedPath)
|
||||
// if ll.GetStyle() == lx.NestedPath {
|
||||
// ll.Info("Nested style") // Output: []: INFO: Nested style
|
||||
// }
|
||||
func GetStyle() lx.StyleType {
|
||||
return defaultLogger.GetStyle()
|
||||
}
|
||||
|
||||
// GetHandler returns the default logger’s current handler for customization or inspection.
|
||||
// The returned handler should not be modified concurrently with logger operations.
|
||||
// Example:
|
||||
//
|
||||
// handler := ll.GetHandler() // Returns the current handler (e.g., TextHandler)
|
||||
func GetHandler() lx.Handler {
|
||||
return defaultLogger.GetHandler()
|
||||
}
|
||||
|
||||
// Separator sets the namespace separator for the default logger (e.g., "/" or ".").
|
||||
// It updates the separator used in namespace paths. Thread-safe with write lock.
|
||||
// Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Separator(".")
|
||||
// ll.Namespace("app").Info("Log") // Output: [app] INFO: Log
|
||||
func Separator(separator string) *Logger {
|
||||
return defaultLogger.Separator(separator)
|
||||
}
|
||||
|
||||
// Prefix sets a prefix to be prepended to all log messages of the default logger.
|
||||
// The prefix is applied before the message in the log output. Thread-safe with write lock.
|
||||
// Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Prefix("APP: ")
|
||||
// ll.Info("Started") // Output: [] INFO: APP: Started
|
||||
func Prefix(prefix string) *Logger {
|
||||
return defaultLogger.Prefix(prefix)
|
||||
}
|
||||
|
||||
// StackSize sets the buffer size for stack trace capture in the default logger.
|
||||
// It configures the maximum size for stack traces in Stack, Fatal, and Panic methods.
|
||||
// Thread-safe with write lock. Returns the default logger for chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.StackSize(65536)
|
||||
// ll.Stack("Error") // Captures up to 64KB stack trace
|
||||
func StackSize(size int) *Logger {
|
||||
return defaultLogger.StackSize(size)
|
||||
}
|
||||
|
||||
// Use adds a middleware function to process log entries before they are handled by the default logger.
|
||||
// It registers the middleware and returns a Middleware handle for removal. Middleware returning
|
||||
// a non-nil error stops the log. Thread-safe with write lock.
|
||||
// Example:
|
||||
//
|
||||
// mw := ll.Use(ll.FuncMiddleware(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// ll.Info("Ignored") // No output
|
||||
// mw.Remove()
|
||||
// ll.Info("Logged") // Output: [] INFO: Logged
|
||||
func Use(fn lx.Handler) *Middleware {
|
||||
return defaultLogger.Use(fn)
|
||||
}
|
||||
|
||||
// Remove removes middleware by the reference returned from Use for the default logger.
|
||||
// It delegates to the Middleware’s Remove method for thread-safe removal.
|
||||
// Example:
|
||||
//
|
||||
// mw := ll.Use(someMiddleware)
|
||||
// ll.Remove(mw) // Removes middleware
|
||||
func Remove(m *Middleware) {
|
||||
defaultLogger.Remove(m)
|
||||
}
|
||||
|
||||
// Clear removes all middleware functions from the default logger.
|
||||
// It resets the middleware chain to empty, ensuring no middleware is applied.
|
||||
// Thread-safe with write lock. Returns the default logger for chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Use(someMiddleware)
|
||||
// ll.Clear()
|
||||
// ll.Info("No middleware") // Output: [] INFO: No middleware
|
||||
func Clear() *Logger {
|
||||
return defaultLogger.Clear()
|
||||
}
|
||||
|
||||
// CanLog checks if a log at the given level would be emitted by the default logger.
|
||||
// It considers enablement, log level, namespaces, sampling, and rate limits.
|
||||
// Thread-safe via the Logger’s shouldLog method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Level(lx.LevelWarn)
|
||||
// canLog := ll.CanLog(lx.LevelInfo) // false
|
||||
func CanLog(level lx.LevelType) bool {
|
||||
return defaultLogger.CanLog(level)
|
||||
}
|
||||
|
||||
// NamespaceEnabled checks if a namespace is enabled in the default logger.
|
||||
// It evaluates the namespace hierarchy, considering parent namespaces, and caches the result
|
||||
// for performance. Thread-safe with read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.NamespaceDisable("app/db")
|
||||
// enabled := ll.NamespaceEnabled("app/db") // false
|
||||
func NamespaceEnabled(path string) bool {
|
||||
return defaultLogger.NamespaceEnabled(path)
|
||||
}
|
||||
|
||||
// Print logs a message at Info level without format specifiers using the default logger.
|
||||
// It concatenates variadic arguments with spaces, minimizing allocations, and delegates
|
||||
// to defaultLogger’s Print method. Thread-safe via the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Print("message", "value") // Output: [] INFO: message value
|
||||
func Print(args ...any) {
|
||||
defaultLogger.Print(args...)
|
||||
}
|
||||
|
||||
// Println logs a message at Info level without format specifiers, minimizing allocations
|
||||
// by concatenating arguments with spaces. It is thread-safe via the log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Println("message", "value") // Output: [] INFO: message value [New Line]
|
||||
func Println(args ...any) {
|
||||
defaultLogger.Println(args...)
|
||||
}
|
||||
|
||||
// Printf logs a message at Info level with a format string using the default logger.
|
||||
// It formats the message and delegates to defaultLogger’s Printf method. Thread-safe via
|
||||
// the Logger’s log method.
|
||||
// Example:
|
||||
//
|
||||
// ll.Printf("Message %s", "value") // Output: [] INFO: Message value
|
||||
func Printf(format string, args ...any) {
|
||||
defaultLogger.Printf(format, args...)
|
||||
}
|
||||
|
||||
// Len returns the total number of log entries sent to the handler by the default logger.
|
||||
// It provides thread-safe access to the entries counter using atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// ll.Info("Test")
|
||||
// count := ll.Len() // Returns 1
|
||||
func Len() int64 {
|
||||
return defaultLogger.Len()
|
||||
}
|
||||
|
||||
// Measure is a benchmarking helper that measures and returns the duration of a function’s execution.
|
||||
// It logs the duration at Info level with a "duration" field using defaultLogger. The function
|
||||
// is executed once, and the elapsed time is returned. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// duration := ll.Measure(func() { time.Sleep(time.Millisecond) })
|
||||
// // Output: [] INFO: function executed [duration=~1ms]
|
||||
func Measure(fns ...func()) time.Duration {
|
||||
start := time.Now()
|
||||
for _, fn := range fns {
|
||||
fn()
|
||||
}
|
||||
duration := time.Since(start)
|
||||
defaultLogger.Fields("duration", duration).Infof("function executed")
|
||||
return duration
|
||||
}
|
||||
|
||||
// Benchmark logs the duration since a start time at Info level using the default logger.
|
||||
// It calculates the time elapsed since the provided start time and logs it with "start",
|
||||
// "end", and "duration" fields. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// start := time.Now()
|
||||
// time.Sleep(time.Millisecond)
|
||||
// ll.Benchmark(start) // Output: [] INFO: benchmark [start=... end=... duration=...]
|
||||
func Benchmark(start time.Time) {
|
||||
defaultLogger.Fields("start", start, "end", time.Now(), "duration", time.Now().Sub(start)).Infof("benchmark")
|
||||
}
|
||||
|
||||
// Clone returns a new logger with the same configuration as the default logger.
|
||||
// It creates a copy of defaultLogger’s settings (level, style, namespaces, etc.) but with
|
||||
// an independent context, allowing customization without affecting the global logger.
|
||||
// Thread-safe via the Logger’s Clone method.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.Clone().Namespace("sub")
|
||||
// logger.Info("Sub-logger") // Output: [sub] INFO: Sub-logger
|
||||
func Clone() *Logger {
|
||||
return defaultLogger.Clone()
|
||||
}
|
||||
|
||||
// Err adds one or more errors to the default logger’s context and logs them.
|
||||
// It stores non-nil errors in the "error" context field and logs their concatenated string
|
||||
// representations (e.g., "failed 1; failed 2") at the Error level. Thread-safe via the Logger’s mutex.
|
||||
// Example:
|
||||
//
|
||||
// err1 := errors.New("failed 1")
|
||||
// ll.Err(err1)
|
||||
// ll.Info("Error occurred") // Output: [] ERROR: failed 1
|
||||
// // [] INFO: Error occurred [error=failed 1]
|
||||
func Err(errs ...error) {
|
||||
defaultLogger.Err(errs...)
|
||||
}
|
||||
|
||||
// Start activates the global logging system.
|
||||
// If the system was shut down, this re-enables all logging operations,
|
||||
// subject to individual logger and namespace configurations.
|
||||
// Thread-safe via atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// ll.Shutdown()
|
||||
// ll.Info("Ignored") // No output
|
||||
// ll.Start()
|
||||
// ll.Info("Logged") // Output: [] INFO: Logged
|
||||
func Start() {
|
||||
atomic.StoreInt32(&systemActive, 1)
|
||||
}
|
||||
|
||||
// Shutdown deactivates the global logging system.
|
||||
// All logging operations are skipped, regardless of individual logger or namespace configurations,
|
||||
// until Start() is called again. Thread-safe via atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// ll.Shutdown()
|
||||
// ll.Info("Ignored") // No output
|
||||
func Shutdown() {
|
||||
atomic.StoreInt32(&systemActive, 0)
|
||||
}
|
||||
|
||||
// Active returns true if the global logging system is currently active.
|
||||
// Thread-safe via atomic operations.
|
||||
// Example:
|
||||
//
|
||||
// if ll.Active() {
|
||||
// ll.Info("System active") // Output: [] INFO: System active
|
||||
// }
|
||||
func Active() bool {
|
||||
return atomic.LoadInt32(&systemActive) == 1
|
||||
}
|
||||
|
||||
// Enable activates logging for the default logger.
|
||||
// It allows logs to be emitted if other conditions (level, namespace) are met.
|
||||
// Thread-safe with write lock. Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Disable()
|
||||
// ll.Info("Ignored") // No output
|
||||
// ll.Enable()
|
||||
// ll.Info("Logged") // Output: [] INFO: Logged
|
||||
func Enable() *Logger {
|
||||
return defaultLogger.Enable()
|
||||
}
|
||||
|
||||
// Disable deactivates logging for the default logger.
|
||||
// It suppresses all logs, regardless of level or namespace. Thread-safe with write lock.
|
||||
// Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Disable()
|
||||
// ll.Info("Ignored") // No output
|
||||
func Disable() *Logger {
|
||||
return defaultLogger.Disable()
|
||||
}
|
||||
|
||||
// Dbg logs debug information including the source file, line number, and expression value
|
||||
// using the default logger. It captures the calling line of code and displays both the
|
||||
// expression and its value. Useful for debugging without temporary print statements.
|
||||
// Example:
|
||||
//
|
||||
// x := 42
|
||||
// ll.Dbg(x) // Output: [file.go:123] x = 42
|
||||
func Dbg(any ...interface{}) {
|
||||
defaultLogger.dbg(2, any...)
|
||||
}
|
||||
|
||||
// Dump displays a hex and ASCII representation of a value’s binary form using the default logger.
|
||||
// It serializes the value using gob encoding or direct conversion and shows a hex/ASCII dump.
|
||||
// Useful for inspecting binary data structures.
|
||||
// Example:
|
||||
//
|
||||
// ll.Dump([]byte{0x41, 0x42}) // Outputs hex/ASCII dump
|
||||
func Dump(any interface{}) {
|
||||
defaultLogger.Dump(any)
|
||||
}
|
||||
|
||||
// Enabled returns whether the default logger is enabled for logging.
|
||||
// It provides thread-safe read access to the enabled field using a read lock.
|
||||
// Example:
|
||||
//
|
||||
// ll.Enable()
|
||||
// if ll.Enabled() {
|
||||
// ll.Info("Logging enabled") // Output: [] INFO: Logging enabled
|
||||
// }
|
||||
func Enabled() bool {
|
||||
return defaultLogger.Enabled()
|
||||
}
|
||||
|
||||
// Fields starts a fluent chain for adding fields using variadic key-value pairs with the default logger.
|
||||
// It creates a FieldBuilder to attach fields, handling non-string keys or uneven pairs by
|
||||
// adding an error field. Thread-safe via the FieldBuilder’s logger.
|
||||
// Example:
|
||||
//
|
||||
// ll.Fields("user", "alice").Info("Action") // Output: [] INFO: Action [user=alice]
|
||||
func Fields(pairs ...any) *FieldBuilder {
|
||||
return defaultLogger.Fields(pairs...)
|
||||
}
|
||||
|
||||
// Field starts a fluent chain for adding fields from a map with the default logger.
|
||||
// It creates a FieldBuilder to attach fields from a map, supporting type-safe field addition.
|
||||
// Thread-safe via the FieldBuilder’s logger.
|
||||
// Example:
|
||||
//
|
||||
// ll.Field(map[string]interface{}{"user": "alice"}).Info("Action") // Output: [] INFO: Action [user=alice]
|
||||
func Field(fields map[string]interface{}) *FieldBuilder {
|
||||
return defaultLogger.Field(fields)
|
||||
}
|
||||
|
||||
// Line adds vertical spacing (newlines) to the log output using the default logger.
|
||||
// If no arguments are provided, it defaults to 1 newline. Multiple values are summed to
|
||||
// determine the total lines. Useful for visually separating log sections. Thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// ll.Line(2).Info("After two newlines") // Adds 2 blank lines before: [] INFO: After two newlines
|
||||
func Line(lines ...int) *Logger {
|
||||
return defaultLogger.Line(lines...)
|
||||
}
|
||||
|
||||
// Indent sets the indentation level for all log messages of the default logger.
|
||||
// Each level adds two spaces to the log message, useful for hierarchical output.
|
||||
// Thread-safe with write lock. Returns the default logger for method chaining.
|
||||
// Example:
|
||||
//
|
||||
// ll.Indent(2)
|
||||
// ll.Info("Indented") // Output: [] INFO: Indented
|
||||
func Indent(depth int) *Logger {
|
||||
return defaultLogger.Indent(depth)
|
||||
}
|
||||
|
||||
// Mark logs the current file and line number where it's called, without any additional debug information.
|
||||
// It's useful for tracing execution flow without the verbosity of Dbg.
|
||||
// Example:
|
||||
//
|
||||
// logger.Mark() // *MARK*: [file.go:123]
|
||||
func Mark(names ...string) {
|
||||
defaultLogger.mark(2, names...)
|
||||
|
||||
}
|
||||
48
vendor/github.com/olekukonko/ll/lc.go
generated
vendored
Normal file
48
vendor/github.com/olekukonko/ll/lc.go
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
package ll
|
||||
|
||||
import "github.com/olekukonko/ll/lx"
|
||||
|
||||
// defaultStore is the global namespace store for enable/disable states.
|
||||
// It is shared across all Logger instances to manage namespace hierarchy consistently.
|
||||
// Thread-safe via the lx.Namespace struct’s sync.Map.
|
||||
var defaultStore = &lx.Namespace{}
|
||||
|
||||
// systemActive indicates if the global logging system is active.
|
||||
// Defaults to true, meaning logging is active unless explicitly shut down.
|
||||
// Or, default to false and require an explicit ll.Start(). Let's default to true for less surprise.
|
||||
var systemActive int32 = 1 // 1 for true, 0 for false (for atomic operations)
|
||||
|
||||
// Option defines a functional option for configuring a Logger.
|
||||
type Option func(*Logger)
|
||||
|
||||
// reverseString reverses the input string by swapping characters from both ends.
|
||||
// It converts the string to a rune slice to handle Unicode characters correctly,
|
||||
// ensuring proper reversal for multi-byte characters.
|
||||
// Used internally for string manipulation, such as in debugging or log formatting.
|
||||
func reverseString(s string) string {
|
||||
// Convert string to rune slice to handle Unicode characters
|
||||
r := []rune(s)
|
||||
// Iterate over half the slice, swapping characters from start and end
|
||||
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
// Convert rune slice back to string and return
|
||||
return string(r)
|
||||
}
|
||||
|
||||
// viewString converts a byte slice to a printable string, replacing non-printable
|
||||
// characters (ASCII < 32 or > 126) with a dot ('.').
|
||||
// It ensures safe display of binary data in logs, such as in the Dump method.
|
||||
// Used for formatting binary data in a human-readable hex/ASCII dump.
|
||||
func viewString(b []byte) string {
|
||||
// Convert byte slice to rune slice via string for processing
|
||||
r := []rune(string(b))
|
||||
// Replace non-printable characters with '.'
|
||||
for i := range r {
|
||||
if r[i] < 32 || r[i] > 126 {
|
||||
r[i] = '.'
|
||||
}
|
||||
}
|
||||
// Return the resulting printable string
|
||||
return string(r)
|
||||
}
|
||||
279
vendor/github.com/olekukonko/ll/lh/buffered.go
generated
vendored
Normal file
279
vendor/github.com/olekukonko/ll/lh/buffered.go
generated
vendored
Normal file
@@ -0,0 +1,279 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Buffering holds configuration for the Buffered handler.
|
||||
type Buffering struct {
|
||||
BatchSize int // Flush when this many entries are buffered (default: 100)
|
||||
FlushInterval time.Duration // Maximum time between flushes (default: 10s)
|
||||
MaxBuffer int // Maximum buffer size before applying backpressure (default: 1000)
|
||||
OnOverflow func(int) // Called when buffer reaches MaxBuffer (default: logs warning)
|
||||
}
|
||||
|
||||
// BufferingOpt configures Buffered handler.
|
||||
type BufferingOpt func(*Buffering)
|
||||
|
||||
// WithBatchSize sets the batch size for flushing.
|
||||
// It specifies the number of log entries to buffer before flushing to the underlying handler.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithBatchSize(50)) // Flush every 50 entries
|
||||
func WithBatchSize(size int) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.BatchSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithFlushInterval sets the maximum time between flushes.
|
||||
// It defines the interval at which buffered entries are flushed, even if the batch size is not reached.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithFlushInterval(5*time.Second)) // Flush every 5 seconds
|
||||
func WithFlushInterval(d time.Duration) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.FlushInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxBuffer sets the maximum buffer size before backpressure.
|
||||
// It limits the number of entries that can be queued in the channel, triggering overflow handling if exceeded.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithMaxBuffer(500)) // Allow up to 500 buffered entries
|
||||
func WithMaxBuffer(size int) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.MaxBuffer = size
|
||||
}
|
||||
}
|
||||
|
||||
// WithOverflowHandler sets the overflow callback.
|
||||
// It specifies a function to call when the buffer reaches MaxBuffer, typically for logging or metrics.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewBuffered(textHandler, WithOverflowHandler(func(n int) { fmt.Printf("Overflow: %d entries\n", n) }))
|
||||
func WithOverflowHandler(fn func(int)) BufferingOpt {
|
||||
return func(c *Buffering) {
|
||||
c.OnOverflow = fn
|
||||
}
|
||||
}
|
||||
|
||||
// Buffered wraps any Handler to provide buffering capabilities.
|
||||
// It buffers log entries in a channel and flushes them based on batch size, time interval, or explicit flush.
|
||||
// The generic type H ensures compatibility with any lx.Handler implementation.
|
||||
// Thread-safe via channels and sync primitives.
|
||||
type Buffered[H lx.Handler] struct {
|
||||
handler H // Underlying handler to process log entries
|
||||
config *Buffering // Configuration for batching and flushing
|
||||
entries chan *lx.Entry // Channel for buffering log entries
|
||||
flushSignal chan struct{} // Channel to trigger explicit flushes
|
||||
shutdown chan struct{} // Channel to signal worker shutdown
|
||||
shutdownOnce sync.Once // Ensures Close is called only once
|
||||
wg sync.WaitGroup // Waits for worker goroutine to finish
|
||||
}
|
||||
|
||||
// NewBuffered creates a new buffered handler that wraps another handler.
|
||||
// It initializes the handler with default or provided configuration options and starts a worker goroutine.
|
||||
// Thread-safe via channel operations and finalizer for cleanup.
|
||||
// Example:
|
||||
//
|
||||
// textHandler := lh.NewTextHandler(os.Stdout)
|
||||
// buffered := NewBuffered(textHandler, WithBatchSize(50))
|
||||
func NewBuffered[H lx.Handler](handler H, opts ...BufferingOpt) *Buffered[H] {
|
||||
// Initialize default configuration
|
||||
config := &Buffering{
|
||||
BatchSize: 100, // Default: flush every 100 entries
|
||||
FlushInterval: 10 * time.Second, // Default: flush every 10 seconds
|
||||
MaxBuffer: 1000, // Default: max 1000 entries in buffer
|
||||
OnOverflow: func(count int) { // Default: log overflow to io.Discard
|
||||
fmt.Fprintf(io.Discard, "log buffer overflow: %d entries\n", count)
|
||||
},
|
||||
}
|
||||
|
||||
// Apply provided options
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
// Ensure sane configuration values
|
||||
if config.BatchSize < 1 {
|
||||
config.BatchSize = 1 // Minimum batch size is 1
|
||||
}
|
||||
if config.MaxBuffer < config.BatchSize {
|
||||
config.MaxBuffer = config.BatchSize * 10 // Ensure buffer is at least 10x batch size
|
||||
}
|
||||
if config.FlushInterval <= 0 {
|
||||
config.FlushInterval = 10 * time.Second // Minimum flush interval is 10s
|
||||
}
|
||||
|
||||
// Initialize Buffered handler
|
||||
b := &Buffered[H]{
|
||||
handler: handler, // Set underlying handler
|
||||
config: config, // Set configuration
|
||||
entries: make(chan *lx.Entry, config.MaxBuffer), // Create buffered channel
|
||||
flushSignal: make(chan struct{}, 1), // Create single-slot flush signal channel
|
||||
shutdown: make(chan struct{}), // Create shutdown signal channel
|
||||
}
|
||||
|
||||
// Start worker goroutine
|
||||
b.wg.Add(1)
|
||||
go b.worker()
|
||||
|
||||
// Set finalizer for cleanup during garbage collection
|
||||
runtime.SetFinalizer(b, (*Buffered[H]).Final)
|
||||
return b
|
||||
}
|
||||
|
||||
// Handle implements the lx.Handler interface.
|
||||
// It buffers log entries in the entries channel or triggers a flush on overflow.
|
||||
// Returns an error if the buffer is full and flush cannot be triggered.
|
||||
// Thread-safe via non-blocking channel operations.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Handle(&lx.Entry{Message: "test"}) // Buffers entry or triggers flush
|
||||
func (b *Buffered[H]) Handle(e *lx.Entry) error {
|
||||
select {
|
||||
case b.entries <- e: // Buffer entry if channel has space
|
||||
return nil
|
||||
default: // Handle buffer overflow
|
||||
if b.config.OnOverflow != nil {
|
||||
b.config.OnOverflow(len(b.entries)) // Call overflow handler
|
||||
}
|
||||
select {
|
||||
case b.flushSignal <- struct{}{}: // Trigger flush if possible
|
||||
return fmt.Errorf("log buffer overflow, triggering flush")
|
||||
default: // Flush already in progress
|
||||
return fmt.Errorf("log buffer overflow and flush already in progress")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush triggers an immediate flush of buffered entries.
|
||||
// It sends a signal to the worker to process all buffered entries.
|
||||
// If a flush is already pending, it waits briefly and may exit without flushing.
|
||||
// Thread-safe via non-blocking channel operations.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Flush() // Flushes all buffered entries
|
||||
func (b *Buffered[H]) Flush() {
|
||||
select {
|
||||
case b.flushSignal <- struct{}{}: // Signal worker to flush
|
||||
case <-time.After(100 * time.Millisecond): // Timeout if flush is pending
|
||||
// Flush already pending
|
||||
}
|
||||
}
|
||||
|
||||
// Close flushes any remaining entries and stops the worker.
|
||||
// It ensures shutdown is performed only once and waits for the worker to finish.
|
||||
// Thread-safe via sync.Once and WaitGroup.
|
||||
// Returns nil as it does not produce errors.
|
||||
// Example:
|
||||
//
|
||||
// buffered.Close() // Flushes entries and stops worker
|
||||
func (b *Buffered[H]) Close() error {
|
||||
b.shutdownOnce.Do(func() {
|
||||
close(b.shutdown) // Signal worker to shut down
|
||||
b.wg.Wait() // Wait for worker to finish
|
||||
runtime.SetFinalizer(b, nil) // Remove finalizer
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Final ensures remaining entries are flushed during garbage collection.
|
||||
// It calls Close to flush entries and stop the worker.
|
||||
// Used as a runtime finalizer to prevent log loss.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// runtime.SetFinalizer(buffered, (*Buffered[H]).Final)
|
||||
func (b *Buffered[H]) Final() {
|
||||
b.Close()
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the Buffered handler.
|
||||
// It provides access to BatchSize, FlushInterval, MaxBuffer, and OnOverflow settings.
|
||||
// Example:
|
||||
//
|
||||
// config := buffered.Config() // Access configuration
|
||||
func (b *Buffered[H]) Config() *Buffering {
|
||||
return b.config
|
||||
}
|
||||
|
||||
// worker processes entries and handles flushing.
|
||||
// It runs in a goroutine, buffering entries, flushing on batch size, timer, or explicit signal,
|
||||
// and shutting down cleanly when signaled.
|
||||
// Thread-safe via channel operations and WaitGroup.
|
||||
func (b *Buffered[H]) worker() {
|
||||
defer b.wg.Done() // Signal completion when worker exits
|
||||
batch := make([]*lx.Entry, 0, b.config.BatchSize) // Buffer for batching entries
|
||||
ticker := time.NewTicker(b.config.FlushInterval) // Timer for periodic flushes
|
||||
defer ticker.Stop() // Clean up ticker
|
||||
for {
|
||||
select {
|
||||
case entry := <-b.entries: // Receive new entry
|
||||
batch = append(batch, entry)
|
||||
// Flush if batch size is reached
|
||||
if len(batch) >= b.config.BatchSize {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-ticker.C: // Periodic flush
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-b.flushSignal: // Explicit flush
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
b.drainRemaining() // Drain all entries from the channel
|
||||
case <-b.shutdown: // Shutdown signal
|
||||
if len(batch) > 0 {
|
||||
b.flushBatch(batch)
|
||||
}
|
||||
b.drainRemaining() // Flush remaining entries
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushBatch processes a batch of entries through the wrapped handler.
|
||||
// It writes each entry to the underlying handler, logging any errors to stderr.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// b.flushBatch([]*lx.Entry{entry1, entry2})
|
||||
func (b *Buffered[H]) flushBatch(batch []*lx.Entry) {
|
||||
for _, entry := range batch {
|
||||
// Process each entry through the handler
|
||||
if err := b.handler.Handle(entry); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "log flush error: %v\n", err) // Log errors to stderr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drainRemaining processes any remaining entries in the channel.
|
||||
// It flushes all entries from the entries channel to the underlying handler,
|
||||
// logging any errors to stderr. Used during flush or shutdown.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// b.drainRemaining() // Flushes all pending entries
|
||||
func (b *Buffered[H]) drainRemaining() {
|
||||
for {
|
||||
select {
|
||||
case entry := <-b.entries: // Process next entry
|
||||
if err := b.handler.Handle(entry); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "log drain error: %v\n", err) // Log errors to stderr
|
||||
}
|
||||
default: // Exit when channel is empty
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
473
vendor/github.com/olekukonko/ll/lh/colorized.go
generated
vendored
Normal file
473
vendor/github.com/olekukonko/ll/lh/colorized.go
generated
vendored
Normal file
@@ -0,0 +1,473 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Palette defines ANSI color codes for various log components.
|
||||
// It specifies colors for headers, goroutines, functions, paths, stack traces, and log levels,
|
||||
// used by ColorizedHandler to format log output with color.
|
||||
type Palette struct {
|
||||
Header string // Color for stack trace header and dump separators
|
||||
Goroutine string // Color for goroutine lines in stack traces
|
||||
Func string // Color for function names in stack traces
|
||||
Path string // Color for file paths in stack traces
|
||||
FileLine string // Color for file line numbers (not used in provided code)
|
||||
Reset string // Reset code to clear color formatting
|
||||
Pos string // Color for position in hex dumps
|
||||
Hex string // Color for hex values in dumps
|
||||
Ascii string // Color for ASCII values in dumps
|
||||
Debug string // Color for Debug level messages
|
||||
Info string // Color for Info level messages
|
||||
Warn string // Color for Warn level messages
|
||||
Error string // Color for Error level messages
|
||||
Title string // Color for dump titles (BEGIN/END separators)
|
||||
}
|
||||
|
||||
// darkPalette defines colors optimized for dark terminal backgrounds.
|
||||
// It uses bright, contrasting colors for readability on dark backgrounds.
|
||||
var darkPalette = Palette{
|
||||
Header: "\033[1;31m", // Bold red for headers
|
||||
Goroutine: "\033[1;36m", // Bold cyan for goroutines
|
||||
Func: "\033[97m", // Bright white for functions
|
||||
Path: "\033[38;5;245m", // Light gray for paths
|
||||
FileLine: "\033[38;5;111m", // Muted light blue (unused)
|
||||
Reset: "\033[0m", // Reset color formatting
|
||||
|
||||
Title: "\033[38;5;245m", // Light gray for dump titles
|
||||
Pos: "\033[38;5;117m", // Light blue for dump positions
|
||||
Hex: "\033[38;5;156m", // Light green for hex values
|
||||
Ascii: "\033[38;5;224m", // Light pink for ASCII values
|
||||
|
||||
Debug: "\033[36m", // Cyan for Debug level
|
||||
Info: "\033[32m", // Green for Info level
|
||||
Warn: "\033[33m", // Yellow for Warn level
|
||||
Error: "\033[31m", // Red for Error level
|
||||
}
|
||||
|
||||
// lightPalette defines colors optimized for light terminal backgrounds.
|
||||
// It uses darker colors for better contrast on light backgrounds.
|
||||
var lightPalette = Palette{
|
||||
Header: "\033[1;31m", // Same red for headers
|
||||
Goroutine: "\033[34m", // Blue (darker for light bg)
|
||||
Func: "\033[30m", // Black text for functions
|
||||
Path: "\033[90m", // Dark gray for paths
|
||||
FileLine: "\033[94m", // Blue for file lines (unused)
|
||||
Reset: "\033[0m", // Reset color formatting
|
||||
|
||||
Title: "\033[38;5;245m", // Light gray for dump titles
|
||||
Pos: "\033[38;5;117m", // Light blue for dump positions
|
||||
Hex: "\033[38;5;156m", // Light green for hex values
|
||||
Ascii: "\033[38;5;224m", // Light pink for ASCII values
|
||||
|
||||
Debug: "\033[36m", // Cyan for Debug level
|
||||
Info: "\033[32m", // Green for Info level
|
||||
Warn: "\033[33m", // Yellow for Warn level
|
||||
Error: "\033[31m", // Red for Error level
|
||||
}
|
||||
|
||||
// ColorizedHandler is a handler that outputs log entries with ANSI color codes.
|
||||
// It formats log entries with colored namespace, level, message, fields, and stack traces,
|
||||
// writing the result to the provided writer.
|
||||
// Thread-safe if the underlying writer is thread-safe.
|
||||
type ColorizedHandler struct {
|
||||
w io.Writer // Destination for colored log output
|
||||
palette Palette // Color scheme for formatting
|
||||
showTime bool // Whether to display timestamps
|
||||
timeFormat string // Format for timestamps (defaults to time.RFC3339)
|
||||
}
|
||||
|
||||
// ColorOption defines a configuration function for ColorizedHandler.
|
||||
// It allows customization of the handler, such as setting the color palette.
|
||||
type ColorOption func(*ColorizedHandler)
|
||||
|
||||
// WithColorPallet sets the color palette for the ColorizedHandler.
|
||||
// It allows specifying a custom Palette for dark or light terminal backgrounds.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewColorizedHandler(os.Stdout, WithColorPallet(lightPalette))
|
||||
func WithColorPallet(pallet Palette) ColorOption {
|
||||
return func(c *ColorizedHandler) {
|
||||
c.palette = pallet
|
||||
}
|
||||
}
|
||||
|
||||
// NewColorizedHandler creates a new ColorizedHandler writing to the specified writer.
|
||||
// It initializes the handler with a detected or specified color palette and applies
|
||||
// optional configuration functions.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewColorizedHandler(os.Stdout)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: [app] <colored INFO>: Test
|
||||
func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler {
|
||||
// Initialize with writer
|
||||
c := &ColorizedHandler{w: w,
|
||||
showTime: false,
|
||||
timeFormat: time.RFC3339,
|
||||
}
|
||||
|
||||
// Apply configuration options
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
// Detect palette if not set
|
||||
c.palette = c.detectPalette()
|
||||
return c
|
||||
}
|
||||
|
||||
// Handle processes a log entry and writes it with ANSI color codes.
|
||||
// It delegates to specialized methods based on the entry's class (Dump, Raw, or regular).
|
||||
// Returns an error if writing to the underlying writer fails.
|
||||
// Thread-safe if the writer is thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
|
||||
func (h *ColorizedHandler) Handle(e *lx.Entry) error {
|
||||
switch e.Class {
|
||||
case lx.ClassDump:
|
||||
// Handle hex dump entries
|
||||
return h.handleDumpOutput(e)
|
||||
case lx.ClassRaw:
|
||||
// Write raw entries directly
|
||||
_, err := h.w.Write([]byte(e.Message))
|
||||
return err
|
||||
default:
|
||||
// Handle standard log entries
|
||||
return h.handleRegularOutput(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamped enables or disables timestamp display and optionally sets a custom time format.
|
||||
// If format is empty, defaults to RFC3339.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewColorizedHandler(os.Stdout).Timestamped(true, time.StampMilli)
|
||||
// // Output: Jan 02 15:04:05.000 [app] INFO: Test
|
||||
func (h *ColorizedHandler) Timestamped(enable bool, format ...string) {
|
||||
h.showTime = enable
|
||||
if len(format) > 0 && format[0] != "" {
|
||||
h.timeFormat = format[0]
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegularOutput handles normal log entries.
|
||||
// It formats the entry with colored namespace, level, message, fields, and stack trace (if present),
|
||||
// writing the result to the handler's writer.
|
||||
// Returns an error if writing fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
|
||||
func (h *ColorizedHandler) handleRegularOutput(e *lx.Entry) error {
|
||||
var builder strings.Builder // Buffer for building formatted output
|
||||
|
||||
// Add timestamp if enabled
|
||||
if h.showTime {
|
||||
builder.WriteString(e.Timestamp.Format(h.timeFormat))
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
|
||||
// Format namespace with colors
|
||||
h.formatNamespace(&builder, e)
|
||||
|
||||
// Format level with color based on severity
|
||||
h.formatLevel(&builder, e)
|
||||
|
||||
// Add message and fields
|
||||
builder.WriteString(e.Message)
|
||||
h.formatFields(&builder, e)
|
||||
|
||||
// fmt.Println("------------>", len(e.Stack))
|
||||
// Format stack trace if present
|
||||
if len(e.Stack) > 0 {
|
||||
h.formatStack(&builder, e.Stack)
|
||||
}
|
||||
|
||||
// Append newline for non-None levels
|
||||
if e.Level != lx.LevelNone {
|
||||
builder.WriteString(lx.Newline)
|
||||
}
|
||||
|
||||
// Write formatted output to writer
|
||||
_, err := h.w.Write([]byte(builder.String()))
|
||||
return err
|
||||
}
|
||||
|
||||
// formatNamespace formats the namespace with ANSI color codes.
|
||||
// It supports FlatPath ([parent/child]) and NestedPath ([parent]→[child]) styles.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.formatNamespace(&builder, &lx.Entry{Namespace: "parent/child", Style: lx.FlatPath}) // Writes "[parent/child]: "
|
||||
func (h *ColorizedHandler) formatNamespace(b *strings.Builder, e *lx.Entry) {
|
||||
if e.Namespace == "" {
|
||||
return
|
||||
}
|
||||
|
||||
b.WriteString(lx.LeftBracket)
|
||||
switch e.Style {
|
||||
case lx.NestedPath:
|
||||
// Split namespace and format as [parent]→[child]
|
||||
parts := strings.Split(e.Namespace, lx.Slash)
|
||||
for i, part := range parts {
|
||||
b.WriteString(part)
|
||||
b.WriteString(lx.RightBracket)
|
||||
if i < len(parts)-1 {
|
||||
b.WriteString(lx.Arrow)
|
||||
b.WriteString(lx.LeftBracket)
|
||||
}
|
||||
}
|
||||
default: // FlatPath
|
||||
// Format as [parent/child]
|
||||
b.WriteString(e.Namespace)
|
||||
b.WriteString(lx.RightBracket)
|
||||
}
|
||||
b.WriteString(lx.Colon)
|
||||
b.WriteString(lx.Space)
|
||||
}
|
||||
|
||||
// formatLevel formats the log level with ANSI color codes.
|
||||
// It applies a color based on the level (Debug, Info, Warn, Error) and resets afterward.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.formatLevel(&builder, &lx.Entry{Level: lx.LevelInfo}) // Writes "<green>INFO<reset>: "
|
||||
func (h *ColorizedHandler) formatLevel(b *strings.Builder, e *lx.Entry) {
|
||||
// Map levels to colors
|
||||
color := map[lx.LevelType]string{
|
||||
lx.LevelDebug: h.palette.Debug, // Cyan
|
||||
lx.LevelInfo: h.palette.Info, // Green
|
||||
lx.LevelWarn: h.palette.Warn, // Yellow
|
||||
lx.LevelError: h.palette.Error, // Red
|
||||
}[e.Level]
|
||||
|
||||
b.WriteString(color)
|
||||
b.WriteString(e.Level.String())
|
||||
b.WriteString(h.palette.Reset)
|
||||
b.WriteString(lx.Colon)
|
||||
b.WriteString(lx.Space)
|
||||
}
|
||||
|
||||
// formatFields formats the log entry's fields in sorted order.
|
||||
// It writes fields as [key=value key=value], with no additional coloring.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.formatFields(&builder, &lx.Entry{Fields: map[string]interface{}{"key": "value"}}) // Writes " [key=value]"
|
||||
func (h *ColorizedHandler) formatFields(b *strings.Builder, e *lx.Entry) {
|
||||
if len(e.Fields) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect and sort field keys
|
||||
var keys []string
|
||||
for k := range e.Fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
b.WriteString(lx.Space)
|
||||
b.WriteString(lx.LeftBracket)
|
||||
// Format fields as key=value
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteString(lx.Space)
|
||||
}
|
||||
b.WriteString(k)
|
||||
b.WriteString("=")
|
||||
b.WriteString(fmt.Sprint(e.Fields[k]))
|
||||
}
|
||||
b.WriteString(lx.RightBracket)
|
||||
}
|
||||
|
||||
// formatStack formats a stack trace with ANSI color codes.
|
||||
// It structures the stack trace with colored goroutine, function, and path segments,
|
||||
// using indentation and separators for readability.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends colored stack trace
|
||||
func (h *ColorizedHandler) formatStack(b *strings.Builder, stack []byte) {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(h.palette.Header)
|
||||
b.WriteString("[stack]")
|
||||
b.WriteString(h.palette.Reset)
|
||||
b.WriteString("\n")
|
||||
|
||||
lines := strings.Split(string(stack), "\n")
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Format goroutine line
|
||||
b.WriteString(" ┌─ ")
|
||||
b.WriteString(h.palette.Goroutine)
|
||||
b.WriteString(lines[0])
|
||||
b.WriteString(h.palette.Reset)
|
||||
b.WriteString("\n")
|
||||
|
||||
// Pair function name and file path lines
|
||||
for i := 1; i < len(lines)-1; i += 2 {
|
||||
funcLine := strings.TrimSpace(lines[i])
|
||||
pathLine := strings.TrimSpace(lines[i+1])
|
||||
|
||||
if funcLine != "" {
|
||||
b.WriteString(" │ ")
|
||||
b.WriteString(h.palette.Func)
|
||||
b.WriteString(funcLine)
|
||||
b.WriteString(h.palette.Reset)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if pathLine != "" {
|
||||
b.WriteString(" │ ")
|
||||
|
||||
// Look for last "/" before ".go:"
|
||||
lastSlash := strings.LastIndex(pathLine, "/")
|
||||
goIndex := strings.Index(pathLine, ".go:")
|
||||
|
||||
if lastSlash >= 0 && goIndex > lastSlash {
|
||||
// Prefix path
|
||||
prefix := pathLine[:lastSlash+1]
|
||||
// File and line (e.g., ll.go:698 +0x5c)
|
||||
suffix := pathLine[lastSlash+1:]
|
||||
|
||||
b.WriteString(h.palette.Path)
|
||||
b.WriteString(prefix)
|
||||
b.WriteString(h.palette.Reset)
|
||||
|
||||
b.WriteString(h.palette.Path) // Use mainPath color for suffix
|
||||
b.WriteString(suffix)
|
||||
b.WriteString(h.palette.Reset)
|
||||
} else {
|
||||
// Fallback: whole line is gray
|
||||
b.WriteString(h.palette.Path)
|
||||
b.WriteString(pathLine)
|
||||
b.WriteString(h.palette.Reset)
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any remaining unpaired line
|
||||
if len(lines)%2 == 0 && strings.TrimSpace(lines[len(lines)-1]) != "" {
|
||||
b.WriteString(" │ ")
|
||||
b.WriteString(h.palette.Func)
|
||||
b.WriteString(strings.TrimSpace(lines[len(lines)-1]))
|
||||
b.WriteString(h.palette.Reset)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(" └\n")
|
||||
}
|
||||
|
||||
// handleDumpOutput formats hex dump output with ANSI color codes.
|
||||
// It applies colors to position, hex, ASCII, and title components of the dump,
|
||||
// wrapping the output with colored BEGIN/END separators.
|
||||
// Returns an error if writing fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes colored dump
|
||||
func (h *ColorizedHandler) handleDumpOutput(e *lx.Entry) error {
|
||||
var builder strings.Builder
|
||||
|
||||
// Add timestamp if enabled
|
||||
if h.showTime {
|
||||
builder.WriteString(e.Timestamp.Format(h.timeFormat))
|
||||
builder.WriteString(lx.Newline)
|
||||
}
|
||||
|
||||
// Write colored BEGIN separator
|
||||
builder.WriteString(h.palette.Title)
|
||||
builder.WriteString("---- BEGIN DUMP ----")
|
||||
builder.WriteString(h.palette.Reset)
|
||||
builder.WriteString("\n")
|
||||
|
||||
// Process each line of the dump
|
||||
lines := strings.Split(e.Message, "\n")
|
||||
length := len(lines)
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, "pos ") {
|
||||
// Parse and color position and hex/ASCII parts
|
||||
parts := strings.SplitN(line, "hex:", 2)
|
||||
if len(parts) == 2 {
|
||||
builder.WriteString(h.palette.Pos)
|
||||
builder.WriteString(parts[0])
|
||||
builder.WriteString(h.palette.Reset)
|
||||
|
||||
hexAscii := strings.SplitN(parts[1], "'", 2)
|
||||
builder.WriteString(h.palette.Hex)
|
||||
builder.WriteString("hex:")
|
||||
builder.WriteString(hexAscii[0])
|
||||
builder.WriteString(h.palette.Reset)
|
||||
|
||||
if len(hexAscii) > 1 {
|
||||
builder.WriteString(h.palette.Ascii)
|
||||
builder.WriteString("'")
|
||||
builder.WriteString(hexAscii[1])
|
||||
builder.WriteString(h.palette.Reset)
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(line, "Dumping value of type:") {
|
||||
// Color type dump lines
|
||||
builder.WriteString(h.palette.Header)
|
||||
builder.WriteString(line)
|
||||
builder.WriteString(h.palette.Reset)
|
||||
} else {
|
||||
// Write non-dump lines as-is
|
||||
builder.WriteString(line)
|
||||
}
|
||||
|
||||
// Don't add newline for the last line
|
||||
if i < length-1 {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Write colored END separator
|
||||
builder.WriteString(h.palette.Title)
|
||||
builder.WriteString("---- END DUMP ----")
|
||||
builder.WriteString(h.palette.Reset)
|
||||
builder.WriteString("\n")
|
||||
|
||||
// Write formatted output to writer
|
||||
_, err := h.w.Write([]byte(builder.String()))
|
||||
return err
|
||||
}
|
||||
|
||||
// detectPalette selects a color palette based on terminal environment variables.
|
||||
// It checks TERM_BACKGROUND, COLORFGBG, and AppleInterfaceStyle to determine
|
||||
// whether a light or dark palette is appropriate, defaulting to darkPalette.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// palette := h.detectPalette() // Returns darkPalette or lightPalette
|
||||
func (h *ColorizedHandler) detectPalette() Palette {
|
||||
// Check TERM_BACKGROUND (e.g., iTerm2)
|
||||
if bg, ok := os.LookupEnv("TERM_BACKGROUND"); ok {
|
||||
if bg == "light" {
|
||||
return lightPalette // Use light palette for light background
|
||||
}
|
||||
return darkPalette // Use dark palette otherwise
|
||||
}
|
||||
|
||||
// Check COLORFGBG (traditional xterm)
|
||||
if fgBg, ok := os.LookupEnv("COLORFGBG"); ok {
|
||||
parts := strings.Split(fgBg, ";")
|
||||
if len(parts) >= 2 {
|
||||
bg := parts[len(parts)-1] // Last part (some terminals add more fields)
|
||||
if bg == "7" || bg == "15" || bg == "0;15" { // Handle variations
|
||||
return lightPalette // Use light palette for light background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check macOS dark mode
|
||||
if style, ok := os.LookupEnv("AppleInterfaceStyle"); ok && strings.EqualFold(style, "dark") {
|
||||
return darkPalette // Use dark palette for macOS dark mode
|
||||
}
|
||||
|
||||
// Default: dark (conservative choice for terminals)
|
||||
return darkPalette
|
||||
}
|
||||
170
vendor/github.com/olekukonko/ll/lh/json.go
generated
vendored
Normal file
170
vendor/github.com/olekukonko/ll/lh/json.go
generated
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSONHandler is a handler that outputs log entries as JSON objects.
|
||||
// It formats log entries with timestamp, level, message, namespace, fields, and optional
|
||||
// stack traces or dump segments, writing the result to the provided writer.
|
||||
// Thread-safe with a mutex to protect concurrent writes.
|
||||
type JSONHandler struct {
|
||||
writer io.Writer // Destination for JSON output
|
||||
timeFmt string // Format for timestamp (default: RFC3339Nano)
|
||||
pretty bool // Enable pretty printing with indentation if true
|
||||
fieldMap map[string]string // Optional mapping for field names (not used in provided code)
|
||||
mu sync.Mutex // Protects concurrent access to writer
|
||||
}
|
||||
|
||||
// JsonOutput represents the JSON structure for a log entry.
|
||||
// It includes all relevant log data, such as timestamp, level, message, and optional
|
||||
// stack trace or dump segments, serialized as a JSON object.
|
||||
type JsonOutput struct {
|
||||
Time string `json:"ts"` // Timestamp in specified format
|
||||
Level string `json:"lvl"` // Log level (e.g., "INFO")
|
||||
Class string `json:"class"` // Entry class (e.g., "Text", "Dump")
|
||||
Msg string `json:"msg"` // Log message
|
||||
Namespace string `json:"ns"` // Namespace path
|
||||
Stack []byte `json:"stack"` // Stack trace (if present)
|
||||
Dump []dumpSegment `json:"dump"` // Hex/ASCII dump segments (for ClassDump)
|
||||
Fields map[string]interface{} `json:"fields"` // Custom fields
|
||||
}
|
||||
|
||||
// dumpSegment represents a single segment of a hex/ASCII dump.
|
||||
// Used for ClassDump entries to structure position, hex values, and ASCII representation.
|
||||
type dumpSegment struct {
|
||||
Offset int `json:"offset"` // Starting byte offset of the segment
|
||||
Hex []string `json:"hex"` // Hexadecimal values of bytes
|
||||
ASCII string `json:"ascii"` // ASCII representation of bytes
|
||||
}
|
||||
|
||||
// NewJSONHandler creates a new JSONHandler writing to the specified writer.
|
||||
// It initializes the handler with a default timestamp format (RFC3339Nano) and optional
|
||||
// configuration functions to customize settings like pretty printing.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewJSONHandler(os.Stdout)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: {"ts":"...","lvl":"INFO","class":"Text","msg":"Test","ns":"app","stack":null,"dump":null,"fields":null}
|
||||
func NewJSONHandler(w io.Writer, opts ...func(*JSONHandler)) *JSONHandler {
|
||||
h := &JSONHandler{
|
||||
writer: w, // Set output writer
|
||||
timeFmt: time.RFC3339Nano, // Default timestamp format
|
||||
}
|
||||
// Apply configuration options
|
||||
for _, opt := range opts {
|
||||
opt(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Handle processes a log entry and writes it as JSON.
|
||||
// It delegates to specialized methods based on the entry's class (Dump or regular),
|
||||
// ensuring thread-safety with a mutex.
|
||||
// Returns an error if JSON encoding or writing fails.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
|
||||
func (h *JSONHandler) Handle(e *lx.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Handle dump entries separately
|
||||
if e.Class == lx.ClassDump {
|
||||
return h.handleDump(e)
|
||||
}
|
||||
// Handle standard log entries
|
||||
return h.handleRegular(e)
|
||||
}
|
||||
|
||||
// handleRegular handles standard log entries (non-dump).
|
||||
// It converts the entry to a JsonOutput struct and encodes it as JSON,
|
||||
// applying pretty printing if enabled. Logs encoding errors to stderr for debugging.
|
||||
// Returns an error if encoding or writing fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleRegular(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes JSON object
|
||||
func (h *JSONHandler) handleRegular(e *lx.Entry) error {
|
||||
// Create JSON output structure
|
||||
entry := JsonOutput{
|
||||
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
|
||||
Level: e.Level.String(), // Convert level to string
|
||||
Class: e.Class.String(), // Convert class to string
|
||||
Msg: e.Message, // Set message
|
||||
Namespace: e.Namespace, // Set namespace
|
||||
Dump: nil, // No dump for regular entries
|
||||
Fields: e.Fields, // Copy fields
|
||||
Stack: e.Stack, // Include stack trace if present
|
||||
}
|
||||
// Create JSON encoder
|
||||
enc := json.NewEncoder(h.writer)
|
||||
if h.pretty {
|
||||
// Enable indentation for pretty printing
|
||||
enc.SetIndent("", " ")
|
||||
}
|
||||
// Log encoding attempt for debugging
|
||||
fmt.Fprintf(os.Stderr, "Encoding JSON entry: %v\n", e.Message)
|
||||
// Encode and write JSON
|
||||
err := enc.Encode(entry)
|
||||
if err != nil {
|
||||
// Log encoding error for debugging
|
||||
fmt.Fprintf(os.Stderr, "JSON encode error: %v\n", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// handleDump processes ClassDump entries, converting hex dump output to JSON segments.
|
||||
// It parses the dump message into structured segments with offset, hex, and ASCII data,
|
||||
// encoding them as a JsonOutput struct.
|
||||
// Returns an error if parsing or encoding fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleDump(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes JSON with dump segments
|
||||
func (h *JSONHandler) handleDump(e *lx.Entry) error {
|
||||
var segments []dumpSegment
|
||||
lines := strings.Split(e.Message, "\n")
|
||||
|
||||
// Parse each line of the dump message
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, "pos") {
|
||||
continue // Skip non-dump lines
|
||||
}
|
||||
parts := strings.SplitN(line, "hex:", 2)
|
||||
if len(parts) != 2 {
|
||||
continue // Skip invalid lines
|
||||
}
|
||||
// Parse position
|
||||
var offset int
|
||||
fmt.Sscanf(parts[0], "pos %d", &offset)
|
||||
|
||||
// Parse hex and ASCII
|
||||
hexAscii := strings.SplitN(parts[1], "'", 2)
|
||||
hexStr := strings.Fields(strings.TrimSpace(hexAscii[0]))
|
||||
|
||||
// Create dump segment
|
||||
segments = append(segments, dumpSegment{
|
||||
Offset: offset, // Set byte offset
|
||||
Hex: hexStr, // Set hex values
|
||||
ASCII: strings.Trim(hexAscii[1], "'"), // Set ASCII representation
|
||||
})
|
||||
}
|
||||
|
||||
// Encode JSON output with dump segments
|
||||
return json.NewEncoder(h.writer).Encode(JsonOutput{
|
||||
Time: e.Timestamp.Format(h.timeFmt), // Format timestamp
|
||||
Level: e.Level.String(), // Convert level to string
|
||||
Class: e.Class.String(), // Convert class to string
|
||||
Msg: "dumping segments", // Fixed message for dumps
|
||||
Namespace: e.Namespace, // Set namespace
|
||||
Dump: segments, // Include parsed segments
|
||||
Fields: e.Fields, // Copy fields
|
||||
Stack: e.Stack, // Include stack trace if present
|
||||
})
|
||||
}
|
||||
113
vendor/github.com/olekukonko/ll/lh/memory.go
generated
vendored
Normal file
113
vendor/github.com/olekukonko/ll/lh/memory.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MemoryHandler is an lx.Handler that stores log entries in memory.
|
||||
// Useful for testing or buffering logs for later inspection.
|
||||
// It maintains a thread-safe slice of log entries, protected by a read-write mutex.
|
||||
type MemoryHandler struct {
|
||||
mu sync.RWMutex // Protects concurrent access to entries
|
||||
entries []*lx.Entry // Slice of stored log entries
|
||||
showTime bool // Whether to show timestamps when dumping
|
||||
timeFormat string // Time format for dumping
|
||||
}
|
||||
|
||||
// NewMemoryHandler creates a new MemoryHandler.
|
||||
// It initializes an empty slice for storing log entries, ready for use in logging or testing.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewMemoryHandler()
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Stores entry in memory
|
||||
func NewMemoryHandler() *MemoryHandler {
|
||||
return &MemoryHandler{
|
||||
entries: make([]*lx.Entry, 0), // Initialize empty slice for entries
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamped enables/disables timestamp display when dumping and optionally sets a time format.
|
||||
// Consistent with TextHandler and ColorizedHandler signature.
|
||||
// Example:
|
||||
//
|
||||
// handler.Timestamped(true) // Enable with default format
|
||||
// handler.Timestamped(true, time.StampMilli) // Enable with custom format
|
||||
// handler.Timestamped(false) // Disable
|
||||
func (h *MemoryHandler) Timestamped(enable bool, format ...string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.showTime = enable
|
||||
if len(format) > 0 && format[0] != "" {
|
||||
h.timeFormat = format[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stores the log entry in memory.
|
||||
// It appends the provided entry to the entries slice, ensuring thread-safety with a write lock.
|
||||
// Always returns nil, as it does not perform I/O operations.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Stores entry
|
||||
func (h *MemoryHandler) Handle(entry *lx.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.entries = append(h.entries, entry) // Append entry to slice
|
||||
return nil
|
||||
}
|
||||
|
||||
// Entries returns a copy of the stored log entries.
|
||||
// It creates a new slice with copies of all entries, ensuring thread-safety with a read lock.
|
||||
// The returned slice is safe for external use without affecting the handler's internal state.
|
||||
// Example:
|
||||
//
|
||||
// entries := handler.Entries() // Returns copy of stored entries
|
||||
func (h *MemoryHandler) Entries() []*lx.Entry {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
entries := make([]*lx.Entry, len(h.entries)) // Create new slice for copy
|
||||
copy(entries, h.entries) // Copy entries to new slice
|
||||
return entries
|
||||
}
|
||||
|
||||
// Reset clears all stored entries.
|
||||
// It truncates the entries slice to zero length, preserving capacity, using a write lock for thread-safety.
|
||||
// Example:
|
||||
//
|
||||
// handler.Reset() // Clears all stored entries
|
||||
func (h *MemoryHandler) Reset() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.entries = h.entries[:0] // Truncate slice to zero length
|
||||
}
|
||||
|
||||
// Dump writes all stored log entries to the provided io.Writer in text format.
|
||||
// Entries are formatted as they would be by a TextHandler, including namespace, level,
|
||||
// message, and fields. Thread-safe with read lock.
|
||||
// Returns an error if writing fails.
|
||||
// Example:
|
||||
//
|
||||
// logger := ll.New("test", ll.WithHandler(NewMemoryHandler())).Enable()
|
||||
// logger.Info("Test message")
|
||||
// handler := logger.handler.(*MemoryHandler)
|
||||
// handler.Dump(os.Stdout) // Output: [test] INFO: Test message
|
||||
func (h *MemoryHandler) Dump(w io.Writer) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
// Create a temporary TextHandler to format entries
|
||||
tempHandler := NewTextHandler(w)
|
||||
tempHandler.Timestamped(h.showTime, h.timeFormat)
|
||||
|
||||
// Process each entry through the TextHandler
|
||||
for _, entry := range h.entries {
|
||||
if err := tempHandler.Handle(entry); err != nil {
|
||||
return fmt.Errorf("failed to dump entry: %w", err) // Wrap and return write errors
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
51
vendor/github.com/olekukonko/ll/lh/multi.go
generated
vendored
Normal file
51
vendor/github.com/olekukonko/ll/lh/multi.go
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// MultiHandler combines multiple handlers to process log entries concurrently.
|
||||
// It holds a list of lx.Handler instances and delegates each log entry to all handlers,
|
||||
// collecting any errors into a single combined error.
|
||||
// Thread-safe if the underlying handlers are thread-safe.
|
||||
type MultiHandler struct {
|
||||
Handlers []lx.Handler // List of handlers to process each log entry
|
||||
}
|
||||
|
||||
// NewMultiHandler creates a new MultiHandler with the specified handlers.
|
||||
// It accepts a variadic list of handlers to be executed in order.
|
||||
// The returned handler processes log entries by passing them to each handler in sequence.
|
||||
// Example:
|
||||
//
|
||||
// textHandler := NewTextHandler(os.Stdout)
|
||||
// jsonHandler := NewJSONHandler(os.Stdout)
|
||||
// multi := NewMultiHandler(textHandler, jsonHandler)
|
||||
// logger := ll.New("app").Enable().Handler(multi)
|
||||
// logger.Info("Test") // Processed by both text and JSON handlers
|
||||
func NewMultiHandler(h ...lx.Handler) *MultiHandler {
|
||||
return &MultiHandler{
|
||||
Handlers: h, // Initialize with provided handlers
|
||||
}
|
||||
}
|
||||
|
||||
// Handle implements the Handler interface, calling Handle on each handler in sequence.
|
||||
// It collects any errors from handlers and combines them into a single error using errors.Join.
|
||||
// If no errors occur, it returns nil. Thread-safe if the underlying handlers are thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// multi.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Calls Handle on all handlers
|
||||
func (h *MultiHandler) Handle(e *lx.Entry) error {
|
||||
var errs []error // Collect errors from handlers
|
||||
for i, handler := range h.Handlers {
|
||||
// Process entry with each handler
|
||||
if err := handler.Handle(e); err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "MultiHandler error for handler %d: %v\n", i, err)
|
||||
// Wrap error with handler index for context
|
||||
errs = append(errs, fmt.Errorf("handler %d: %w", i, err))
|
||||
}
|
||||
}
|
||||
// Combine errors into a single error, or return nil if no errors
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
89
vendor/github.com/olekukonko/ll/lh/slog.go
generated
vendored
Normal file
89
vendor/github.com/olekukonko/ll/lh/slog.go
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// SlogHandler adapts a slog.Handler to implement lx.Handler.
|
||||
// It converts lx.Entry objects to slog.Record objects and delegates to an underlying
|
||||
// slog.Handler for processing, enabling compatibility with Go's standard slog package.
|
||||
// Thread-safe if the underlying slog.Handler is thread-safe.
|
||||
type SlogHandler struct {
|
||||
slogHandler slog.Handler // Underlying slog.Handler for processing log records
|
||||
}
|
||||
|
||||
// NewSlogHandler creates a new SlogHandler wrapping the provided slog.Handler.
|
||||
// It initializes the handler with the given slog.Handler, allowing lx.Entry logs to be
|
||||
// processed by slog's logging infrastructure.
|
||||
// Example:
|
||||
//
|
||||
// slogText := slog.NewTextHandler(os.Stdout, nil)
|
||||
// handler := NewSlogHandler(slogText)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: level=INFO msg=Test namespace=app class=Text
|
||||
func NewSlogHandler(h slog.Handler) *SlogHandler {
|
||||
return &SlogHandler{slogHandler: h}
|
||||
}
|
||||
|
||||
// Handle converts an lx.Entry to slog.Record and delegates to the slog.Handler.
|
||||
// It maps the entry's fields, level, namespace, class, and stack trace to slog attributes,
|
||||
// passing the resulting record to the underlying slog.Handler.
|
||||
// Returns an error if the slog.Handler fails to process the record.
|
||||
// Thread-safe if the underlying slog.Handler is thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Processes as slog record
|
||||
func (h *SlogHandler) Handle(e *lx.Entry) error {
|
||||
// Convert lx.LevelType to slog.Level
|
||||
level := toSlogLevel(e.Level)
|
||||
|
||||
// Create a slog.Record with the entry's data
|
||||
record := slog.NewRecord(
|
||||
e.Timestamp, // time.Time for log timestamp
|
||||
level, // slog.Level for log severity
|
||||
e.Message, // string for log message
|
||||
0, // pc (program counter, optional, not used)
|
||||
)
|
||||
|
||||
// Add standard fields as attributes
|
||||
record.AddAttrs(
|
||||
slog.String("namespace", e.Namespace), // Add namespace as string attribute
|
||||
slog.String("class", e.Class.String()), // Add class as string attribute
|
||||
)
|
||||
|
||||
// Add stack trace if present
|
||||
if len(e.Stack) > 0 {
|
||||
record.AddAttrs(slog.String("stack", string(e.Stack))) // Add stack trace as string
|
||||
}
|
||||
|
||||
// Add custom fields
|
||||
for k, v := range e.Fields {
|
||||
record.AddAttrs(slog.Any(k, v)) // Add each field as a key-value attribute
|
||||
}
|
||||
|
||||
// Handle the record with the underlying slog.Handler
|
||||
return h.slogHandler.Handle(context.Background(), record)
|
||||
}
|
||||
|
||||
// toSlogLevel converts lx.LevelType to slog.Level.
|
||||
// It maps the logging levels used by the lx package to those used by slog,
|
||||
// defaulting to slog.LevelInfo for unknown levels.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// level := toSlogLevel(lx.LevelDebug) // Returns slog.LevelDebug
|
||||
func toSlogLevel(level lx.LevelType) slog.Level {
|
||||
switch level {
|
||||
case lx.LevelDebug:
|
||||
return slog.LevelDebug
|
||||
case lx.LevelInfo:
|
||||
return slog.LevelInfo
|
||||
case lx.LevelWarn:
|
||||
return slog.LevelWarn
|
||||
case lx.LevelError:
|
||||
return slog.LevelError
|
||||
default:
|
||||
return slog.LevelInfo // Default for unknown levels
|
||||
}
|
||||
}
|
||||
225
vendor/github.com/olekukonko/ll/lh/text.go
generated
vendored
Normal file
225
vendor/github.com/olekukonko/ll/lh/text.go
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TextHandler is a handler that outputs log entries as plain text.
|
||||
// It formats log entries with namespace, level, message, fields, and optional stack traces,
|
||||
// writing the result to the provided writer.
|
||||
// Thread-safe if the underlying writer is thread-safe.
|
||||
type TextHandler struct {
|
||||
w io.Writer // Destination for formatted log output
|
||||
showTime bool // Whether to display timestamps
|
||||
timeFormat string // Format for timestamps (defaults to time.RFC3339)
|
||||
}
|
||||
|
||||
// NewTextHandler creates a new TextHandler writing to the specified writer.
|
||||
// It initializes the handler with the given writer, suitable for outputs like stdout or files.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewTextHandler(os.Stdout)
|
||||
// logger := ll.New("app").Enable().Handler(handler)
|
||||
// logger.Info("Test") // Output: [app] INFO: Test
|
||||
func NewTextHandler(w io.Writer) *TextHandler {
|
||||
return &TextHandler{
|
||||
w: w,
|
||||
showTime: false,
|
||||
timeFormat: time.RFC3339,
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamped enables or disables timestamp display and optionally sets a custom time format.
|
||||
// If format is empty, defaults to RFC3339.
|
||||
// Example:
|
||||
//
|
||||
// handler := NewTextHandler(os.Stdout).TextWithTime(true, time.StampMilli)
|
||||
// // Output: Jan 02 15:04:05.000 [app] INFO: Test
|
||||
func (h *TextHandler) Timestamped(enable bool, format ...string) {
|
||||
h.showTime = enable
|
||||
if len(format) > 0 && format[0] != "" {
|
||||
h.timeFormat = format[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Handle processes a log entry and writes it as plain text.
|
||||
// It delegates to specialized methods based on the entry's class (Dump, Raw, or regular).
|
||||
// Returns an error if writing to the underlying writer fails.
|
||||
// Thread-safe if the writer is thread-safe.
|
||||
// Example:
|
||||
//
|
||||
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
|
||||
func (h *TextHandler) Handle(e *lx.Entry) error {
|
||||
// Special handling for dump output
|
||||
if e.Class == lx.ClassDump {
|
||||
return h.handleDumpOutput(e)
|
||||
}
|
||||
|
||||
// Raw entries are written directly without formatting
|
||||
if e.Class == lx.ClassRaw {
|
||||
_, err := h.w.Write([]byte(e.Message))
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle standard log entries
|
||||
return h.handleRegularOutput(e)
|
||||
}
|
||||
|
||||
// handleRegularOutput handles normal log entries.
|
||||
// It formats the entry with namespace, level, message, fields, and stack trace (if present),
|
||||
// writing the result to the handler's writer.
|
||||
// Returns an error if writing fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes "INFO: test"
|
||||
func (h *TextHandler) handleRegularOutput(e *lx.Entry) error {
|
||||
var builder strings.Builder // Buffer for building formatted output
|
||||
|
||||
// Add timestamp if enabled
|
||||
if h.showTime {
|
||||
builder.WriteString(e.Timestamp.Format(h.timeFormat))
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
|
||||
// Format namespace based on style
|
||||
switch e.Style {
|
||||
case lx.NestedPath:
|
||||
if e.Namespace != "" {
|
||||
// Split namespace into parts and format as [parent]→[child]
|
||||
parts := strings.Split(e.Namespace, lx.Slash)
|
||||
for i, part := range parts {
|
||||
builder.WriteString(lx.LeftBracket)
|
||||
builder.WriteString(part)
|
||||
builder.WriteString(lx.RightBracket)
|
||||
if i < len(parts)-1 {
|
||||
builder.WriteString(lx.Arrow)
|
||||
}
|
||||
}
|
||||
builder.WriteString(lx.Colon)
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
default: // FlatPath
|
||||
if e.Namespace != "" {
|
||||
// Format namespace as [parent/child]
|
||||
builder.WriteString(lx.LeftBracket)
|
||||
builder.WriteString(e.Namespace)
|
||||
builder.WriteString(lx.RightBracket)
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
}
|
||||
|
||||
// Add level and message
|
||||
builder.WriteString(e.Level.String())
|
||||
builder.WriteString(lx.Colon)
|
||||
builder.WriteString(lx.Space)
|
||||
builder.WriteString(e.Message)
|
||||
|
||||
// Add fields in sorted order
|
||||
if len(e.Fields) > 0 {
|
||||
var keys []string
|
||||
for k := range e.Fields {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
// Sort keys for consistent output
|
||||
sort.Strings(keys)
|
||||
builder.WriteString(lx.Space)
|
||||
builder.WriteString(lx.LeftBracket)
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
builder.WriteString(lx.Space)
|
||||
}
|
||||
// Format field as key=value
|
||||
builder.WriteString(k)
|
||||
builder.WriteString("=")
|
||||
builder.WriteString(fmt.Sprint(e.Fields[k]))
|
||||
}
|
||||
builder.WriteString(lx.RightBracket)
|
||||
}
|
||||
|
||||
// Add stack trace if present
|
||||
if len(e.Stack) > 0 {
|
||||
h.formatStack(&builder, e.Stack)
|
||||
}
|
||||
|
||||
// Append newline for non-None levels
|
||||
if e.Level != lx.LevelNone {
|
||||
builder.WriteString(lx.Newline)
|
||||
}
|
||||
|
||||
// Write formatted output to writer
|
||||
_, err := h.w.Write([]byte(builder.String()))
|
||||
return err
|
||||
}
|
||||
|
||||
// handleDumpOutput specially formats hex dump output (plain text version).
|
||||
// It wraps the dump message with BEGIN/END separators for clarity.
|
||||
// Returns an error if writing fails.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61"}) // Writes "---- BEGIN DUMP ----\npos 00 hex: 61\n---- END DUMP ----\n"
|
||||
func (h *TextHandler) handleDumpOutput(e *lx.Entry) error {
|
||||
// For text handler, we just add a newline before dump output
|
||||
var builder strings.Builder // Buffer for building formatted output
|
||||
|
||||
// Add timestamp if enabled
|
||||
if h.showTime {
|
||||
builder.WriteString(e.Timestamp.Format(h.timeFormat))
|
||||
builder.WriteString(lx.Newline)
|
||||
}
|
||||
|
||||
// Add separator lines and dump content
|
||||
builder.WriteString("---- BEGIN DUMP ----\n")
|
||||
builder.WriteString(e.Message)
|
||||
builder.WriteString("---- END DUMP ----\n")
|
||||
|
||||
// Write formatted output to writer
|
||||
_, err := h.w.Write([]byte(builder.String()))
|
||||
return err
|
||||
}
|
||||
|
||||
// formatStack formats a stack trace for plain text output.
|
||||
// It structures the stack trace with indentation and separators for readability,
|
||||
// including goroutine and function/file details.
|
||||
// Example (internal usage):
|
||||
//
|
||||
// h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends formatted stack trace
|
||||
func (h *TextHandler) formatStack(b *strings.Builder, stack []byte) {
|
||||
lines := strings.Split(string(stack), "\n")
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Start stack trace section
|
||||
b.WriteString("\n[stack]\n")
|
||||
|
||||
// First line: goroutine
|
||||
b.WriteString(" ┌─ ")
|
||||
b.WriteString(lines[0])
|
||||
b.WriteString("\n")
|
||||
|
||||
// Iterate through remaining lines
|
||||
for i := 1; i < len(lines); i++ {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, ".go") {
|
||||
// File path lines get extra indent
|
||||
b.WriteString(" ├ ")
|
||||
} else {
|
||||
// Function names
|
||||
b.WriteString(" │ ")
|
||||
}
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// End stack trace section
|
||||
b.WriteString(" └\n")
|
||||
}
|
||||
1448
vendor/github.com/olekukonko/ll/ll.go
generated
vendored
Normal file
1448
vendor/github.com/olekukonko/ll/ll.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
164
vendor/github.com/olekukonko/ll/lx/lx.go
generated
vendored
Normal file
164
vendor/github.com/olekukonko/ll/lx/lx.go
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Formatting constants for log output.
|
||||
// These constants define the characters used to format log messages, ensuring consistency
|
||||
// across handlers (e.g., text, JSON, colorized). They are used to construct namespace paths,
|
||||
// level indicators, and field separators in log entries.
|
||||
const (
|
||||
Space = " " // Single space for separating elements (e.g., between level and message)
|
||||
DoubleSpace = " " // Double space for indentation (e.g., for hierarchical output)
|
||||
Slash = "/" // Separator for namespace paths (e.g., "parent/child")
|
||||
Arrow = "→" // Arrow for NestedPath style namespaces (e.g., [parent]→[child])
|
||||
LeftBracket = "[" // Opening bracket for namespaces and fields (e.g., [app])
|
||||
RightBracket = "]" // Closing bracket for namespaces and fields (e.g., [app])
|
||||
Colon = ":" // Separator after namespace or level (e.g., [app]: INFO:)
|
||||
Dot = "." // Separator for namespace paths (e.g., "parent.child")
|
||||
Newline = "\n" // Newline for separating log entries or stack trace lines
|
||||
)
|
||||
|
||||
// DefaultEnabled defines the default logging state (disabled).
|
||||
// It specifies whether logging is enabled by default for new Logger instances in the ll package.
|
||||
// Set to false to prevent logging until explicitly enabled.
|
||||
const (
|
||||
DefaultEnabled = false // Default state for new loggers (disabled)
|
||||
)
|
||||
|
||||
// Log level constants, ordered by increasing severity.
|
||||
// These constants define the severity levels for log messages, used to filter logs based
|
||||
// on the logger’s minimum level. They are ordered to allow comparison (e.g., LevelDebug < LevelWarn).
|
||||
const (
|
||||
LevelNone LevelType = iota // Debug level for detailed diagnostic information
|
||||
LevelInfo // Info level for general operational messages
|
||||
LevelWarn // Warn level for warning conditions
|
||||
LevelError // Error level for error conditions requiring attention
|
||||
LevelDebug // None level for logs without a specific severity (e.g., raw output)
|
||||
)
|
||||
|
||||
// Log class constants, defining the type of log entry.
|
||||
// These constants categorize log entries by their content or purpose, influencing how
|
||||
// handlers process them (e.g., text, JSON, hex dump).
|
||||
const (
|
||||
ClassText ClassType = iota // Text entries for standard log messages
|
||||
ClassJSON // JSON entries for structured output
|
||||
ClassDump // Dump entries for hex/ASCII dumps
|
||||
ClassSpecial // Special entries for custom or non-standard logs
|
||||
ClassRaw // Raw entries for unformatted output
|
||||
)
|
||||
|
||||
// Namespace style constants.
|
||||
// These constants define how namespace paths are formatted in log output, affecting the
|
||||
// visual representation of hierarchical namespaces.
|
||||
const (
|
||||
FlatPath StyleType = iota // Formats namespaces as [parent/child]
|
||||
NestedPath // Formats namespaces as [parent]→[child]
|
||||
)
|
||||
|
||||
// LevelType represents the severity of a log message.
|
||||
// It is an integer type used to define log levels (Debug, Info, Warn, Error, None), with associated
|
||||
// string representations for display in log output.
|
||||
type LevelType int
|
||||
|
||||
// String converts a LevelType to its string representation.
|
||||
// It maps each level constant to a human-readable string, returning "UNKNOWN" for invalid levels.
|
||||
// Used by handlers to display the log level in output.
|
||||
// Example:
|
||||
//
|
||||
// var level lx.LevelType = lx.LevelInfo
|
||||
// fmt.Println(level.String()) // Output: INFO
|
||||
func (l LevelType) String() string {
|
||||
switch l {
|
||||
case LevelDebug:
|
||||
return "DEBUG"
|
||||
case LevelInfo:
|
||||
return "INFO"
|
||||
case LevelWarn:
|
||||
return "WARN"
|
||||
case LevelError:
|
||||
return "ERROR"
|
||||
case LevelNone:
|
||||
return "NONE"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// StyleType defines how namespace paths are formatted in log output.
|
||||
// It is an integer type used to select between FlatPath ([parent/child]) and NestedPath
|
||||
// ([parent]→[child]) styles, affecting how handlers render namespace hierarchies.
|
||||
type StyleType int
|
||||
|
||||
// Entry represents a single log entry passed to handlers.
|
||||
// It encapsulates all information about a log message, including its timestamp, severity,
|
||||
// content, namespace, metadata, and formatting style. Handlers process Entry instances
|
||||
// to produce formatted output (e.g., text, JSON). The struct is immutable once created,
|
||||
// ensuring thread-safety in handler processing.
|
||||
type Entry struct {
|
||||
Timestamp time.Time // Time the log was created
|
||||
Level LevelType // Severity level of the log (Debug, Info, Warn, Error, None)
|
||||
Message string // Log message content
|
||||
Namespace string // Namespace path (e.g., "parent/child")
|
||||
Fields map[string]interface{} // Additional key-value metadata (e.g., {"user": "alice"})
|
||||
Style StyleType // Namespace formatting style (FlatPath or NestedPath)
|
||||
Error error // Associated error, if any (e.g., for error logs)
|
||||
Class ClassType // Type of log entry (Text, JSON, Dump, Special, Raw)
|
||||
Stack []byte // Stack trace data (if present)
|
||||
Id int `json:"-"` // Unique ID for the entry, ignored in JSON output
|
||||
}
|
||||
|
||||
// Handler defines the interface for processing log entries.
|
||||
// Implementations (e.g., TextHandler, JSONHandler) format and output log entries to various
|
||||
// destinations (e.g., stdout, files). The Handle method returns an error if processing fails,
|
||||
// allowing the logger to handle output failures gracefully.
|
||||
// Example (simplified handler implementation):
|
||||
//
|
||||
// type MyHandler struct{}
|
||||
// func (h *MyHandler) Handle(e *Entry) error {
|
||||
// fmt.Printf("[%s] %s: %s\n", e.Namespace, e.Level.String(), e.Message)
|
||||
// return nil
|
||||
// }
|
||||
type Handler interface {
|
||||
Handle(e *Entry) error // Processes a log entry, returning any error
|
||||
}
|
||||
|
||||
// Timestamper defines an interface for handlers that support timestamp configuration.
|
||||
// It includes a method to enable or disable timestamp logging and optionally set the timestamp format.
|
||||
type Timestamper interface {
|
||||
// Timestamped enables or disables timestamp logging and allows specifying an optional format.
|
||||
// Parameters:
|
||||
// enable: Boolean to enable or disable timestamp logging
|
||||
// format: Optional string(s) to specify the timestamp format
|
||||
Timestamped(enable bool, format ...string)
|
||||
}
|
||||
|
||||
// ClassType represents the type of a log entry.
|
||||
// It is an integer type used to categorize log entries (Text, JSON, Dump, Special, Raw),
|
||||
// influencing how handlers process and format them.
|
||||
type ClassType int
|
||||
|
||||
// String converts a ClassType to its string representation.
|
||||
// It maps each class constant to a human-readable string, returning "UNKNOWN" for invalid classes.
|
||||
// Used by handlers to indicate the entry type in output (e.g., JSON fields).
|
||||
// Example:
|
||||
//
|
||||
// var class lx.ClassType = lx.ClassText
|
||||
// fmt.Println(class.String()) // Output: TEST
|
||||
func (t ClassType) String() string {
|
||||
switch t {
|
||||
case ClassText:
|
||||
return "TEST" // Note: Likely a typo, should be "TEXT"
|
||||
case ClassJSON:
|
||||
return "JSON"
|
||||
case ClassDump:
|
||||
return "DUMP"
|
||||
case ClassSpecial:
|
||||
return "SPECIAL"
|
||||
case ClassRaw:
|
||||
return "RAW"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
102
vendor/github.com/olekukonko/ll/lx/ns.go
generated
vendored
Normal file
102
vendor/github.com/olekukonko/ll/lx/ns.go
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
package lx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// namespaceRule stores the cached result of Enabled.
|
||||
type namespaceRule struct {
|
||||
isEnabledByRule bool
|
||||
isDisabledByRule bool
|
||||
}
|
||||
|
||||
// Namespace manages thread-safe namespace enable/disable states with caching.
|
||||
// The store holds explicit user-defined rules (path -> bool).
|
||||
// The cache holds computed effective states for paths (path -> namespaceRule)
|
||||
// based on hierarchical rules to optimize lookups.
|
||||
type Namespace struct {
|
||||
store sync.Map // path (string) -> rule (bool: true=enable, false=disable)
|
||||
cache sync.Map // path (string) -> namespaceRule
|
||||
}
|
||||
|
||||
// Set defines an explicit enable/disable rule for a namespace path.
|
||||
// It clears the cache to ensure subsequent lookups reflect the change.
|
||||
func (ns *Namespace) Set(path string, enabled bool) {
|
||||
ns.store.Store(path, enabled)
|
||||
ns.clearCache()
|
||||
}
|
||||
|
||||
// Load retrieves an explicit rule from the store for a path.
|
||||
// Returns the rule (true=enable, false=disable) and whether it exists.
|
||||
// Does not consider hierarchy or caching.
|
||||
func (ns *Namespace) Load(path string) (rule interface{}, found bool) {
|
||||
return ns.store.Load(path)
|
||||
}
|
||||
|
||||
// Store directly sets a rule in the store, bypassing cache invalidation.
|
||||
// Intended for internal use or sync.Map parity; prefer Set for standard use.
|
||||
func (ns *Namespace) Store(path string, rule bool) {
|
||||
ns.store.Store(path, rule)
|
||||
}
|
||||
|
||||
// clearCache clears the cache of Enabled results.
|
||||
// Called by Set to ensure consistency after rule changes.
|
||||
func (ns *Namespace) clearCache() {
|
||||
ns.cache.Range(func(key, _ interface{}) bool {
|
||||
ns.cache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Enabled checks if a path is enabled by namespace rules, considering the most
|
||||
// specific rule (path or closest prefix) in the store. Results are cached.
|
||||
// Args:
|
||||
// - path: Absolute namespace path to check.
|
||||
// - separator: Character delimiting path segments (e.g., "/", ".").
|
||||
//
|
||||
// Returns:
|
||||
// - isEnabledByRule: True if an explicit rule enables the path.
|
||||
// - isDisabledByRule: True if an explicit rule disables the path.
|
||||
//
|
||||
// If both are false, no explicit rule applies to the path or its prefixes.
|
||||
func (ns *Namespace) Enabled(path string, separator string) (isEnabledByRule bool, isDisabledByRule bool) {
|
||||
if path == "" { // Root path has no explicit rule
|
||||
return false, false
|
||||
}
|
||||
|
||||
// Check cache
|
||||
if cachedValue, found := ns.cache.Load(path); found {
|
||||
if state, ok := cachedValue.(namespaceRule); ok {
|
||||
return state.isEnabledByRule, state.isDisabledByRule
|
||||
}
|
||||
ns.cache.Delete(path) // Remove invalid cache entry
|
||||
}
|
||||
|
||||
// Compute: Most specific rule wins
|
||||
parts := strings.Split(path, separator)
|
||||
computedIsEnabled := false
|
||||
computedIsDisabled := false
|
||||
|
||||
for i := len(parts); i >= 1; i-- {
|
||||
currentPrefix := strings.Join(parts[:i], separator)
|
||||
if val, ok := ns.store.Load(currentPrefix); ok {
|
||||
if rule := val.(bool); rule {
|
||||
computedIsEnabled = true
|
||||
computedIsDisabled = false
|
||||
} else {
|
||||
computedIsEnabled = false
|
||||
computedIsDisabled = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Cache result, including (false, false) for no rule
|
||||
ns.cache.Store(path, namespaceRule{
|
||||
isEnabledByRule: computedIsEnabled,
|
||||
isDisabledByRule: computedIsDisabled,
|
||||
})
|
||||
|
||||
return computedIsEnabled, computedIsDisabled
|
||||
}
|
||||
124
vendor/github.com/olekukonko/ll/middleware.go
generated
vendored
Normal file
124
vendor/github.com/olekukonko/ll/middleware.go
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
package ll
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll/lx"
|
||||
)
|
||||
|
||||
// Middleware represents a registered middleware and its operations in the logging pipeline.
|
||||
// It holds an ID for identification, a reference to the parent logger, and the handler function
|
||||
// that processes log entries. Middleware is used to transform or filter log entries before they
|
||||
// are passed to the logger's output handler.
|
||||
type Middleware struct {
|
||||
id int // Unique identifier for the middleware
|
||||
logger *Logger // Parent logger instance for context and logging operations
|
||||
fn lx.Handler // Handler function that processes log entries
|
||||
}
|
||||
|
||||
// Remove unregisters the middleware from the logger’s middleware chain.
|
||||
// It safely removes the middleware by its ID, ensuring thread-safety with a mutex lock.
|
||||
// If the middleware or logger is nil, it returns early to prevent panics.
|
||||
// Example usage:
|
||||
//
|
||||
// // Using a named middleware function
|
||||
// mw := logger.Use(authMiddleware)
|
||||
// defer mw.Remove()
|
||||
//
|
||||
// // Using an inline middleware
|
||||
// mw = logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// defer mw.Remove()
|
||||
func (m *Middleware) Remove() {
|
||||
// Check for nil middleware or logger to avoid panics
|
||||
if m == nil || m.logger == nil {
|
||||
return
|
||||
}
|
||||
// Acquire write lock to modify middleware slice
|
||||
m.logger.mu.Lock()
|
||||
defer m.logger.mu.Unlock()
|
||||
// Iterate through middleware slice to find and remove matching ID
|
||||
for i, entry := range m.logger.middleware {
|
||||
if entry.id == m.id {
|
||||
// Remove middleware by slicing out the matching entry
|
||||
m.logger.middleware = append(m.logger.middleware[:i], m.logger.middleware[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logger returns the parent logger for optional chaining.
|
||||
// This allows middleware to access the logger for additional operations, such as logging errors
|
||||
// or creating derived loggers. It is useful for fluent API patterns.
|
||||
// Example:
|
||||
//
|
||||
// mw := logger.Use(authMiddleware)
|
||||
// mw.Logger().Info("Middleware registered")
|
||||
func (m *Middleware) Logger() *Logger {
|
||||
return m.logger
|
||||
}
|
||||
|
||||
// Error logs an error message at the Error level if the middleware blocks a log entry.
|
||||
// It uses the parent logger to emit the error and returns the middleware for chaining.
|
||||
// This is useful for debugging or auditing when middleware rejects a log.
|
||||
// Example:
|
||||
//
|
||||
// mw := logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// mw.Error("Rejected low-level log")
|
||||
func (m *Middleware) Error(args ...any) *Middleware {
|
||||
m.logger.Error(args...)
|
||||
return m
|
||||
}
|
||||
|
||||
// Errorf logs an error message at the Error level if the middleware blocks a log entry.
|
||||
// It uses the parent logger to emit the error and returns the middleware for chaining.
|
||||
// This is useful for debugging or auditing when middleware rejects a log.
|
||||
// Example:
|
||||
//
|
||||
// mw := logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level < lx.LevelWarn {
|
||||
// return fmt.Errorf("level too low")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
// mw.Errorf("Rejected low-level log")
|
||||
func (m *Middleware) Errorf(format string, args ...any) *Middleware {
|
||||
m.logger.Errorf(format, args...)
|
||||
return m
|
||||
}
|
||||
|
||||
// middlewareFunc is a function adapter that implements the lx.Handler interface.
|
||||
// It allows plain functions with the signature `func(*lx.Entry) error` to be used as middleware.
|
||||
// The function should return nil to allow the log to proceed or a non-nil error to reject it,
|
||||
// stopping the log from being emitted by the logger.
|
||||
type middlewareFunc func(*lx.Entry) error
|
||||
|
||||
// Handle implements the lx.Handler interface for middlewareFunc.
|
||||
// It calls the underlying function with the log entry and returns its result.
|
||||
// This enables seamless integration of function-based middleware into the logging pipeline.
|
||||
func (mf middlewareFunc) Handle(e *lx.Entry) error {
|
||||
return mf(e)
|
||||
}
|
||||
|
||||
// Middle creates a middleware handler from a function.
|
||||
// It wraps a function with the signature `func(*lx.Entry) error` into a middlewareFunc,
|
||||
// allowing it to be used in the logger’s middleware pipeline. A non-nil error returned by
|
||||
// the function will stop the log from being emitted, ensuring precise control over logging.
|
||||
// Example:
|
||||
//
|
||||
// logger.Use(ll.Middle(func(e *lx.Entry) error {
|
||||
// if e.Level == lx.LevelDebug {
|
||||
// return fmt.Errorf("debug logs disabled")
|
||||
// }
|
||||
// return nil
|
||||
// }))
|
||||
func Middle(fn func(*lx.Entry) error) lx.Handler {
|
||||
return middlewareFunc(fn)
|
||||
}
|
||||
21
vendor/github.com/olekukonko/tablewriter/.gitignore
generated
vendored
21
vendor/github.com/olekukonko/tablewriter/.gitignore
generated
vendored
@@ -1,15 +1,10 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Go template
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# folders
|
||||
.idea
|
||||
.vscode
|
||||
/tmp
|
||||
/lab
|
||||
dev.sh
|
||||
*csv2table
|
||||
_test/
|
||||
|
||||
22
vendor/github.com/olekukonko/tablewriter/.travis.yml
generated
vendored
22
vendor/github.com/olekukonko/tablewriter/.travis.yml
generated
vendored
@@ -1,22 +0,0 @@
|
||||
language: go
|
||||
arch:
|
||||
- ppc64le
|
||||
- amd64
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- 1.8
|
||||
- 1.9
|
||||
- "1.10"
|
||||
- tip
|
||||
jobs:
|
||||
exclude :
|
||||
- arch : ppc64le
|
||||
go :
|
||||
- 1.3
|
||||
- arch : ppc64le
|
||||
go :
|
||||
- 1.4
|
||||
3280
vendor/github.com/olekukonko/tablewriter/MIGRATION.md
generated
vendored
Normal file
3280
vendor/github.com/olekukonko/tablewriter/MIGRATION.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1460
vendor/github.com/olekukonko/tablewriter/README.md
generated
vendored
1460
vendor/github.com/olekukonko/tablewriter/README.md
generated
vendored
File diff suppressed because it is too large
Load Diff
466
vendor/github.com/olekukonko/tablewriter/README_LEGACY.md
generated
vendored
Normal file
466
vendor/github.com/olekukonko/tablewriter/README_LEGACY.md
generated
vendored
Normal file
@@ -0,0 +1,466 @@
|
||||
ASCII Table Writer
|
||||
=========
|
||||
|
||||
[](https://github.com/olekukonko/tablewriter/actions?query=workflow%3Aci)
|
||||
[](https://sourcegraph.com/github.com/olekukonko/tablewriter)
|
||||
[](https://godoc.org/github.com/olekukonko/tablewriter)
|
||||
|
||||
|
||||
## Important Notice: Modernization in Progress
|
||||
|
||||
The `tablewriter` package is being modernized on the `prototype` branch with generics, streaming support, and a modular design, targeting `v0.2.0`. Until this is released:
|
||||
|
||||
**For Production Use**: Use the stable version `v0.0.5`:
|
||||
```bash
|
||||
go get github.com/olekukonko/tablewriter@v0.0.5
|
||||
```
|
||||
|
||||
####
|
||||
|
||||
For Development Preview: Try the in-progress version (unstable)
|
||||
|
||||
```bash
|
||||
go get github.com/olekukonko/tablewriter@master
|
||||
```
|
||||
|
||||
#### Features
|
||||
|
||||
- Automatic Padding
|
||||
- Support Multiple Lines
|
||||
- Supports Alignment
|
||||
- Support Custom Separators
|
||||
- Automatic Alignment of numbers & percentage
|
||||
- Write directly to http , file etc via `io.Writer`
|
||||
- Read directly from CSV file
|
||||
- Optional row line via `SetRowLine`
|
||||
- Normalise table header
|
||||
- Make CSV Headers optional
|
||||
- Enable or disable table border
|
||||
- Set custom footer support
|
||||
- Optional identical cells merging
|
||||
- Set custom caption
|
||||
- Optional reflowing of paragraphs in multi-line cells.
|
||||
|
||||
#### Example 1 - Basic
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"A", "The Good", "500"},
|
||||
[]string{"B", "The Very very Bad Man", "288"},
|
||||
[]string{"C", "The Ugly", "120"},
|
||||
[]string{"D", "The Gopher", "800"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Name", "Sign", "Rating"})
|
||||
|
||||
for _, v := range data {
|
||||
table.Append(v)
|
||||
}
|
||||
table.Render() // Send output
|
||||
```
|
||||
|
||||
##### Output 1
|
||||
|
||||
```
|
||||
+------+-----------------------+--------+
|
||||
| NAME | SIGN | RATING |
|
||||
+------+-----------------------+--------+
|
||||
| A | The Good | 500 |
|
||||
| B | The Very very Bad Man | 288 |
|
||||
| C | The Ugly | 120 |
|
||||
| D | The Gopher | 800 |
|
||||
+------+-----------------------+--------+
|
||||
```
|
||||
|
||||
#### Example 2 - Without Border / Footer / Bulk Append
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
|
||||
[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
|
||||
[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
|
||||
[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
|
||||
table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
|
||||
table.EnableBorder(false) // Set Border to false
|
||||
table.AppendBulk(data) // Add Bulk Data
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 2
|
||||
|
||||
```
|
||||
|
||||
DATE | DESCRIPTION | CV2 | AMOUNT
|
||||
-----------+--------------------------+-------+----------
|
||||
1/1/2014 | Domain name | 2233 | $10.98
|
||||
1/1/2014 | January Hosting | 2233 | $54.95
|
||||
1/4/2014 | February Hosting | 2233 | $51.00
|
||||
1/4/2014 | February Extra Bandwidth | 2233 | $30.00
|
||||
-----------+--------------------------+-------+----------
|
||||
TOTAL | $146 93
|
||||
--------+----------
|
||||
|
||||
```
|
||||
|
||||
#### Example 3 - CSV
|
||||
|
||||
```go
|
||||
table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 3
|
||||
|
||||
```
|
||||
+----------+--------------+------+-----+---------+----------------+
|
||||
| FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA |
|
||||
+----------+--------------+------+-----+---------+----------------+
|
||||
| user_id | smallint(5) | NO | PRI | NULL | auto_increment |
|
||||
| username | varchar(10) | NO | | NULL | |
|
||||
| password | varchar(100) | NO | | NULL | |
|
||||
+----------+--------------+------+-----+---------+----------------+
|
||||
```
|
||||
|
||||
#### Example 4 - Custom Separator
|
||||
|
||||
```go
|
||||
table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true)
|
||||
table.SetRowLine(true) // Enable row line
|
||||
|
||||
// Change table lines
|
||||
table.SetCenterSeparator("*")
|
||||
table.SetColumnSeparator("╪")
|
||||
table.SetRowSeparator("-")
|
||||
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 4
|
||||
|
||||
```
|
||||
*------------*-----------*---------*
|
||||
╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪
|
||||
*------------*-----------*---------*
|
||||
╪ John ╪ Barry ╪ 123456 ╪
|
||||
*------------*-----------*---------*
|
||||
╪ Kathy ╪ Smith ╪ 687987 ╪
|
||||
*------------*-----------*---------*
|
||||
╪ Bob ╪ McCornick ╪ 3979870 ╪
|
||||
*------------*-----------*---------*
|
||||
```
|
||||
|
||||
#### Example 5 - Markdown Format
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
|
||||
[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
|
||||
[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
|
||||
[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
|
||||
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
|
||||
table.SetCenterSeparator("|")
|
||||
table.AppendBulk(data) // Add Bulk Data
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 5
|
||||
|
||||
```
|
||||
| DATE | DESCRIPTION | CV2 | AMOUNT |
|
||||
|----------|--------------------------|------|--------|
|
||||
| 1/1/2014 | Domain name | 2233 | $10.98 |
|
||||
| 1/1/2014 | January Hosting | 2233 | $54.95 |
|
||||
| 1/4/2014 | February Hosting | 2233 | $51.00 |
|
||||
| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 |
|
||||
```
|
||||
|
||||
#### Example 6 - Identical cells merging
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"1/1/2014", "Domain name", "1234", "$10.98"},
|
||||
[]string{"1/1/2014", "January Hosting", "2345", "$54.95"},
|
||||
[]string{"1/4/2014", "February Hosting", "3456", "$51.00"},
|
||||
[]string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
|
||||
table.SetFooter([]string{"", "", "Total", "$146.93"})
|
||||
table.SetAutoMergeCells(true)
|
||||
table.SetRowLine(true)
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 6
|
||||
|
||||
```
|
||||
+----------+--------------------------+-------+---------+
|
||||
| DATE | DESCRIPTION | CV2 | AMOUNT |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| 1/1/2014 | Domain name | 1234 | $10.98 |
|
||||
+ +--------------------------+-------+---------+
|
||||
| | January Hosting | 2345 | $54.95 |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| 1/4/2014 | February Hosting | 3456 | $51.00 |
|
||||
+ +--------------------------+-------+---------+
|
||||
| | February Extra Bandwidth | 4567 | $30.00 |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| TOTAL | $146 93 |
|
||||
+----------+--------------------------+-------+---------+
|
||||
```
|
||||
|
||||
#### Example 7 - Identical cells merging (specify the column index to merge)
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"1/1/2014", "Domain name", "1234", "$10.98"},
|
||||
[]string{"1/1/2014", "January Hosting", "1234", "$10.98"},
|
||||
[]string{"1/4/2014", "February Hosting", "3456", "$51.00"},
|
||||
[]string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
|
||||
table.SetFooter([]string{"", "", "Total", "$146.93"})
|
||||
table.SetAutoMergeCellsByColumnIndex([]int{2, 3})
|
||||
table.SetRowLine(true)
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 7
|
||||
|
||||
```
|
||||
+----------+--------------------------+-------+---------+
|
||||
| DATE | DESCRIPTION | CV2 | AMOUNT |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| 1/1/2014 | Domain name | 1234 | $10.98 |
|
||||
+----------+--------------------------+ + +
|
||||
| 1/1/2014 | January Hosting | | |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| 1/4/2014 | February Hosting | 3456 | $51.00 |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| 1/4/2014 | February Extra Bandwidth | 4567 | $30.00 |
|
||||
+----------+--------------------------+-------+---------+
|
||||
| TOTAL | $146.93 |
|
||||
+----------+--------------------------+-------+---------+
|
||||
```
|
||||
|
||||
#### Table with color
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"1/1/2014", "Domain name", "2233", "$10.98"},
|
||||
[]string{"1/1/2014", "January Hosting", "2233", "$54.95"},
|
||||
[]string{"1/4/2014", "February Hosting", "2233", "$51.00"},
|
||||
[]string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Description", "CV2", "Amount"})
|
||||
table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer
|
||||
table.EnableBorder(false) // Set Border to false
|
||||
|
||||
table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
|
||||
tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor},
|
||||
tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor},
|
||||
tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor})
|
||||
|
||||
table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
|
||||
|
||||
table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{},
|
||||
tablewriter.Colors{tablewriter.Bold},
|
||||
tablewriter.Colors{tablewriter.FgHiRedColor})
|
||||
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
```
|
||||
|
||||
#### Table with color Output
|
||||
|
||||

|
||||
|
||||
#### Example - 8 Table Cells with Color
|
||||
|
||||
Individual Cell Colors from `func Rich` take precedence over Column Colors
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"Test1Merge", "HelloCol2 - 1", "HelloCol3 - 1", "HelloCol4 - 1"},
|
||||
[]string{"Test1Merge", "HelloCol2 - 2", "HelloCol3 - 2", "HelloCol4 - 2"},
|
||||
[]string{"Test1Merge", "HelloCol2 - 3", "HelloCol3 - 3", "HelloCol4 - 3"},
|
||||
[]string{"Test2Merge", "HelloCol2 - 4", "HelloCol3 - 4", "HelloCol4 - 4"},
|
||||
[]string{"Test2Merge", "HelloCol2 - 5", "HelloCol3 - 5", "HelloCol4 - 5"},
|
||||
[]string{"Test2Merge", "HelloCol2 - 6", "HelloCol3 - 6", "HelloCol4 - 6"},
|
||||
[]string{"Test2Merge", "HelloCol2 - 7", "HelloCol3 - 7", "HelloCol4 - 7"},
|
||||
[]string{"Test3Merge", "HelloCol2 - 8", "HelloCol3 - 8", "HelloCol4 - 8"},
|
||||
[]string{"Test3Merge", "HelloCol2 - 9", "HelloCol3 - 9", "HelloCol4 - 9"},
|
||||
[]string{"Test3Merge", "HelloCol2 - 10", "HelloCol3 -10", "HelloCol4 - 10"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Col1", "Col2", "Col3", "Col4"})
|
||||
table.SetFooter([]string{"", "", "Footer3", "Footer4"})
|
||||
table.EnableBorder(false)
|
||||
|
||||
table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
|
||||
tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor},
|
||||
tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor},
|
||||
tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor})
|
||||
|
||||
table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
|
||||
|
||||
table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{},
|
||||
tablewriter.Colors{tablewriter.Bold},
|
||||
tablewriter.Colors{tablewriter.FgHiRedColor})
|
||||
|
||||
colorData1 := []string{"TestCOLOR1Merge", "HelloCol2 - COLOR1", "HelloCol3 - COLOR1", "HelloCol4 - COLOR1"}
|
||||
colorData2 := []string{"TestCOLOR2Merge", "HelloCol2 - COLOR2", "HelloCol3 - COLOR2", "HelloCol4 - COLOR2"}
|
||||
|
||||
for i, row := range data {
|
||||
if i == 4 {
|
||||
table.Rich(colorData1, []tablewriter.Colors{tablewriter.Colors{}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgWhiteColor}, tablewriter.Colors{}})
|
||||
table.Rich(colorData2, []tablewriter.Colors{tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold, tablewriter.BgRedColor}, tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Italic, tablewriter.BgHiCyanColor}})
|
||||
}
|
||||
table.Append(row)
|
||||
}
|
||||
|
||||
table.SetAutoMergeCells(true)
|
||||
table.Render()
|
||||
|
||||
```
|
||||
|
||||
##### Table cells with color Output
|
||||
|
||||

|
||||
|
||||
#### Example 9 - Set table caption
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
[]string{"A", "The Good", "500"},
|
||||
[]string{"B", "The Very very Bad Man", "288"},
|
||||
[]string{"C", "The Ugly", "120"},
|
||||
[]string{"D", "The Gopher", "800"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Name", "Sign", "Rating"})
|
||||
table.SetCaption(true, "Movie ratings.")
|
||||
|
||||
for _, v := range data {
|
||||
table.Append(v)
|
||||
}
|
||||
table.Render() // Send output
|
||||
```
|
||||
|
||||
Note: Caption text will wrap with total width of rendered table.
|
||||
|
||||
##### Output 9
|
||||
|
||||
```
|
||||
+------+-----------------------+--------+
|
||||
| NAME | SIGN | RATING |
|
||||
+------+-----------------------+--------+
|
||||
| A | The Good | 500 |
|
||||
| B | The Very very Bad Man | 288 |
|
||||
| C | The Ugly | 120 |
|
||||
| D | The Gopher | 800 |
|
||||
+------+-----------------------+--------+
|
||||
Movie ratings.
|
||||
```
|
||||
|
||||
#### Example 10 - Set NoWhiteSpace and TablePadding option
|
||||
|
||||
```go
|
||||
data := [][]string{
|
||||
{"node1.example.com", "Ready", "compute", "1.11"},
|
||||
{"node2.example.com", "Ready", "compute", "1.11"},
|
||||
{"node3.example.com", "Ready", "compute", "1.11"},
|
||||
{"node4.example.com", "NotReady", "compute", "1.11"},
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Name", "Status", "Role", "Version"})
|
||||
table.SetAutoWrapText(false)
|
||||
table.SetAutoFormatHeaders(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetHeaderLine(false)
|
||||
table.EnableBorder(false)
|
||||
table.SetTablePadding("\t") // pad with tabs
|
||||
table.SetNoWhiteSpace(true)
|
||||
table.AppendBulk(data) // Add Bulk Data
|
||||
table.Render()
|
||||
```
|
||||
|
||||
##### Output 10
|
||||
|
||||
```
|
||||
NAME STATUS ROLE VERSION
|
||||
node1.example.com Ready compute 1.11
|
||||
node2.example.com Ready compute 1.11
|
||||
node3.example.com Ready compute 1.11
|
||||
node4.example.com NotReady compute 1.11
|
||||
```
|
||||
|
||||
#### Render table into a string
|
||||
|
||||
Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the
|
||||
`strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"fmt"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tableString := &strings.Builder{}
|
||||
table := tablewriter.NewWriter(tableString)
|
||||
|
||||
/*
|
||||
* Code to fill the table
|
||||
*/
|
||||
|
||||
table.Render()
|
||||
|
||||
fmt.Println(tableString.String())
|
||||
}
|
||||
```
|
||||
|
||||
#### TODO
|
||||
|
||||
- ~~Import Directly from CSV~~ - `done`
|
||||
- ~~Support for `SetFooter`~~ - `done`
|
||||
- ~~Support for `SetBorder`~~ - `done`
|
||||
- ~~Support table with uneven rows~~ - `done`
|
||||
- ~~Support custom alignment~~
|
||||
- General Improvement & Optimisation
|
||||
- `NewHTML` Parse table from HTML
|
||||
966
vendor/github.com/olekukonko/tablewriter/config.go
generated
vendored
Normal file
966
vendor/github.com/olekukonko/tablewriter/config.go
generated
vendored
Normal file
@@ -0,0 +1,966 @@
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// Config represents the table configuration
|
||||
type Config struct {
|
||||
MaxWidth int
|
||||
Header tw.CellConfig
|
||||
Row tw.CellConfig
|
||||
Footer tw.CellConfig
|
||||
Debug bool
|
||||
Stream tw.StreamConfig
|
||||
Behavior tw.Behavior
|
||||
Widths tw.CellWidth
|
||||
}
|
||||
|
||||
// ConfigBuilder provides a fluent interface for building Config
|
||||
type ConfigBuilder struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewConfigBuilder creates a new ConfigBuilder with defaults
|
||||
func NewConfigBuilder() *ConfigBuilder {
|
||||
return &ConfigBuilder{
|
||||
config: defaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// Build returns the built Config
|
||||
func (b *ConfigBuilder) Build() Config {
|
||||
return b.config
|
||||
}
|
||||
|
||||
// Header returns a HeaderConfigBuilder for header configuration
|
||||
func (b *ConfigBuilder) Header() *HeaderConfigBuilder {
|
||||
return &HeaderConfigBuilder{
|
||||
parent: b,
|
||||
config: &b.config.Header,
|
||||
}
|
||||
}
|
||||
|
||||
// Row returns a RowConfigBuilder for row configuration
|
||||
func (b *ConfigBuilder) Row() *RowConfigBuilder {
|
||||
return &RowConfigBuilder{
|
||||
parent: b,
|
||||
config: &b.config.Row,
|
||||
}
|
||||
}
|
||||
|
||||
// Footer returns a FooterConfigBuilder for footer configuration
|
||||
func (b *ConfigBuilder) Footer() *FooterConfigBuilder {
|
||||
return &FooterConfigBuilder{
|
||||
parent: b,
|
||||
config: &b.config.Footer,
|
||||
}
|
||||
}
|
||||
|
||||
// Behavior returns a BehaviorConfigBuilder for behavior configuration
|
||||
func (b *ConfigBuilder) Behavior() *BehaviorConfigBuilder {
|
||||
return &BehaviorConfigBuilder{
|
||||
parent: b,
|
||||
config: &b.config.Behavior,
|
||||
}
|
||||
}
|
||||
|
||||
// ForColumn returns a ColumnConfigBuilder for column-specific configuration
|
||||
func (b *ConfigBuilder) ForColumn(col int) *ColumnConfigBuilder {
|
||||
return &ColumnConfigBuilder{
|
||||
parent: b,
|
||||
col: col,
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrimSpace enables or disables automatic trimming of leading/trailing spaces.
|
||||
// Ignored in streaming mode.
|
||||
func (b *ConfigBuilder) WithTrimSpace(state tw.State) *ConfigBuilder {
|
||||
b.config.Behavior.TrimSpace = state
|
||||
return b
|
||||
}
|
||||
|
||||
// WithDebug enables/disables debug logging
|
||||
func (b *ConfigBuilder) WithDebug(debug bool) *ConfigBuilder {
|
||||
b.config.Debug = debug
|
||||
return b
|
||||
}
|
||||
|
||||
// WithAutoHide enables or disables automatic hiding of empty columns (ignored in streaming mode).
|
||||
func (b *ConfigBuilder) WithAutoHide(state tw.State) *ConfigBuilder {
|
||||
b.config.Behavior.AutoHide = state
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFooterAlignment sets the text alignment for all footer cells.
|
||||
// Invalid alignments are ignored.
|
||||
func (b *ConfigBuilder) WithFooterAlignment(align tw.Align) *ConfigBuilder {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return b
|
||||
}
|
||||
b.config.Footer.Alignment.Global = align
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFooterAutoFormat enables or disables automatic formatting (e.g., title case) for footer cells.
|
||||
func (b *ConfigBuilder) WithFooterAutoFormat(autoFormat tw.State) *ConfigBuilder {
|
||||
b.config.Footer.Formatting.AutoFormat = autoFormat
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFooterAutoWrap sets the wrapping behavior for footer cells (e.g., truncate, normal, break).
|
||||
// Invalid wrap modes are ignored.
|
||||
func (b *ConfigBuilder) WithFooterAutoWrap(autoWrap int) *ConfigBuilder {
|
||||
if autoWrap < tw.WrapNone || autoWrap > tw.WrapBreak {
|
||||
return b
|
||||
}
|
||||
b.config.Footer.Formatting.AutoWrap = autoWrap
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFooterGlobalPadding sets the global padding for all footer cells.
|
||||
func (b *ConfigBuilder) WithFooterGlobalPadding(padding tw.Padding) *ConfigBuilder {
|
||||
b.config.Footer.Padding.Global = padding
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFooterMaxWidth sets the maximum content width for footer cells.
|
||||
// Negative values are ignored.
|
||||
func (b *ConfigBuilder) WithFooterMaxWidth(maxWidth int) *ConfigBuilder {
|
||||
if maxWidth < 0 {
|
||||
return b
|
||||
}
|
||||
b.config.Footer.ColMaxWidths.Global = maxWidth
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFooterMergeMode sets the merge behavior for footer cells (e.g., horizontal, hierarchical).
|
||||
// Invalid merge modes are ignored.
|
||||
func (b *ConfigBuilder) WithFooterMergeMode(mergeMode int) *ConfigBuilder {
|
||||
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
|
||||
return b
|
||||
}
|
||||
b.config.Footer.Formatting.MergeMode = mergeMode
|
||||
return b
|
||||
}
|
||||
|
||||
// WithHeaderAlignment sets the text alignment for all header cells.
|
||||
// Invalid alignments are ignored.
|
||||
func (b *ConfigBuilder) WithHeaderAlignment(align tw.Align) *ConfigBuilder {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return b
|
||||
}
|
||||
b.config.Header.Alignment.Global = align
|
||||
return b
|
||||
}
|
||||
|
||||
// WithHeaderAutoFormat enables or disables automatic formatting (e.g., title case) for header cells.
|
||||
func (b *ConfigBuilder) WithHeaderAutoFormat(autoFormat tw.State) *ConfigBuilder {
|
||||
b.config.Header.Formatting.AutoFormat = autoFormat
|
||||
return b
|
||||
}
|
||||
|
||||
// WithHeaderAutoWrap sets the wrapping behavior for header cells (e.g., truncate, normal).
|
||||
// Invalid wrap modes are ignored.
|
||||
func (b *ConfigBuilder) WithHeaderAutoWrap(autoWrap int) *ConfigBuilder {
|
||||
if autoWrap < tw.WrapNone || autoWrap > tw.WrapBreak {
|
||||
return b
|
||||
}
|
||||
b.config.Header.Formatting.AutoWrap = autoWrap
|
||||
return b
|
||||
}
|
||||
|
||||
// WithHeaderGlobalPadding sets the global padding for all header cells.
|
||||
func (b *ConfigBuilder) WithHeaderGlobalPadding(padding tw.Padding) *ConfigBuilder {
|
||||
b.config.Header.Padding.Global = padding
|
||||
return b
|
||||
}
|
||||
|
||||
// WithHeaderMaxWidth sets the maximum content width for header cells.
|
||||
// Negative values are ignored.
|
||||
func (b *ConfigBuilder) WithHeaderMaxWidth(maxWidth int) *ConfigBuilder {
|
||||
if maxWidth < 0 {
|
||||
return b
|
||||
}
|
||||
b.config.Header.ColMaxWidths.Global = maxWidth
|
||||
return b
|
||||
}
|
||||
|
||||
// WithHeaderMergeMode sets the merge behavior for header cells (e.g., horizontal, vertical).
|
||||
// Invalid merge modes are ignored.
|
||||
func (b *ConfigBuilder) WithHeaderMergeMode(mergeMode int) *ConfigBuilder {
|
||||
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
|
||||
return b
|
||||
}
|
||||
b.config.Header.Formatting.MergeMode = mergeMode
|
||||
return b
|
||||
}
|
||||
|
||||
// WithMaxWidth sets the maximum width for the entire table (0 means unlimited).
|
||||
// Negative values are treated as 0.
|
||||
func (b *ConfigBuilder) WithMaxWidth(width int) *ConfigBuilder {
|
||||
if width < 0 {
|
||||
b.config.MaxWidth = 0
|
||||
} else {
|
||||
b.config.MaxWidth = width
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithRowAlignment sets the text alignment for all row cells.
|
||||
// Invalid alignments are ignored.
|
||||
func (b *ConfigBuilder) WithRowAlignment(align tw.Align) *ConfigBuilder {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return b
|
||||
}
|
||||
b.config.Row.Alignment.Global = align
|
||||
return b
|
||||
}
|
||||
|
||||
// WithRowAutoFormat enables or disables automatic formatting for row cells.
|
||||
func (b *ConfigBuilder) WithRowAutoFormat(autoFormat tw.State) *ConfigBuilder {
|
||||
b.config.Row.Formatting.AutoFormat = autoFormat
|
||||
return b
|
||||
}
|
||||
|
||||
// WithRowAutoWrap sets the wrapping behavior for row cells (e.g., truncate, normal).
|
||||
// Invalid wrap modes are ignored.
|
||||
func (b *ConfigBuilder) WithRowAutoWrap(autoWrap int) *ConfigBuilder {
|
||||
if autoWrap < tw.WrapNone || autoWrap > tw.WrapBreak {
|
||||
return b
|
||||
}
|
||||
b.config.Row.Formatting.AutoWrap = autoWrap
|
||||
return b
|
||||
}
|
||||
|
||||
// WithRowGlobalPadding sets the global padding for all row cells.
|
||||
func (b *ConfigBuilder) WithRowGlobalPadding(padding tw.Padding) *ConfigBuilder {
|
||||
b.config.Row.Padding.Global = padding
|
||||
return b
|
||||
}
|
||||
|
||||
// WithRowMaxWidth sets the maximum content width for row cells.
|
||||
// Negative values are ignored.
|
||||
func (b *ConfigBuilder) WithRowMaxWidth(maxWidth int) *ConfigBuilder {
|
||||
if maxWidth < 0 {
|
||||
return b
|
||||
}
|
||||
b.config.Row.ColMaxWidths.Global = maxWidth
|
||||
return b
|
||||
}
|
||||
|
||||
// WithRowMergeMode sets the merge behavior for row cells (e.g., horizontal, hierarchical).
|
||||
// Invalid merge modes are ignored.
|
||||
func (b *ConfigBuilder) WithRowMergeMode(mergeMode int) *ConfigBuilder {
|
||||
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
|
||||
return b
|
||||
}
|
||||
b.config.Row.Formatting.MergeMode = mergeMode
|
||||
return b
|
||||
}
|
||||
|
||||
// HeaderConfigBuilder configures header settings
|
||||
type HeaderConfigBuilder struct {
|
||||
parent *ConfigBuilder
|
||||
config *tw.CellConfig
|
||||
}
|
||||
|
||||
// Build returns the parent ConfigBuilder
|
||||
func (h *HeaderConfigBuilder) Build() *ConfigBuilder {
|
||||
return h.parent
|
||||
}
|
||||
|
||||
// Alignment returns an AlignmentConfigBuilder for header alignment
|
||||
func (h *HeaderConfigBuilder) Alignment() *AlignmentConfigBuilder {
|
||||
return &AlignmentConfigBuilder{
|
||||
parent: h.parent,
|
||||
config: &h.config.Alignment,
|
||||
section: "header",
|
||||
}
|
||||
}
|
||||
|
||||
// Formatting returns a HeaderFormattingBuilder for header formatting
|
||||
func (h *HeaderConfigBuilder) Formatting() *HeaderFormattingBuilder {
|
||||
return &HeaderFormattingBuilder{
|
||||
parent: h,
|
||||
config: &h.config.Formatting,
|
||||
section: "header",
|
||||
}
|
||||
}
|
||||
|
||||
// Padding returns a HeaderPaddingBuilder for header padding
|
||||
func (h *HeaderConfigBuilder) Padding() *HeaderPaddingBuilder {
|
||||
return &HeaderPaddingBuilder{
|
||||
parent: h,
|
||||
config: &h.config.Padding,
|
||||
section: "header",
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a HeaderFilterBuilder for header filtering
|
||||
func (h *HeaderConfigBuilder) Filter() *HeaderFilterBuilder {
|
||||
return &HeaderFilterBuilder{
|
||||
parent: h,
|
||||
config: &h.config.Filter,
|
||||
section: "header",
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks returns a HeaderCallbacksBuilder for header callbacks
|
||||
func (h *HeaderConfigBuilder) Callbacks() *HeaderCallbacksBuilder {
|
||||
return &HeaderCallbacksBuilder{
|
||||
parent: h,
|
||||
config: &h.config.Callbacks,
|
||||
section: "header",
|
||||
}
|
||||
}
|
||||
|
||||
// RowConfigBuilder configures row settings
|
||||
type RowConfigBuilder struct {
|
||||
parent *ConfigBuilder
|
||||
config *tw.CellConfig
|
||||
}
|
||||
|
||||
// Build returns the parent ConfigBuilder
|
||||
func (r *RowConfigBuilder) Build() *ConfigBuilder {
|
||||
return r.parent
|
||||
}
|
||||
|
||||
// Alignment returns an AlignmentConfigBuilder for row alignment
|
||||
func (r *RowConfigBuilder) Alignment() *AlignmentConfigBuilder {
|
||||
return &AlignmentConfigBuilder{
|
||||
parent: r.parent,
|
||||
config: &r.config.Alignment,
|
||||
section: "row",
|
||||
}
|
||||
}
|
||||
|
||||
// Formatting returns a RowFormattingBuilder for row formatting
|
||||
func (r *RowConfigBuilder) Formatting() *RowFormattingBuilder {
|
||||
return &RowFormattingBuilder{
|
||||
parent: r,
|
||||
config: &r.config.Formatting,
|
||||
section: "row",
|
||||
}
|
||||
}
|
||||
|
||||
// Padding returns a RowPaddingBuilder for row padding
|
||||
func (r *RowConfigBuilder) Padding() *RowPaddingBuilder {
|
||||
return &RowPaddingBuilder{
|
||||
parent: r,
|
||||
config: &r.config.Padding,
|
||||
section: "row",
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a RowFilterBuilder for row filtering
|
||||
func (r *RowConfigBuilder) Filter() *RowFilterBuilder {
|
||||
return &RowFilterBuilder{
|
||||
parent: r,
|
||||
config: &r.config.Filter,
|
||||
section: "row",
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks returns a RowCallbacksBuilder for row callbacks
|
||||
func (r *RowConfigBuilder) Callbacks() *RowCallbacksBuilder {
|
||||
return &RowCallbacksBuilder{
|
||||
parent: r,
|
||||
config: &r.config.Callbacks,
|
||||
section: "row",
|
||||
}
|
||||
}
|
||||
|
||||
// FooterConfigBuilder configures footer settings
|
||||
type FooterConfigBuilder struct {
|
||||
parent *ConfigBuilder
|
||||
config *tw.CellConfig
|
||||
}
|
||||
|
||||
// Build returns the parent ConfigBuilder
|
||||
func (f *FooterConfigBuilder) Build() *ConfigBuilder {
|
||||
return f.parent
|
||||
}
|
||||
|
||||
// Alignment returns an AlignmentConfigBuilder for footer alignment
|
||||
func (f *FooterConfigBuilder) Alignment() *AlignmentConfigBuilder {
|
||||
return &AlignmentConfigBuilder{
|
||||
parent: f.parent,
|
||||
config: &f.config.Alignment,
|
||||
section: "footer",
|
||||
}
|
||||
}
|
||||
|
||||
// Formatting returns a FooterFormattingBuilder for footer formatting
|
||||
func (f *FooterConfigBuilder) Formatting() *FooterFormattingBuilder {
|
||||
return &FooterFormattingBuilder{
|
||||
parent: f,
|
||||
config: &f.config.Formatting,
|
||||
section: "footer",
|
||||
}
|
||||
}
|
||||
|
||||
// Padding returns a FooterPaddingBuilder for footer padding
|
||||
func (f *FooterConfigBuilder) Padding() *FooterPaddingBuilder {
|
||||
return &FooterPaddingBuilder{
|
||||
parent: f,
|
||||
config: &f.config.Padding,
|
||||
section: "footer",
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a FooterFilterBuilder for footer filtering
|
||||
func (f *FooterConfigBuilder) Filter() *FooterFilterBuilder {
|
||||
return &FooterFilterBuilder{
|
||||
parent: f,
|
||||
config: &f.config.Filter,
|
||||
section: "footer",
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks returns a FooterCallbacksBuilder for footer callbacks
|
||||
func (f *FooterConfigBuilder) Callbacks() *FooterCallbacksBuilder {
|
||||
return &FooterCallbacksBuilder{
|
||||
parent: f,
|
||||
config: &f.config.Callbacks,
|
||||
section: "footer",
|
||||
}
|
||||
}
|
||||
|
||||
// AlignmentConfigBuilder configures alignment settings
|
||||
type AlignmentConfigBuilder struct {
|
||||
parent *ConfigBuilder
|
||||
config *tw.CellAlignment
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent ConfigBuilder
|
||||
func (a *AlignmentConfigBuilder) Build() *ConfigBuilder {
|
||||
return a.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets global alignment
|
||||
func (a *AlignmentConfigBuilder) WithGlobal(align tw.Align) *AlignmentConfigBuilder {
|
||||
if err := align.Validate(); err == nil {
|
||||
a.config.Global = align
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column alignments
|
||||
func (a *AlignmentConfigBuilder) WithPerColumn(alignments []tw.Align) *AlignmentConfigBuilder {
|
||||
if len(alignments) > 0 {
|
||||
a.config.PerColumn = alignments
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// HeaderFormattingBuilder configures header formatting
|
||||
type HeaderFormattingBuilder struct {
|
||||
parent *HeaderConfigBuilder
|
||||
config *tw.CellFormatting
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent HeaderConfigBuilder
|
||||
func (hf *HeaderFormattingBuilder) Build() *HeaderConfigBuilder {
|
||||
return hf.parent
|
||||
}
|
||||
|
||||
// WithAutoFormat enables/disables auto formatting
|
||||
func (hf *HeaderFormattingBuilder) WithAutoFormat(autoFormat tw.State) *HeaderFormattingBuilder {
|
||||
hf.config.AutoFormat = autoFormat
|
||||
return hf
|
||||
}
|
||||
|
||||
// WithAutoWrap sets auto wrap mode
|
||||
func (hf *HeaderFormattingBuilder) WithAutoWrap(autoWrap int) *HeaderFormattingBuilder {
|
||||
if autoWrap >= tw.WrapNone && autoWrap <= tw.WrapBreak {
|
||||
hf.config.AutoWrap = autoWrap
|
||||
}
|
||||
return hf
|
||||
}
|
||||
|
||||
// WithMergeMode sets merge mode
|
||||
func (hf *HeaderFormattingBuilder) WithMergeMode(mergeMode int) *HeaderFormattingBuilder {
|
||||
if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical {
|
||||
hf.config.MergeMode = mergeMode
|
||||
}
|
||||
return hf
|
||||
}
|
||||
|
||||
// RowFormattingBuilder configures row formatting
|
||||
type RowFormattingBuilder struct {
|
||||
parent *RowConfigBuilder
|
||||
config *tw.CellFormatting
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent RowConfigBuilder
|
||||
func (rf *RowFormattingBuilder) Build() *RowConfigBuilder {
|
||||
return rf.parent
|
||||
}
|
||||
|
||||
// WithAutoFormat enables/disables auto formatting
|
||||
func (rf *RowFormattingBuilder) WithAutoFormat(autoFormat tw.State) *RowFormattingBuilder {
|
||||
rf.config.AutoFormat = autoFormat
|
||||
return rf
|
||||
}
|
||||
|
||||
// WithAutoWrap sets auto wrap mode
|
||||
func (rf *RowFormattingBuilder) WithAutoWrap(autoWrap int) *RowFormattingBuilder {
|
||||
if autoWrap >= tw.WrapNone && autoWrap <= tw.WrapBreak {
|
||||
rf.config.AutoWrap = autoWrap
|
||||
}
|
||||
return rf
|
||||
}
|
||||
|
||||
// WithMergeMode sets merge mode
|
||||
func (rf *RowFormattingBuilder) WithMergeMode(mergeMode int) *RowFormattingBuilder {
|
||||
if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical {
|
||||
rf.config.MergeMode = mergeMode
|
||||
}
|
||||
return rf
|
||||
}
|
||||
|
||||
// FooterFormattingBuilder configures footer formatting
|
||||
type FooterFormattingBuilder struct {
|
||||
parent *FooterConfigBuilder
|
||||
config *tw.CellFormatting
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent FooterConfigBuilder
|
||||
func (ff *FooterFormattingBuilder) Build() *FooterConfigBuilder {
|
||||
return ff.parent
|
||||
}
|
||||
|
||||
// WithAutoFormat enables/disables auto formatting
|
||||
func (ff *FooterFormattingBuilder) WithAutoFormat(autoFormat tw.State) *FooterFormattingBuilder {
|
||||
ff.config.AutoFormat = autoFormat
|
||||
return ff
|
||||
}
|
||||
|
||||
// WithAutoWrap sets auto wrap mode
|
||||
func (ff *FooterFormattingBuilder) WithAutoWrap(autoWrap int) *FooterFormattingBuilder {
|
||||
if autoWrap >= tw.WrapNone && autoWrap <= tw.WrapBreak {
|
||||
ff.config.AutoWrap = autoWrap
|
||||
}
|
||||
return ff
|
||||
}
|
||||
|
||||
// WithMergeMode sets merge mode
|
||||
func (ff *FooterFormattingBuilder) WithMergeMode(mergeMode int) *FooterFormattingBuilder {
|
||||
if mergeMode >= tw.MergeNone && mergeMode <= tw.MergeHierarchical {
|
||||
ff.config.MergeMode = mergeMode
|
||||
}
|
||||
return ff
|
||||
}
|
||||
|
||||
// HeaderPaddingBuilder configures header padding
|
||||
type HeaderPaddingBuilder struct {
|
||||
parent *HeaderConfigBuilder
|
||||
config *tw.CellPadding
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent HeaderConfigBuilder
|
||||
func (hp *HeaderPaddingBuilder) Build() *HeaderConfigBuilder {
|
||||
return hp.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets global padding
|
||||
func (hp *HeaderPaddingBuilder) WithGlobal(padding tw.Padding) *HeaderPaddingBuilder {
|
||||
hp.config.Global = padding
|
||||
return hp
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column padding
|
||||
func (hp *HeaderPaddingBuilder) WithPerColumn(padding []tw.Padding) *HeaderPaddingBuilder {
|
||||
hp.config.PerColumn = padding
|
||||
return hp
|
||||
}
|
||||
|
||||
// AddColumnPadding adds padding for a specific column in the header
|
||||
func (hp *HeaderPaddingBuilder) AddColumnPadding(padding tw.Padding) *HeaderPaddingBuilder {
|
||||
hp.config.PerColumn = append(hp.config.PerColumn, padding)
|
||||
return hp
|
||||
}
|
||||
|
||||
// RowPaddingBuilder configures row padding
|
||||
type RowPaddingBuilder struct {
|
||||
parent *RowConfigBuilder
|
||||
config *tw.CellPadding
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent RowConfigBuilder
|
||||
func (rp *RowPaddingBuilder) Build() *RowConfigBuilder {
|
||||
return rp.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets global padding
|
||||
func (rp *RowPaddingBuilder) WithGlobal(padding tw.Padding) *RowPaddingBuilder {
|
||||
rp.config.Global = padding
|
||||
return rp
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column padding
|
||||
func (rp *RowPaddingBuilder) WithPerColumn(padding []tw.Padding) *RowPaddingBuilder {
|
||||
rp.config.PerColumn = padding
|
||||
return rp
|
||||
}
|
||||
|
||||
// AddColumnPadding adds padding for a specific column in the rows
|
||||
func (rp *RowPaddingBuilder) AddColumnPadding(padding tw.Padding) *RowPaddingBuilder {
|
||||
rp.config.PerColumn = append(rp.config.PerColumn, padding)
|
||||
return rp
|
||||
}
|
||||
|
||||
// FooterPaddingBuilder configures footer padding
|
||||
type FooterPaddingBuilder struct {
|
||||
parent *FooterConfigBuilder
|
||||
config *tw.CellPadding
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent FooterConfigBuilder
|
||||
func (fp *FooterPaddingBuilder) Build() *FooterConfigBuilder {
|
||||
return fp.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets global padding
|
||||
func (fp *FooterPaddingBuilder) WithGlobal(padding tw.Padding) *FooterPaddingBuilder {
|
||||
fp.config.Global = padding
|
||||
return fp
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column padding
|
||||
func (fp *FooterPaddingBuilder) WithPerColumn(padding []tw.Padding) *FooterPaddingBuilder {
|
||||
fp.config.PerColumn = padding
|
||||
return fp
|
||||
}
|
||||
|
||||
// AddColumnPadding adds padding for a specific column in the footer
|
||||
func (fp *FooterPaddingBuilder) AddColumnPadding(padding tw.Padding) *FooterPaddingBuilder {
|
||||
fp.config.PerColumn = append(fp.config.PerColumn, padding)
|
||||
return fp
|
||||
}
|
||||
|
||||
// BehaviorConfigBuilder configures behavior settings
|
||||
type BehaviorConfigBuilder struct {
|
||||
parent *ConfigBuilder
|
||||
config *tw.Behavior
|
||||
}
|
||||
|
||||
// Build returns the parent ConfigBuilder
|
||||
func (bb *BehaviorConfigBuilder) Build() *ConfigBuilder {
|
||||
return bb.parent
|
||||
}
|
||||
|
||||
// WithAutoHide enables/disables auto-hide
|
||||
func (bb *BehaviorConfigBuilder) WithAutoHide(state tw.State) *BehaviorConfigBuilder {
|
||||
bb.config.AutoHide = state
|
||||
return bb
|
||||
}
|
||||
|
||||
// WithTrimSpace enables/disables trim space
|
||||
func (bb *BehaviorConfigBuilder) WithTrimSpace(state tw.State) *BehaviorConfigBuilder {
|
||||
bb.config.TrimSpace = state
|
||||
return bb
|
||||
}
|
||||
|
||||
// WithHeaderHide enables/disables header visibility
|
||||
func (bb *BehaviorConfigBuilder) WithHeaderHide(state tw.State) *BehaviorConfigBuilder {
|
||||
bb.config.Header.Hide = state
|
||||
return bb
|
||||
}
|
||||
|
||||
// WithFooterHide enables/disables footer visibility
|
||||
func (bb *BehaviorConfigBuilder) WithFooterHide(state tw.State) *BehaviorConfigBuilder {
|
||||
bb.config.Footer.Hide = state
|
||||
return bb
|
||||
}
|
||||
|
||||
// WithCompactMerge enables/disables compact width optimization for merged cells
|
||||
func (bb *BehaviorConfigBuilder) WithCompactMerge(state tw.State) *BehaviorConfigBuilder {
|
||||
bb.config.Compact.Merge = state
|
||||
return bb
|
||||
}
|
||||
|
||||
// WithAutoHeader enables/disables automatic header extraction for structs in Bulk.
|
||||
func (bb *BehaviorConfigBuilder) WithAutoHeader(state tw.State) *BehaviorConfigBuilder {
|
||||
bb.config.Structs.AutoHeader = state
|
||||
return bb
|
||||
}
|
||||
|
||||
// ColumnConfigBuilder configures column-specific settings
|
||||
type ColumnConfigBuilder struct {
|
||||
parent *ConfigBuilder
|
||||
col int
|
||||
}
|
||||
|
||||
// Build returns the parent ConfigBuilder
|
||||
func (c *ColumnConfigBuilder) Build() *ConfigBuilder {
|
||||
return c.parent
|
||||
}
|
||||
|
||||
// WithAlignment sets alignment for the column
|
||||
func (c *ColumnConfigBuilder) WithAlignment(align tw.Align) *ColumnConfigBuilder {
|
||||
if err := align.Validate(); err == nil {
|
||||
// Ensure slices are large enough
|
||||
if len(c.parent.config.Header.Alignment.PerColumn) <= c.col {
|
||||
newAligns := make([]tw.Align, c.col+1)
|
||||
copy(newAligns, c.parent.config.Header.Alignment.PerColumn)
|
||||
c.parent.config.Header.Alignment.PerColumn = newAligns
|
||||
}
|
||||
c.parent.config.Header.Alignment.PerColumn[c.col] = align
|
||||
|
||||
if len(c.parent.config.Row.Alignment.PerColumn) <= c.col {
|
||||
newAligns := make([]tw.Align, c.col+1)
|
||||
copy(newAligns, c.parent.config.Row.Alignment.PerColumn)
|
||||
c.parent.config.Row.Alignment.PerColumn = newAligns
|
||||
}
|
||||
c.parent.config.Row.Alignment.PerColumn[c.col] = align
|
||||
|
||||
if len(c.parent.config.Footer.Alignment.PerColumn) <= c.col {
|
||||
newAligns := make([]tw.Align, c.col+1)
|
||||
copy(newAligns, c.parent.config.Footer.Alignment.PerColumn)
|
||||
c.parent.config.Footer.Alignment.PerColumn = newAligns
|
||||
}
|
||||
c.parent.config.Footer.Alignment.PerColumn[c.col] = align
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// WithMaxWidth sets max width for the column
|
||||
func (c *ColumnConfigBuilder) WithMaxWidth(width int) *ColumnConfigBuilder {
|
||||
if width >= 0 {
|
||||
// Initialize maps if needed
|
||||
if c.parent.config.Header.ColMaxWidths.PerColumn == nil {
|
||||
c.parent.config.Header.ColMaxWidths.PerColumn = make(tw.Mapper[int, int])
|
||||
c.parent.config.Row.ColMaxWidths.PerColumn = make(tw.Mapper[int, int])
|
||||
c.parent.config.Footer.ColMaxWidths.PerColumn = make(tw.Mapper[int, int])
|
||||
}
|
||||
c.parent.config.Header.ColMaxWidths.PerColumn[c.col] = width
|
||||
c.parent.config.Row.ColMaxWidths.PerColumn[c.col] = width
|
||||
c.parent.config.Footer.ColMaxWidths.PerColumn[c.col] = width
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// HeaderFilterBuilder configures header filtering
|
||||
type HeaderFilterBuilder struct {
|
||||
parent *HeaderConfigBuilder
|
||||
config *tw.CellFilter
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent HeaderConfigBuilder
|
||||
func (hf *HeaderFilterBuilder) Build() *HeaderConfigBuilder {
|
||||
return hf.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets the global filter function for the header
|
||||
func (hf *HeaderFilterBuilder) WithGlobal(filter func([]string) []string) *HeaderFilterBuilder {
|
||||
if filter != nil {
|
||||
hf.config.Global = filter
|
||||
}
|
||||
return hf
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column filter functions for the header
|
||||
func (hf *HeaderFilterBuilder) WithPerColumn(filters []func(string) string) *HeaderFilterBuilder {
|
||||
if len(filters) > 0 {
|
||||
hf.config.PerColumn = filters
|
||||
}
|
||||
return hf
|
||||
}
|
||||
|
||||
// AddColumnFilter adds a filter function for a specific column in the header
|
||||
func (hf *HeaderFilterBuilder) AddColumnFilter(filter func(string) string) *HeaderFilterBuilder {
|
||||
if filter != nil {
|
||||
hf.config.PerColumn = append(hf.config.PerColumn, filter)
|
||||
}
|
||||
return hf
|
||||
}
|
||||
|
||||
// RowFilterBuilder configures row filtering
|
||||
type RowFilterBuilder struct {
|
||||
parent *RowConfigBuilder
|
||||
config *tw.CellFilter
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent RowConfigBuilder
|
||||
func (rf *RowFilterBuilder) Build() *RowConfigBuilder {
|
||||
return rf.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets the global filter function for the rows
|
||||
func (rf *RowFilterBuilder) WithGlobal(filter func([]string) []string) *RowFilterBuilder {
|
||||
if filter != nil {
|
||||
rf.config.Global = filter
|
||||
}
|
||||
return rf
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column filter functions for the rows
|
||||
func (rf *RowFilterBuilder) WithPerColumn(filters []func(string) string) *RowFilterBuilder {
|
||||
if len(filters) > 0 {
|
||||
rf.config.PerColumn = filters
|
||||
}
|
||||
return rf
|
||||
}
|
||||
|
||||
// AddColumnFilter adds a filter function for a specific column in the rows
|
||||
func (rf *RowFilterBuilder) AddColumnFilter(filter func(string) string) *RowFilterBuilder {
|
||||
if filter != nil {
|
||||
rf.config.PerColumn = append(rf.config.PerColumn, filter)
|
||||
}
|
||||
return rf
|
||||
}
|
||||
|
||||
// FooterFilterBuilder configures footer filtering
|
||||
type FooterFilterBuilder struct {
|
||||
parent *FooterConfigBuilder
|
||||
config *tw.CellFilter
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent FooterConfigBuilder
|
||||
func (ff *FooterFilterBuilder) Build() *FooterConfigBuilder {
|
||||
return ff.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets the global filter function for the footer
|
||||
func (ff *FooterFilterBuilder) WithGlobal(filter func([]string) []string) *FooterFilterBuilder {
|
||||
if filter != nil {
|
||||
ff.config.Global = filter
|
||||
}
|
||||
return ff
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column filter functions for the footer
|
||||
func (ff *FooterFilterBuilder) WithPerColumn(filters []func(string) string) *FooterFilterBuilder {
|
||||
if len(filters) > 0 {
|
||||
ff.config.PerColumn = filters
|
||||
}
|
||||
return ff
|
||||
}
|
||||
|
||||
// AddColumnFilter adds a filter function for a specific column in the footer
|
||||
func (ff *FooterFilterBuilder) AddColumnFilter(filter func(string) string) *FooterFilterBuilder {
|
||||
if filter != nil {
|
||||
ff.config.PerColumn = append(ff.config.PerColumn, filter)
|
||||
}
|
||||
return ff
|
||||
}
|
||||
|
||||
// HeaderCallbacksBuilder configures header callbacks
|
||||
type HeaderCallbacksBuilder struct {
|
||||
parent *HeaderConfigBuilder
|
||||
config *tw.CellCallbacks
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent HeaderConfigBuilder
|
||||
func (hc *HeaderCallbacksBuilder) Build() *HeaderConfigBuilder {
|
||||
return hc.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets the global callback function for the header
|
||||
func (hc *HeaderCallbacksBuilder) WithGlobal(callback func()) *HeaderCallbacksBuilder {
|
||||
if callback != nil {
|
||||
hc.config.Global = callback
|
||||
}
|
||||
return hc
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column callback functions for the header
|
||||
func (hc *HeaderCallbacksBuilder) WithPerColumn(callbacks []func()) *HeaderCallbacksBuilder {
|
||||
if len(callbacks) > 0 {
|
||||
hc.config.PerColumn = callbacks
|
||||
}
|
||||
return hc
|
||||
}
|
||||
|
||||
// AddColumnCallback adds a callback function for a specific column in the header
|
||||
func (hc *HeaderCallbacksBuilder) AddColumnCallback(callback func()) *HeaderCallbacksBuilder {
|
||||
if callback != nil {
|
||||
hc.config.PerColumn = append(hc.config.PerColumn, callback)
|
||||
}
|
||||
return hc
|
||||
}
|
||||
|
||||
// RowCallbacksBuilder configures row callbacks
|
||||
type RowCallbacksBuilder struct {
|
||||
parent *RowConfigBuilder
|
||||
config *tw.CellCallbacks
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent RowConfigBuilder
|
||||
func (rc *RowCallbacksBuilder) Build() *RowConfigBuilder {
|
||||
return rc.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets the global callback function for the rows
|
||||
func (rc *RowCallbacksBuilder) WithGlobal(callback func()) *RowCallbacksBuilder {
|
||||
if callback != nil {
|
||||
rc.config.Global = callback
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column callback functions for the rows
|
||||
func (rc *RowCallbacksBuilder) WithPerColumn(callbacks []func()) *RowCallbacksBuilder {
|
||||
if len(callbacks) > 0 {
|
||||
rc.config.PerColumn = callbacks
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// AddColumnCallback adds a callback function for a specific column in the rows
|
||||
func (rc *RowCallbacksBuilder) AddColumnCallback(callback func()) *RowCallbacksBuilder {
|
||||
if callback != nil {
|
||||
rc.config.PerColumn = append(rc.config.PerColumn, callback)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// FooterCallbacksBuilder configures footer callbacks
|
||||
type FooterCallbacksBuilder struct {
|
||||
parent *FooterConfigBuilder
|
||||
config *tw.CellCallbacks
|
||||
section string
|
||||
}
|
||||
|
||||
// Build returns the parent FooterConfigBuilder
|
||||
func (fc *FooterCallbacksBuilder) Build() *FooterConfigBuilder {
|
||||
return fc.parent
|
||||
}
|
||||
|
||||
// WithGlobal sets the global callback function for the footer
|
||||
func (fc *FooterCallbacksBuilder) WithGlobal(callback func()) *FooterCallbacksBuilder {
|
||||
if callback != nil {
|
||||
fc.config.Global = callback
|
||||
}
|
||||
return fc
|
||||
}
|
||||
|
||||
// WithPerColumn sets per-column callback functions for the footer
|
||||
func (fc *FooterCallbacksBuilder) WithPerColumn(callbacks []func()) *FooterCallbacksBuilder {
|
||||
if len(callbacks) > 0 {
|
||||
fc.config.PerColumn = callbacks
|
||||
}
|
||||
return fc
|
||||
}
|
||||
|
||||
// AddColumnCallback adds a callback function for a specific column in the footer
|
||||
func (fc *FooterCallbacksBuilder) AddColumnCallback(callback func()) *FooterCallbacksBuilder {
|
||||
if callback != nil {
|
||||
fc.config.PerColumn = append(fc.config.PerColumn, callback)
|
||||
}
|
||||
return fc
|
||||
}
|
||||
88
vendor/github.com/olekukonko/tablewriter/csv.go
generated
vendored
88
vendor/github.com/olekukonko/tablewriter/csv.go
generated
vendored
@@ -1,10 +1,3 @@
|
||||
// Copyright 2014 Oleku Konko All rights reserved.
|
||||
// Use of this source code is governed by a MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This module is a Table Writer API for the Go Programming Language.
|
||||
// The protocols were written in pure Go and works on windows and unix systems
|
||||
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
@@ -13,40 +6,89 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Start A new table by importing from a CSV file
|
||||
// NewCSV Start A new table by importing from a CSV file
|
||||
// Takes io.Writer and csv File name
|
||||
func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) {
|
||||
func NewCSV(writer io.Writer, fileName string, hasHeader bool, opts ...Option) (*Table, error) {
|
||||
// Open the CSV file
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return &Table{}, err
|
||||
// Log implicitly handled by NewTable if logger is configured via opts
|
||||
return nil, err // Return nil *Table on error
|
||||
}
|
||||
defer file.Close()
|
||||
defer file.Close() // Ensure file is closed
|
||||
|
||||
// Create a CSV reader
|
||||
csvReader := csv.NewReader(file)
|
||||
t, err := NewCSVReader(writer, csvReader, hasHeader)
|
||||
return t, err
|
||||
|
||||
// Delegate to NewCSVReader, passing through the options
|
||||
return NewCSVReader(writer, csvReader, hasHeader, opts...)
|
||||
}
|
||||
|
||||
// Start a New Table Writer with csv.Reader
|
||||
// NewCSVReader Start a New Table Writer with csv.Reader
|
||||
// This enables customisation such as reader.Comma = ';'
|
||||
// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94
|
||||
func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) {
|
||||
t := NewWriter(writer)
|
||||
func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool, opts ...Option) (*Table, error) {
|
||||
// Create a new table instance using the modern API and provided options.
|
||||
// Options configure the table's appearance and behavior (renderer, borders, etc.).
|
||||
t := NewTable(writer, opts...) // Logger setup happens here if WithLogger/WithDebug is passed
|
||||
|
||||
// Process header row if specified
|
||||
if hasHeader {
|
||||
// Read the first row
|
||||
headers, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return &Table{}, err
|
||||
// Handle EOF specifically: means the CSV was empty or contained only an empty header line.
|
||||
if err == io.EOF {
|
||||
t.logger.Debug("NewCSVReader: CSV empty or only header found (EOF after header read attempt).")
|
||||
// Return the table configured by opts, but without data/header.
|
||||
// It's ready for Render() which will likely output nothing or just borders if configured.
|
||||
return t, nil
|
||||
}
|
||||
// Log other read errors
|
||||
t.logger.Errorf("NewCSVReader: Error reading CSV header: %v", err)
|
||||
return nil, err // Return nil *Table on critical read error
|
||||
}
|
||||
|
||||
// Check if the read header is genuinely empty (e.g., a blank line in the CSV)
|
||||
isEmptyHeader := true
|
||||
for _, h := range headers {
|
||||
if h != "" {
|
||||
isEmptyHeader = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isEmptyHeader {
|
||||
t.Header(headers) // Use the Table method to set the header data
|
||||
t.logger.Debugf("NewCSVReader: Header set from CSV: %v", headers)
|
||||
} else {
|
||||
t.logger.Debug("NewCSVReader: Read an empty header line, skipping setting table header.")
|
||||
}
|
||||
t.SetHeader(headers)
|
||||
}
|
||||
|
||||
// Process data rows
|
||||
rowCount := 0
|
||||
for {
|
||||
record, err := csvReader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return &Table{}, err
|
||||
break // Reached the end of the CSV data
|
||||
}
|
||||
t.Append(record)
|
||||
if err != nil {
|
||||
// Log other read errors during data processing
|
||||
t.logger.Errorf("NewCSVReader: Error reading CSV record: %v", err)
|
||||
return nil, err // Return nil *Table on critical read error
|
||||
}
|
||||
|
||||
// Append the record to the table's internal buffer (for batch rendering).
|
||||
// The Table.Append method handles conversion and storage.
|
||||
if appendErr := t.Append(record); appendErr != nil {
|
||||
t.logger.Errorf("NewCSVReader: Error appending record #%d: %v", rowCount+1, appendErr)
|
||||
// Decide if append error is fatal. For now, let's treat it as fatal.
|
||||
return nil, appendErr
|
||||
}
|
||||
rowCount++
|
||||
}
|
||||
t.logger.Debugf("NewCSVReader: Finished reading CSV. Appended %d data rows.", rowCount)
|
||||
|
||||
// Return the configured and populated table instance, ready for Render() call.
|
||||
return t, nil
|
||||
}
|
||||
|
||||
220
vendor/github.com/olekukonko/tablewriter/deprecated.go
generated
vendored
Normal file
220
vendor/github.com/olekukonko/tablewriter/deprecated.go
generated
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// WithBorders configures the table's border settings by updating the renderer's border configuration.
|
||||
// This function is deprecated and will be removed in a future version.
|
||||
//
|
||||
// Deprecated: Use [WithRendition] to configure border settings for renderers that support
|
||||
// [tw.Renditioning], or update the renderer's [tw.RenderConfig] directly via its Config() method.
|
||||
// This function has no effect if no renderer is set on the table.
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// table.Options(WithBorders(tw.Border{Top: true, Bottom: true}))
|
||||
// // New (recommended)
|
||||
// table.Options(WithRendition(tw.Rendition{Borders: tw.Border{Top: true, Bottom: true}}))
|
||||
//
|
||||
// Parameters:
|
||||
// - borders: The [tw.Border] configuration to apply to the renderer's borders.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An [Option] that updates the renderer's border settings if a renderer is set.
|
||||
// Logs a debug message if debugging is enabled and a renderer is present.
|
||||
func WithBorders(borders tw.Border) Option {
|
||||
return func(target *Table) {
|
||||
if target.renderer != nil {
|
||||
cfg := target.renderer.Config()
|
||||
cfg.Borders = borders
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithBorders applied to Table: %+v", borders)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Behavior is an alias for [tw.Behavior] to configure table behavior settings.
|
||||
// This type is deprecated and will be removed in a future version.
|
||||
//
|
||||
// Deprecated: Use [tw.Behavior] directly to configure settings such as auto-hiding empty
|
||||
// columns, trimming spaces, or controlling header/footer visibility.
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// var b tablewriter.Behavior = tablewriter.Behavior{AutoHide: tw.On}
|
||||
// // New (recommended)
|
||||
// var b tw.Behavior = tw.Behavior{AutoHide: tw.On}
|
||||
type Behavior tw.Behavior
|
||||
|
||||
// Settings is an alias for [tw.Settings] to configure renderer settings.
|
||||
// This type is deprecated and will be removed in a future version.
|
||||
//
|
||||
// Deprecated: Use [tw.Settings] directly to configure renderer settings, such as
|
||||
// separators and line styles.
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// var s tablewriter.Settings = tablewriter.Settings{Separator: "|"}
|
||||
// // New (recommended)
|
||||
// var s tw.Settings = tw.Settings{Separator: "|"}
|
||||
type Settings tw.Settings
|
||||
|
||||
// WithRendererSettings updates the renderer's settings, such as separators and line styles.
|
||||
// This function is deprecated and will be removed in a future version.
|
||||
//
|
||||
// Deprecated: Use [WithRendition] to update renderer settings for renderers that implement
|
||||
// [tw.Renditioning], or configure the renderer's [tw.Settings] directly via its
|
||||
// [tw.Renderer.Config] method. This function has no effect if no renderer is set.
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// table.Options(WithRendererSettings(tw.Settings{Separator: "|"}))
|
||||
// // New (recommended)
|
||||
// table.Options(WithRendition(tw.Rendition{Settings: tw.Settings{Separator: "|"}}))
|
||||
//
|
||||
// Parameters:
|
||||
// - settings: The [tw.Settings] configuration to apply to the renderer.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An [Option] that updates the renderer's settings if a renderer is set.
|
||||
// Logs a debug message if debugging is enabled and a renderer is present.
|
||||
func WithRendererSettings(settings tw.Settings) Option {
|
||||
return func(target *Table) {
|
||||
if target.renderer != nil {
|
||||
cfg := target.renderer.Config()
|
||||
cfg.Settings = settings
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRendererSettings applied to Table: %+v", settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithAlignment sets the text alignment for footer cells within the formatting configuration.
|
||||
// This method is deprecated and will be removed in the next version.
|
||||
//
|
||||
// Deprecated: Use [FooterConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal]
|
||||
// or [AlignmentConfigBuilder.WithPerColumn] to configure footer alignments.
|
||||
// Alternatively, apply a complete [tw.CellAlignment] configuration using
|
||||
// [WithFooterAlignmentConfig].
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// builder.Footer().Formatting().WithAlignment(tw.AlignRight)
|
||||
// // New (recommended)
|
||||
// builder.Footer().Alignment().WithGlobal(tw.AlignRight)
|
||||
// // Or
|
||||
// table.Options(WithFooterAlignmentConfig(tw.CellAlignment{Global: tw.AlignRight}))
|
||||
//
|
||||
// Parameters:
|
||||
// - align: The [tw.Align] value to set for footer cells. Valid values are
|
||||
// [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone].
|
||||
// Invalid alignments are ignored.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// The [FooterFormattingBuilder] instance for method chaining.
|
||||
func (ff *FooterFormattingBuilder) WithAlignment(align tw.Align) *FooterFormattingBuilder {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return ff
|
||||
}
|
||||
ff.config.Alignment = align
|
||||
return ff
|
||||
}
|
||||
|
||||
// WithAlignment sets the text alignment for header cells within the formatting configuration.
|
||||
// This method is deprecated and will be removed in the next version.
|
||||
//
|
||||
// Deprecated: Use [HeaderConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal]
|
||||
// or [AlignmentConfigBuilder.WithPerColumn] to configure header alignments.
|
||||
// Alternatively, apply a complete [tw.CellAlignment] configuration using
|
||||
// [WithHeaderAlignmentConfig].
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// builder.Header().Formatting().WithAlignment(tw.AlignCenter)
|
||||
// // New (recommended)
|
||||
// builder.Header().Alignment().WithGlobal(tw.AlignCenter)
|
||||
// // Or
|
||||
// table.Options(WithHeaderAlignmentConfig(tw.CellAlignment{Global: tw.AlignCenter}))
|
||||
//
|
||||
// Parameters:
|
||||
// - align: The [tw.Align] value to set for header cells. Valid values are
|
||||
// [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone].
|
||||
// Invalid alignments are ignored.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// The [HeaderFormattingBuilder] instance for method chaining.
|
||||
func (hf *HeaderFormattingBuilder) WithAlignment(align tw.Align) *HeaderFormattingBuilder {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return hf
|
||||
}
|
||||
hf.config.Alignment = align
|
||||
return hf
|
||||
}
|
||||
|
||||
// WithAlignment sets the text alignment for row cells within the formatting configuration.
|
||||
// This method is deprecated and will be removed in the next version.
|
||||
//
|
||||
// Deprecated: Use [RowConfigBuilder.Alignment] with [AlignmentConfigBuilder.WithGlobal]
|
||||
// or [AlignmentConfigBuilder.WithPerColumn] to configure row alignments.
|
||||
// Alternatively, apply a complete [tw.CellAlignment] configuration using
|
||||
// [WithRowAlignmentConfig].
|
||||
//
|
||||
// Example migration:
|
||||
//
|
||||
// // Old (deprecated)
|
||||
// builder.Row().Formatting().WithAlignment(tw.AlignLeft)
|
||||
// // New (recommended)
|
||||
// builder.Row().Alignment().WithGlobal(tw.AlignLeft)
|
||||
// // Or
|
||||
// table.Options(WithRowAlignmentConfig(tw.CellAlignment{Global: tw.AlignLeft}))
|
||||
//
|
||||
// Parameters:
|
||||
// - align: The [tw.Align] value to set for row cells. Valid values are
|
||||
// [tw.AlignLeft], [tw.AlignRight], [tw.AlignCenter], and [tw.AlignNone].
|
||||
// Invalid alignments are ignored.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// The [RowFormattingBuilder] instance for method chaining.
|
||||
func (rf *RowFormattingBuilder) WithAlignment(align tw.Align) *RowFormattingBuilder {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return rf
|
||||
}
|
||||
rf.config.Alignment = align
|
||||
return rf
|
||||
}
|
||||
|
||||
// WithTableMax sets the maximum width of the entire table in characters.
|
||||
// Negative values are ignored, and the change is logged if debugging is enabled.
|
||||
// The width constrains the table's rendering, potentially causing text wrapping or truncation
|
||||
// based on the configuration's wrapping settings (e.g., tw.WrapTruncate).
|
||||
// If debug logging is enabled via WithDebug(true), the applied width is logged.
|
||||
//
|
||||
// Deprecated: Use WithMaxWidth instead, which provides the same functionality with a clearer name
|
||||
// and consistent naming across the package. For example:
|
||||
//
|
||||
// tablewriter.NewTable(os.Stdout, tablewriter.WithMaxWidth(80))
|
||||
func WithTableMax(width int) Option {
|
||||
return func(target *Table) {
|
||||
if width < 0 {
|
||||
return
|
||||
}
|
||||
target.config.MaxWidth = width
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithTableMax applied to Table: %v", width)
|
||||
}
|
||||
}
|
||||
}
|
||||
903
vendor/github.com/olekukonko/tablewriter/option.go
generated
vendored
Normal file
903
vendor/github.com/olekukonko/tablewriter/option.go
generated
vendored
Normal file
@@ -0,0 +1,903 @@
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Option defines a function type for configuring a Table instance.
|
||||
type Option func(target *Table)
|
||||
|
||||
// WithAutoHide enables or disables automatic hiding of columns with empty data rows.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithAutoHide(state tw.State) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Behavior.AutoHide = state
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithAutoHide applied to Table: %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithColumnMax sets a global maximum column width for the table in streaming mode.
|
||||
// Negative values are ignored, and the change is logged if debugging is enabled.
|
||||
func WithColumnMax(width int) Option {
|
||||
return func(target *Table) {
|
||||
if width < 0 {
|
||||
return
|
||||
}
|
||||
target.config.Widths.Global = width
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithColumnMax applied to Table: %v", width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxWidth sets a global maximum table width for the table.
|
||||
// Negative values are ignored, and the change is logged if debugging is enabled.
|
||||
func WithMaxWidth(width int) Option {
|
||||
return func(target *Table) {
|
||||
if width < 0 {
|
||||
return
|
||||
}
|
||||
target.config.MaxWidth = width
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithTableMax applied to Table: %v", width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithWidths sets per-column widths for the table.
|
||||
// Negative widths are removed, and the change is logged if debugging is enabled.
|
||||
func WithWidths(width tw.CellWidth) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Widths = width
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithColumnWidths applied to Table: %v", width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithColumnWidths sets per-column widths for the table.
|
||||
// Negative widths are removed, and the change is logged if debugging is enabled.
|
||||
func WithColumnWidths(widths tw.Mapper[int, int]) Option {
|
||||
return func(target *Table) {
|
||||
for k, v := range widths {
|
||||
if v < 0 {
|
||||
delete(widths, k)
|
||||
}
|
||||
}
|
||||
target.config.Widths.PerColumn = widths
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithColumnWidths applied to Table: %v", widths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfig applies a custom configuration to the table by merging it with the default configuration.
|
||||
func WithConfig(cfg Config) Option {
|
||||
return func(target *Table) {
|
||||
target.config = mergeConfig(defaultConfig(), cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDebug enables or disables debug logging and adjusts the logger level accordingly.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithDebug(debug bool) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooter sets the table footers by calling the Footer method.
|
||||
func WithFooter(footers []string) Option {
|
||||
return func(target *Table) {
|
||||
target.Footer(footers)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterConfig applies a full footer configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterConfig(config tw.CellConfig) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Footer = config
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithFooterConfig applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterAlignmentConfig applies a footer alignment configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterAlignmentConfig(alignment tw.CellAlignment) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Footer.Alignment = alignment
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterAlignmentConfig applied to Table: %+v", alignment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterMergeMode sets the merge mode for footer cells.
|
||||
// Invalid merge modes are ignored, and the change is logged if debugging is enabled.
|
||||
func WithFooterMergeMode(mergeMode int) Option {
|
||||
return func(target *Table) {
|
||||
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
|
||||
return
|
||||
}
|
||||
target.config.Footer.Formatting.MergeMode = mergeMode
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterMergeMode applied to Table: %v", mergeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterAutoWrap sets the wrapping behavior for footer cells.
|
||||
// Invalid wrap modes are ignored, and the change is logged if debugging is enabled.
|
||||
func WithFooterAutoWrap(wrap int) Option {
|
||||
return func(target *Table) {
|
||||
if wrap < tw.WrapNone || wrap > tw.WrapBreak {
|
||||
return
|
||||
}
|
||||
target.config.Footer.Formatting.AutoWrap = wrap
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterAutoWrap applied to Table: %v", wrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterFilter sets the filter configuration for footer cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterFilter(filter tw.CellFilter) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Footer.Filter = filter
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithFooterFilter applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterCallbacks sets the callback configuration for footer cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterCallbacks(callbacks tw.CellCallbacks) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Footer.Callbacks = callbacks
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithFooterCallbacks applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterPaddingPerColumn sets per-column padding for footer cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterPaddingPerColumn(padding []tw.Padding) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Footer.Padding.PerColumn = padding
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterPaddingPerColumn applied to Table: %+v", padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterMaxWidth sets the maximum content width for footer cells.
|
||||
// Negative values are ignored, and the change is logged if debugging is enabled.
|
||||
func WithFooterMaxWidth(maxWidth int) Option {
|
||||
return func(target *Table) {
|
||||
if maxWidth < 0 {
|
||||
return
|
||||
}
|
||||
target.config.Footer.ColMaxWidths.Global = maxWidth
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterMaxWidth applied to Table: %v", maxWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeader sets the table headers by calling the Header method.
|
||||
func WithHeader(headers []string) Option {
|
||||
return func(target *Table) {
|
||||
target.Header(headers)
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderAlignment sets the text alignment for header cells.
|
||||
// Invalid alignments are ignored, and the change is logged if debugging is enabled.
|
||||
func WithHeaderAlignment(align tw.Align) Option {
|
||||
return func(target *Table) {
|
||||
if align != tw.AlignLeft && align != tw.AlignRight && align != tw.AlignCenter && align != tw.AlignNone {
|
||||
return
|
||||
}
|
||||
target.config.Header.Alignment.Global = align
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderAlignment applied to Table: %v", align)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderAutoWrap sets the wrapping behavior for header cells.
|
||||
// Invalid wrap modes are ignored, and the change is logged if debugging is enabled.
|
||||
func WithHeaderAutoWrap(wrap int) Option {
|
||||
return func(target *Table) {
|
||||
if wrap < tw.WrapNone || wrap > tw.WrapBreak {
|
||||
return
|
||||
}
|
||||
target.config.Header.Formatting.AutoWrap = wrap
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderAutoWrap applied to Table: %v", wrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderMergeMode sets the merge mode for header cells.
|
||||
// Invalid merge modes are ignored, and the change is logged if debugging is enabled.
|
||||
func WithHeaderMergeMode(mergeMode int) Option {
|
||||
return func(target *Table) {
|
||||
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
|
||||
return
|
||||
}
|
||||
target.config.Header.Formatting.MergeMode = mergeMode
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderMergeMode applied to Table: %v", mergeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderFilter sets the filter configuration for header cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderFilter(filter tw.CellFilter) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Filter = filter
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithHeaderFilter applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderCallbacks sets the callback configuration for header cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderCallbacks(callbacks tw.CellCallbacks) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Callbacks = callbacks
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithHeaderCallbacks applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderPaddingPerColumn sets per-column padding for header cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderPaddingPerColumn(padding []tw.Padding) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Padding.PerColumn = padding
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderPaddingPerColumn applied to Table: %+v", padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderMaxWidth sets the maximum content width for header cells.
|
||||
// Negative values are ignored, and the change is logged if debugging is enabled.
|
||||
func WithHeaderMaxWidth(maxWidth int) Option {
|
||||
return func(target *Table) {
|
||||
if maxWidth < 0 {
|
||||
return
|
||||
}
|
||||
target.config.Header.ColMaxWidths.Global = maxWidth
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderMaxWidth applied to Table: %v", maxWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowAlignment sets the text alignment for row cells.
|
||||
// Invalid alignments are ignored, and the change is logged if debugging is enabled.
|
||||
func WithRowAlignment(align tw.Align) Option {
|
||||
return func(target *Table) {
|
||||
if err := align.Validate(); err != nil {
|
||||
return
|
||||
}
|
||||
target.config.Row.Alignment.Global = align
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowAlignment applied to Table: %v", align)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowAutoWrap sets the wrapping behavior for row cells.
|
||||
// Invalid wrap modes are ignored, and the change is logged if debugging is enabled.
|
||||
func WithRowAutoWrap(wrap int) Option {
|
||||
return func(target *Table) {
|
||||
if wrap < tw.WrapNone || wrap > tw.WrapBreak {
|
||||
return
|
||||
}
|
||||
target.config.Row.Formatting.AutoWrap = wrap
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowAutoWrap applied to Table: %v", wrap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowMergeMode sets the merge mode for row cells.
|
||||
// Invalid merge modes are ignored, and the change is logged if debugging is enabled.
|
||||
func WithRowMergeMode(mergeMode int) Option {
|
||||
return func(target *Table) {
|
||||
if mergeMode < tw.MergeNone || mergeMode > tw.MergeHierarchical {
|
||||
return
|
||||
}
|
||||
target.config.Row.Formatting.MergeMode = mergeMode
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowMergeMode applied to Table: %v", mergeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowFilter sets the filter configuration for row cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRowFilter(filter tw.CellFilter) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Row.Filter = filter
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithRowFilter applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowCallbacks sets the callback configuration for row cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRowCallbacks(callbacks tw.CellCallbacks) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Row.Callbacks = callbacks
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithRowCallbacks applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowPaddingPerColumn sets per-column padding for row cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRowPaddingPerColumn(padding []tw.Padding) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Row.Padding.PerColumn = padding
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowPaddingPerColumn applied to Table: %+v", padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderAlignmentConfig applies a header alignment configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderAlignmentConfig(alignment tw.CellAlignment) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Alignment = alignment
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderAlignmentConfig applied to Table: %+v", alignment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderConfig applies a full header configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderConfig(config tw.CellConfig) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header = config
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithHeaderConfig applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets a custom logger for the table and updates the renderer if present.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithLogger(logger *ll.Logger) Option {
|
||||
return func(target *Table) {
|
||||
target.logger = logger
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithLogger applied to Table.")
|
||||
if target.renderer != nil {
|
||||
target.renderer.Logger(target.logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRenderer sets a custom renderer for the table and attaches the logger if present.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRenderer(f tw.Renderer) Option {
|
||||
return func(target *Table) {
|
||||
target.renderer = f
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRenderer applied to Table: %T", f)
|
||||
f.Logger(target.logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowConfig applies a full row configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRowConfig(config tw.CellConfig) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Row = config
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithRowConfig applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowAlignmentConfig applies a row alignment configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRowAlignmentConfig(alignment tw.CellAlignment) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Row.Alignment = alignment
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowAlignmentConfig applied to Table: %+v", alignment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowMaxWidth sets the maximum content width for row cells.
|
||||
// Negative values are ignored, and the change is logged if debugging is enabled.
|
||||
func WithRowMaxWidth(maxWidth int) Option {
|
||||
return func(target *Table) {
|
||||
if maxWidth < 0 {
|
||||
return
|
||||
}
|
||||
target.config.Row.ColMaxWidths.Global = maxWidth
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowMaxWidth applied to Table: %v", maxWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithStreaming applies a streaming configuration to the table by merging it with the existing configuration.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithStreaming(c tw.StreamConfig) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Stream = mergeStreamConfig(target.config.Stream, c)
|
||||
if target.logger != nil {
|
||||
target.logger.Debug("Option: WithStreaming applied to Table.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithStringer sets a custom stringer function for converting row data and clears the stringer cache.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithStringer(stringer interface{}) Option {
|
||||
return func(t *Table) {
|
||||
t.stringer = stringer
|
||||
t.stringerCacheMu.Lock()
|
||||
t.stringerCache = make(map[reflect.Type]reflect.Value)
|
||||
t.stringerCacheMu.Unlock()
|
||||
if t.logger != nil {
|
||||
t.logger.Debug("Stringer updated, cache cleared")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithStringerCache enables caching for the stringer function.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithStringerCache() Option {
|
||||
return func(t *Table) {
|
||||
t.stringerCacheEnabled = true
|
||||
if t.logger != nil {
|
||||
t.logger.Debug("Option: WithStringerCache enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrimSpace sets whether leading and trailing spaces are automatically trimmed.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithTrimSpace(state tw.State) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Behavior.TrimSpace = state
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithTrimSpace applied to Table: %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderAutoFormat enables or disables automatic formatting for header cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderAutoFormat(state tw.State) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Formatting.AutoFormat = state
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderAutoFormat applied to Table: %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterAutoFormat enables or disables automatic formatting for footer cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterAutoFormat(state tw.State) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Footer.Formatting.AutoFormat = state
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterAutoFormat applied to Table: %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRowAutoFormat enables or disables automatic formatting for row cells.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRowAutoFormat(state tw.State) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Row.Formatting.AutoFormat = state
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRowAutoFormat applied to Table: %v", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeaderControl sets the control behavior for the table header.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithHeaderControl(control tw.Control) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Behavior.Header = control
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithHeaderControl applied to Table: %v", control)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFooterControl sets the control behavior for the table footer.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithFooterControl(control tw.Control) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Behavior.Footer = control
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithFooterControl applied to Table: %v", control)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithAlignment sets the default column alignment for the header, rows, and footer.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithAlignment(alignment tw.Alignment) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Alignment.PerColumn = alignment
|
||||
target.config.Row.Alignment.PerColumn = alignment
|
||||
target.config.Footer.Alignment.PerColumn = alignment
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithAlignment applied to Table: %+v", alignment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithBehavior applies a behavior configuration to the table.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithBehavior(behavior tw.Behavior) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Behavior = behavior
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithBehavior applied to Table: %+v", behavior)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithPadding sets the global padding for the header, rows, and footer.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithPadding(padding tw.Padding) Option {
|
||||
return func(target *Table) {
|
||||
target.config.Header.Padding.Global = padding
|
||||
target.config.Row.Padding.Global = padding
|
||||
target.config.Footer.Padding.Global = padding
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithPadding applied to Table: %+v", padding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRendition allows updating the active renderer's rendition configuration
|
||||
// by merging the provided rendition.
|
||||
// If the renderer does not implement tw.Renditioning, a warning is logged.
|
||||
// Logs the change if debugging is enabled.
|
||||
func WithRendition(rendition tw.Rendition) Option {
|
||||
return func(target *Table) {
|
||||
if target.renderer == nil {
|
||||
if target.logger != nil {
|
||||
target.logger.Warn("Option: WithRendition: No renderer set on table.")
|
||||
}
|
||||
return
|
||||
}
|
||||
if ru, ok := target.renderer.(tw.Renditioning); ok {
|
||||
ru.Rendition(rendition)
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", rendition)
|
||||
}
|
||||
} else {
|
||||
if target.logger != nil {
|
||||
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithEastAsian configures the global East Asian width calculation setting.
|
||||
// - enable=true: Enables East Asian width calculations. CJK and ambiguous characters
|
||||
// are typically measured as double width.
|
||||
// - enable=false: Disables East Asian width calculations. Characters are generally
|
||||
// measured as single width, subject to Unicode standards.
|
||||
//
|
||||
// This setting affects all subsequent display width calculations using the twdw package.
|
||||
func WithEastAsian(enable bool) Option {
|
||||
return func(target *Table) {
|
||||
twwidth.SetEastAsian(enable)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCondition provides a way to set a custom global runewidth.Condition
|
||||
// that will be used for all subsequent display width calculations by the twwidth (twdw) package.
|
||||
//
|
||||
// The runewidth.Condition object allows for more fine-grained control over how rune widths
|
||||
// are determined, beyond just toggling EastAsianWidth. This could include settings for
|
||||
// ambiguous width characters or other future properties of runewidth.Condition.
|
||||
func WithCondition(condition *runewidth.Condition) Option {
|
||||
return func(target *Table) {
|
||||
twwidth.SetCondition(condition)
|
||||
}
|
||||
}
|
||||
|
||||
// WithSymbols sets the symbols used for drawing table borders and separators.
|
||||
// The symbols are applied to the table's renderer configuration, if a renderer is set.
|
||||
// If no renderer is set (target.renderer is nil), this option has no effect. .
|
||||
func WithSymbols(symbols tw.Symbols) Option {
|
||||
return func(target *Table) {
|
||||
if target.renderer != nil {
|
||||
cfg := target.renderer.Config()
|
||||
cfg.Symbols = symbols
|
||||
|
||||
if ru, ok := target.renderer.(tw.Renditioning); ok {
|
||||
ru.Rendition(cfg)
|
||||
if target.logger != nil {
|
||||
target.logger.Debugf("Option: WithRendition: Applied to renderer via Renditioning.SetRendition(): %+v", cfg)
|
||||
}
|
||||
} else {
|
||||
if target.logger != nil {
|
||||
target.logger.Warnf("Option: WithRendition: Current renderer type %T does not implement tw.Renditioning. Rendition may not be applied as expected.", target.renderer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
MaxWidth: 0,
|
||||
Header: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoWrap: tw.WrapTruncate,
|
||||
AutoFormat: tw.On,
|
||||
MergeMode: tw.MergeNone,
|
||||
},
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.PaddingDefault,
|
||||
},
|
||||
Alignment: tw.CellAlignment{
|
||||
Global: tw.AlignCenter,
|
||||
PerColumn: []tw.Align{},
|
||||
},
|
||||
},
|
||||
Row: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoWrap: tw.WrapNormal,
|
||||
AutoFormat: tw.Off,
|
||||
MergeMode: tw.MergeNone,
|
||||
},
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.PaddingDefault,
|
||||
},
|
||||
Alignment: tw.CellAlignment{
|
||||
Global: tw.AlignLeft,
|
||||
PerColumn: []tw.Align{},
|
||||
},
|
||||
},
|
||||
Footer: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoWrap: tw.WrapNormal,
|
||||
AutoFormat: tw.Off,
|
||||
MergeMode: tw.MergeNone,
|
||||
},
|
||||
Padding: tw.CellPadding{
|
||||
Global: tw.PaddingDefault,
|
||||
},
|
||||
Alignment: tw.CellAlignment{
|
||||
Global: tw.AlignRight,
|
||||
PerColumn: []tw.Align{},
|
||||
},
|
||||
},
|
||||
Stream: tw.StreamConfig{
|
||||
Enable: false,
|
||||
StrictColumns: false,
|
||||
},
|
||||
Debug: false,
|
||||
Behavior: tw.Behavior{
|
||||
AutoHide: tw.Off,
|
||||
TrimSpace: tw.On,
|
||||
Structs: tw.Struct{
|
||||
AutoHeader: tw.Off,
|
||||
Tags: []string{"json", "db"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCellConfig merges a source CellConfig into a destination CellConfig, prioritizing non-default source values.
|
||||
// It handles deep merging for complex fields like padding and callbacks.
|
||||
func mergeCellConfig(dst, src tw.CellConfig) tw.CellConfig {
|
||||
if src.Formatting.Alignment != tw.Empty {
|
||||
dst.Formatting.Alignment = src.Formatting.Alignment
|
||||
}
|
||||
|
||||
if src.Formatting.AutoWrap != 0 {
|
||||
dst.Formatting.AutoWrap = src.Formatting.AutoWrap
|
||||
}
|
||||
if src.ColMaxWidths.Global != 0 {
|
||||
dst.ColMaxWidths.Global = src.ColMaxWidths.Global
|
||||
}
|
||||
if src.Formatting.MergeMode != 0 {
|
||||
dst.Formatting.MergeMode = src.Formatting.MergeMode
|
||||
}
|
||||
|
||||
dst.Formatting.AutoFormat = src.Formatting.AutoFormat
|
||||
|
||||
if src.Padding.Global.Paddable() {
|
||||
dst.Padding.Global = src.Padding.Global
|
||||
}
|
||||
|
||||
if len(src.Padding.PerColumn) > 0 {
|
||||
if dst.Padding.PerColumn == nil {
|
||||
dst.Padding.PerColumn = make([]tw.Padding, len(src.Padding.PerColumn))
|
||||
} else if len(src.Padding.PerColumn) > len(dst.Padding.PerColumn) {
|
||||
dst.Padding.PerColumn = append(dst.Padding.PerColumn, make([]tw.Padding, len(src.Padding.PerColumn)-len(dst.Padding.PerColumn))...)
|
||||
}
|
||||
for i, pad := range src.Padding.PerColumn {
|
||||
if pad.Paddable() {
|
||||
dst.Padding.PerColumn[i] = pad
|
||||
}
|
||||
}
|
||||
}
|
||||
if src.Callbacks.Global != nil {
|
||||
dst.Callbacks.Global = src.Callbacks.Global
|
||||
}
|
||||
if len(src.Callbacks.PerColumn) > 0 {
|
||||
if dst.Callbacks.PerColumn == nil {
|
||||
dst.Callbacks.PerColumn = make([]func(), len(src.Callbacks.PerColumn))
|
||||
} else if len(src.Callbacks.PerColumn) > len(dst.Callbacks.PerColumn) {
|
||||
dst.Callbacks.PerColumn = append(dst.Callbacks.PerColumn, make([]func(), len(src.Callbacks.PerColumn)-len(dst.Callbacks.PerColumn))...)
|
||||
}
|
||||
for i, cb := range src.Callbacks.PerColumn {
|
||||
if cb != nil {
|
||||
dst.Callbacks.PerColumn[i] = cb
|
||||
}
|
||||
}
|
||||
}
|
||||
if src.Filter.Global != nil {
|
||||
dst.Filter.Global = src.Filter.Global
|
||||
}
|
||||
if len(src.Filter.PerColumn) > 0 {
|
||||
if dst.Filter.PerColumn == nil {
|
||||
dst.Filter.PerColumn = make([]func(string) string, len(src.Filter.PerColumn))
|
||||
} else if len(src.Filter.PerColumn) > len(dst.Filter.PerColumn) {
|
||||
dst.Filter.PerColumn = append(dst.Filter.PerColumn, make([]func(string) string, len(src.Filter.PerColumn)-len(dst.Filter.PerColumn))...)
|
||||
}
|
||||
for i, filter := range src.Filter.PerColumn {
|
||||
if filter != nil {
|
||||
dst.Filter.PerColumn[i] = filter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge Alignment
|
||||
if src.Alignment.Global != tw.Empty {
|
||||
dst.Alignment.Global = src.Alignment.Global
|
||||
}
|
||||
|
||||
if len(src.Alignment.PerColumn) > 0 {
|
||||
if dst.Alignment.PerColumn == nil {
|
||||
dst.Alignment.PerColumn = make([]tw.Align, len(src.Alignment.PerColumn))
|
||||
} else if len(src.Alignment.PerColumn) > len(dst.Alignment.PerColumn) {
|
||||
dst.Alignment.PerColumn = append(dst.Alignment.PerColumn, make([]tw.Align, len(src.Alignment.PerColumn)-len(dst.Alignment.PerColumn))...)
|
||||
}
|
||||
for i, align := range src.Alignment.PerColumn {
|
||||
if align != tw.Skip {
|
||||
dst.Alignment.PerColumn[i] = align
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(src.ColumnAligns) > 0 {
|
||||
if dst.ColumnAligns == nil {
|
||||
dst.ColumnAligns = make([]tw.Align, len(src.ColumnAligns))
|
||||
} else if len(src.ColumnAligns) > len(dst.ColumnAligns) {
|
||||
dst.ColumnAligns = append(dst.ColumnAligns, make([]tw.Align, len(src.ColumnAligns)-len(dst.ColumnAligns))...)
|
||||
}
|
||||
for i, align := range src.ColumnAligns {
|
||||
if align != tw.Skip {
|
||||
dst.ColumnAligns[i] = align
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(src.ColMaxWidths.PerColumn) > 0 {
|
||||
if dst.ColMaxWidths.PerColumn == nil {
|
||||
dst.ColMaxWidths.PerColumn = make(map[int]int)
|
||||
}
|
||||
for k, v := range src.ColMaxWidths.PerColumn {
|
||||
if v != 0 {
|
||||
dst.ColMaxWidths.PerColumn[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// mergeConfig merges a source Config into a destination Config, prioritizing non-default source values.
|
||||
// It performs deep merging for complex types like Header, Row, Footer, and Stream.
|
||||
func mergeConfig(dst, src Config) Config {
|
||||
if src.MaxWidth != 0 {
|
||||
dst.MaxWidth = src.MaxWidth
|
||||
}
|
||||
|
||||
dst.Debug = src.Debug || dst.Debug
|
||||
dst.Behavior.AutoHide = src.Behavior.AutoHide
|
||||
dst.Behavior.TrimSpace = src.Behavior.TrimSpace
|
||||
dst.Behavior.Compact = src.Behavior.Compact
|
||||
dst.Behavior.Header = src.Behavior.Header
|
||||
dst.Behavior.Footer = src.Behavior.Footer
|
||||
dst.Behavior.Footer = src.Behavior.Footer
|
||||
|
||||
dst.Behavior.Structs.AutoHeader = src.Behavior.Structs.AutoHeader
|
||||
|
||||
// check lent of tags
|
||||
if len(src.Behavior.Structs.Tags) > 0 {
|
||||
dst.Behavior.Structs.Tags = src.Behavior.Structs.Tags
|
||||
}
|
||||
|
||||
if src.Widths.Global != 0 {
|
||||
dst.Widths.Global = src.Widths.Global
|
||||
}
|
||||
if len(src.Widths.PerColumn) > 0 {
|
||||
if dst.Widths.PerColumn == nil {
|
||||
dst.Widths.PerColumn = make(map[int]int)
|
||||
}
|
||||
for k, v := range src.Widths.PerColumn {
|
||||
if v != 0 {
|
||||
dst.Widths.PerColumn[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dst.Header = mergeCellConfig(dst.Header, src.Header)
|
||||
dst.Row = mergeCellConfig(dst.Row, src.Row)
|
||||
dst.Footer = mergeCellConfig(dst.Footer, src.Footer)
|
||||
dst.Stream = mergeStreamConfig(dst.Stream, src.Stream)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// mergeStreamConfig merges a source StreamConfig into a destination StreamConfig, prioritizing non-default source values.
|
||||
func mergeStreamConfig(dst, src tw.StreamConfig) tw.StreamConfig {
|
||||
if src.Enable {
|
||||
dst.Enable = true
|
||||
}
|
||||
|
||||
dst.StrictColumns = src.StrictColumns
|
||||
return dst
|
||||
}
|
||||
|
||||
// padLine pads a line to the specified column count by appending empty strings as needed.
|
||||
func padLine(line []string, numCols int) []string {
|
||||
if len(line) >= numCols {
|
||||
return line
|
||||
}
|
||||
padded := make([]string, numCols)
|
||||
copy(padded, line)
|
||||
for i := len(line); i < numCols; i++ {
|
||||
padded[i] = tw.Empty
|
||||
}
|
||||
return padded
|
||||
}
|
||||
237
vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go
generated
vendored
Normal file
237
vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go
generated
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright 2014 Oleku Konko All rights reserved.
|
||||
// Use of this source code is governed by a MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This module is a Table Writer API for the Go Programming Language.
|
||||
// The protocols were written in pure Go and works on windows and unix systems
|
||||
|
||||
package twwarp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth" // IMPORT YOUR NEW PACKAGE
|
||||
"github.com/rivo/uniseg"
|
||||
// "github.com/mattn/go-runewidth" // This can be removed if all direct uses are gone
|
||||
)
|
||||
|
||||
const (
|
||||
nl = "\n"
|
||||
sp = " "
|
||||
)
|
||||
|
||||
const defaultPenalty = 1e5
|
||||
|
||||
func SplitWords(s string) []string {
|
||||
words := make([]string, 0, len(s)/5)
|
||||
var wordBegin int
|
||||
wordPending := false
|
||||
for i, c := range s {
|
||||
if unicode.IsSpace(c) {
|
||||
if wordPending {
|
||||
words = append(words, s[wordBegin:i])
|
||||
wordPending = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !wordPending {
|
||||
wordBegin = i
|
||||
wordPending = true
|
||||
}
|
||||
}
|
||||
if wordPending {
|
||||
words = append(words, s[wordBegin:])
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// WrapString wraps s into a paragraph of lines of length lim, with minimal
|
||||
// raggedness.
|
||||
func WrapString(s string, lim int) ([]string, int) {
|
||||
if s == sp {
|
||||
return []string{sp}, lim
|
||||
}
|
||||
words := SplitWords(s)
|
||||
if len(words) == 0 {
|
||||
return []string{""}, lim
|
||||
}
|
||||
var lines []string
|
||||
max := 0
|
||||
for _, v := range words {
|
||||
// max = runewidth.StringWidth(v) // OLD
|
||||
max = twwidth.Width(v) // NEW: Use twdw.Width
|
||||
if max > lim {
|
||||
lim = max
|
||||
}
|
||||
}
|
||||
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
|
||||
lines = append(lines, strings.Join(line, sp))
|
||||
}
|
||||
return lines, lim
|
||||
}
|
||||
|
||||
// WrapStringWithSpaces wraps a string into lines of a specified display width while preserving
|
||||
// leading and trailing spaces. It splits the input string into words, condenses internal multiple
|
||||
// spaces to a single space, and wraps the content to fit within the given width limit, measured
|
||||
// using Unicode-aware display width. The function is used in the logging library to format log
|
||||
// messages for consistent output. It returns the wrapped lines as a slice of strings and the
|
||||
// adjusted width limit, which may increase if a single word exceeds the input limit. Thread-safe
|
||||
// as it does not modify shared state.
|
||||
func WrapStringWithSpaces(s string, lim int) ([]string, int) {
|
||||
if len(s) == 0 {
|
||||
return []string{""}, lim
|
||||
}
|
||||
if strings.TrimSpace(s) == "" { // All spaces
|
||||
// if runewidth.StringWidth(s) <= lim { // OLD
|
||||
if twwidth.Width(s) <= lim { // NEW: Use twdw.Width
|
||||
// return []string{s}, runewidth.StringWidth(s) // OLD
|
||||
return []string{s}, twwidth.Width(s) // NEW: Use twdw.Width
|
||||
}
|
||||
// For very long all-space strings, "wrap" by truncating to the limit.
|
||||
if lim > 0 {
|
||||
substring, _ := stringToDisplayWidth(s, lim)
|
||||
return []string{substring}, lim
|
||||
}
|
||||
return []string{""}, lim
|
||||
}
|
||||
|
||||
var leadingSpaces, trailingSpaces, coreContent string
|
||||
firstNonSpace := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
|
||||
leadingSpaces = s[:firstNonSpace]
|
||||
lastNonSpace := strings.LastIndexFunc(s, func(r rune) bool { return !unicode.IsSpace(r) })
|
||||
trailingSpaces = s[lastNonSpace+1:]
|
||||
coreContent = s[firstNonSpace : lastNonSpace+1]
|
||||
|
||||
if coreContent == "" {
|
||||
return []string{leadingSpaces + trailingSpaces}, lim
|
||||
}
|
||||
|
||||
words := SplitWords(coreContent)
|
||||
if len(words) == 0 {
|
||||
return []string{leadingSpaces + trailingSpaces}, lim
|
||||
}
|
||||
|
||||
var lines []string
|
||||
currentLim := lim
|
||||
|
||||
maxCoreWordWidth := 0
|
||||
for _, v := range words {
|
||||
// w := runewidth.StringWidth(v) // OLD
|
||||
w := twwidth.Width(v) // NEW: Use twdw.Width
|
||||
if w > maxCoreWordWidth {
|
||||
maxCoreWordWidth = w
|
||||
}
|
||||
}
|
||||
|
||||
if maxCoreWordWidth > currentLim {
|
||||
currentLim = maxCoreWordWidth
|
||||
}
|
||||
|
||||
wrappedWordLines := WrapWords(words, 1, currentLim, defaultPenalty)
|
||||
|
||||
for i, lineWords := range wrappedWordLines {
|
||||
joinedLine := strings.Join(lineWords, sp)
|
||||
finalLine := leadingSpaces + joinedLine
|
||||
if i == len(wrappedWordLines)-1 { // Last line
|
||||
finalLine += trailingSpaces
|
||||
}
|
||||
lines = append(lines, finalLine)
|
||||
}
|
||||
return lines, currentLim
|
||||
}
|
||||
|
||||
// stringToDisplayWidth returns a substring of s that has a display width
|
||||
// as close as possible to, but not exceeding, targetWidth.
|
||||
// It returns the substring and its actual display width.
|
||||
func stringToDisplayWidth(s string, targetWidth int) (substring string, actualWidth int) {
|
||||
if targetWidth <= 0 {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
var currentWidth int
|
||||
var endIndex int // Tracks the byte index in the original string
|
||||
|
||||
g := uniseg.NewGraphemes(s)
|
||||
for g.Next() {
|
||||
grapheme := g.Str()
|
||||
// graphemeWidth := runewidth.StringWidth(grapheme) // OLD
|
||||
graphemeWidth := twwidth.Width(grapheme) // NEW: Use twdw.Width
|
||||
|
||||
if currentWidth+graphemeWidth > targetWidth {
|
||||
break
|
||||
}
|
||||
|
||||
currentWidth += graphemeWidth
|
||||
_, e := g.Positions()
|
||||
endIndex = e
|
||||
}
|
||||
return s[:endIndex], currentWidth
|
||||
}
|
||||
|
||||
// WrapWords is the low-level line-breaking algorithm, useful if you need more
|
||||
// control over the details of the text wrapping process. For most uses,
|
||||
// WrapString will be sufficient and more convenient.
|
||||
//
|
||||
// WrapWords splits a list of words into lines with minimal "raggedness",
|
||||
// treating each rune as one unit, accounting for spc units between adjacent
|
||||
// words on each line, and attempting to limit lines to lim units. Raggedness
|
||||
// is the total error over all lines, where error is the square of the
|
||||
// difference of the length of the line and lim. Too-long lines (which only
|
||||
// happen when a single word is longer than lim units) have pen penalty units
|
||||
// added to the error.
|
||||
func WrapWords(words []string, spc, lim, pen int) [][]string {
|
||||
n := len(words)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
lengths := make([]int, n)
|
||||
for i := 0; i < n; i++ {
|
||||
// lengths[i] = runewidth.StringWidth(words[i]) // OLD
|
||||
lengths[i] = twwidth.Width(words[i]) // NEW: Use twdw.Width
|
||||
}
|
||||
nbrk := make([]int, n)
|
||||
cost := make([]int, n)
|
||||
for i := range cost {
|
||||
cost[i] = math.MaxInt32
|
||||
}
|
||||
remainderLen := lengths[n-1] // Uses updated lengths
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if i < n-1 {
|
||||
remainderLen += spc + lengths[i]
|
||||
}
|
||||
if remainderLen <= lim {
|
||||
cost[i] = 0
|
||||
nbrk[i] = n
|
||||
continue
|
||||
}
|
||||
phraseLen := lengths[i]
|
||||
for j := i + 1; j < n; j++ {
|
||||
if j > i+1 {
|
||||
phraseLen += spc + lengths[j-1]
|
||||
}
|
||||
d := lim - phraseLen
|
||||
c := d*d + cost[j]
|
||||
if phraseLen > lim {
|
||||
c += pen // too-long lines get a worse penalty
|
||||
}
|
||||
if c < cost[i] {
|
||||
cost[i] = c
|
||||
nbrk[i] = j
|
||||
}
|
||||
}
|
||||
}
|
||||
var lines [][]string
|
||||
i := 0
|
||||
for i < n {
|
||||
lines = append(lines, words[i:nbrk[i]])
|
||||
i = nbrk[i]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// getLines decomposes a multiline string into a slice of strings.
|
||||
func getLines(s string) []string {
|
||||
return strings.Split(s, nl)
|
||||
}
|
||||
321
vendor/github.com/olekukonko/tablewriter/pkg/twwidth/width.go
generated
vendored
Normal file
321
vendor/github.com/olekukonko/tablewriter/pkg/twwidth/width.go
generated
vendored
Normal file
@@ -0,0 +1,321 @@
|
||||
package twwidth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// condition holds the global runewidth configuration, including East Asian width settings.
|
||||
var condition *runewidth.Condition
|
||||
|
||||
// mu protects access to condition and widthCache for thread safety.
|
||||
var mu sync.Mutex
|
||||
|
||||
// ansi is a compiled regular expression for stripping ANSI escape codes from strings.
|
||||
var ansi = Filter()
|
||||
|
||||
func init() {
|
||||
condition = runewidth.NewCondition()
|
||||
widthCache = make(map[cacheKey]int)
|
||||
}
|
||||
|
||||
// cacheKey is used as a key for memoizing string width results in widthCache.
|
||||
type cacheKey struct {
|
||||
str string // Input string
|
||||
eastAsianWidth bool // East Asian width setting
|
||||
}
|
||||
|
||||
// widthCache stores memoized results of Width calculations to improve performance.
|
||||
var widthCache map[cacheKey]int
|
||||
|
||||
// Filter compiles and returns a regular expression for matching ANSI escape sequences,
|
||||
// including CSI (Control Sequence Introducer) and OSC (Operating System Command) sequences.
|
||||
// The returned regex can be used to strip ANSI codes from strings.
|
||||
func Filter() *regexp.Regexp {
|
||||
var regESC = "\x1b" // ASCII escape character
|
||||
var regBEL = "\x07" // ASCII bell character
|
||||
|
||||
// ANSI string terminator: either ESC+\ or BEL
|
||||
var regST = "(" + regexp.QuoteMeta(regESC+"\\") + "|" + regexp.QuoteMeta(regBEL) + ")"
|
||||
// Control Sequence Introducer (CSI): ESC[ followed by parameters and a final byte
|
||||
var regCSI = regexp.QuoteMeta(regESC+"[") + "[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]"
|
||||
// Operating System Command (OSC): ESC] followed by arbitrary content until a terminator
|
||||
var regOSC = regexp.QuoteMeta(regESC+"]") + ".*?" + regST
|
||||
|
||||
// Combine CSI and OSC patterns into a single regex
|
||||
return regexp.MustCompile("(" + regCSI + "|" + regOSC + ")")
|
||||
}
|
||||
|
||||
// SetEastAsian enables or disables East Asian width handling for width calculations.
|
||||
// When the setting changes, the width cache is cleared to ensure accuracy.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// twdw.SetEastAsian(true) // Enable East Asian width handling
|
||||
func SetEastAsian(enable bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if condition.EastAsianWidth != enable {
|
||||
condition.EastAsianWidth = enable
|
||||
widthCache = make(map[cacheKey]int) // Clear cache on setting change
|
||||
}
|
||||
}
|
||||
|
||||
// SetCondition updates the global runewidth.Condition used for width calculations.
|
||||
// When the condition is changed, the width cache is cleared.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// newCond := runewidth.NewCondition()
|
||||
// newCond.EastAsianWidth = true
|
||||
// twdw.SetCondition(newCond)
|
||||
func SetCondition(newCond *runewidth.Condition) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
condition = newCond
|
||||
widthCache = make(map[cacheKey]int) // Clear cache on setting change
|
||||
}
|
||||
|
||||
// Width calculates the visual width of a string, excluding ANSI escape sequences,
|
||||
// using the go-runewidth package for accurate Unicode handling. It accounts for the
|
||||
// current East Asian width setting and caches results for performance.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// width := twdw.Width("Hello\x1b[31mWorld") // Returns 10
|
||||
func Width(str string) int {
|
||||
mu.Lock()
|
||||
key := cacheKey{str: str, eastAsianWidth: condition.EastAsianWidth}
|
||||
if w, found := widthCache[key]; found {
|
||||
mu.Unlock()
|
||||
return w
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
// Use a temporary condition to avoid holding the lock during calculation
|
||||
tempCond := runewidth.NewCondition()
|
||||
tempCond.EastAsianWidth = key.eastAsianWidth
|
||||
|
||||
stripped := ansi.ReplaceAllLiteralString(str, "")
|
||||
calculatedWidth := tempCond.StringWidth(stripped)
|
||||
|
||||
mu.Lock()
|
||||
widthCache[key] = calculatedWidth
|
||||
mu.Unlock()
|
||||
|
||||
return calculatedWidth
|
||||
}
|
||||
|
||||
// WidthNoCache calculates the visual width of a string without using or
|
||||
// updating the global cache. It uses the current global East Asian width setting.
|
||||
// This function is intended for internal use (e.g., benchmarking) and is thread-safe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// width := twdw.WidthNoCache("Hello\x1b[31mWorld") // Returns 10
|
||||
func WidthNoCache(str string) int {
|
||||
mu.Lock()
|
||||
currentEA := condition.EastAsianWidth
|
||||
mu.Unlock()
|
||||
|
||||
tempCond := runewidth.NewCondition()
|
||||
tempCond.EastAsianWidth = currentEA
|
||||
|
||||
stripped := ansi.ReplaceAllLiteralString(str, "")
|
||||
return tempCond.StringWidth(stripped)
|
||||
}
|
||||
|
||||
// Display calculates the visual width of a string, excluding ANSI escape sequences,
|
||||
// using the provided runewidth condition. Unlike Width, it does not use caching
|
||||
// and is intended for cases where a specific condition is required.
|
||||
// This function is thread-safe with respect to the provided condition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// cond := runewidth.NewCondition()
|
||||
// width := twdw.Display(cond, "Hello\x1b[31mWorld") // Returns 10
|
||||
func Display(cond *runewidth.Condition, str string) int {
|
||||
return cond.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
|
||||
}
|
||||
|
||||
// Truncate shortens a string to fit within a specified visual width, optionally
|
||||
// appending a suffix (e.g., "..."). It preserves ANSI escape sequences and adds
|
||||
// a reset sequence (\x1b[0m) if needed to prevent formatting bleed. The function
|
||||
// respects the global East Asian width setting and is thread-safe.
|
||||
//
|
||||
// If maxWidth is negative, an empty string is returned. If maxWidth is zero and
|
||||
// a suffix is provided, the suffix is returned. If the string's visual width is
|
||||
// less than or equal to maxWidth, the string (and suffix, if provided and fits)
|
||||
// is returned unchanged.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// s := twdw.Truncate("Hello\x1b[31mWorld", 5, "...") // Returns "Hello..."
|
||||
// s = twdw.Truncate("Hello", 10) // Returns "Hello"
|
||||
func Truncate(s string, maxWidth int, suffix ...string) string {
|
||||
if maxWidth < 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
suffixStr := strings.Join(suffix, "")
|
||||
sDisplayWidth := Width(s) // Uses global cached Width
|
||||
suffixDisplayWidth := Width(suffixStr) // Uses global cached Width
|
||||
|
||||
// Case 1: Original string is visually empty.
|
||||
if sDisplayWidth == 0 {
|
||||
// If suffix is provided and fits within maxWidth (or if maxWidth is generous)
|
||||
if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth {
|
||||
return suffixStr
|
||||
}
|
||||
// If s has ANSI codes (len(s)>0) but maxWidth is 0, can't display them.
|
||||
if maxWidth == 0 && len(s) > 0 {
|
||||
return ""
|
||||
}
|
||||
return s // Returns "" or original ANSI codes
|
||||
}
|
||||
|
||||
// Case 2: maxWidth is 0, but string has content. Cannot display anything.
|
||||
if maxWidth == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Case 3: String fits completely or fits with suffix.
|
||||
// Here, maxWidth is the total budget for the line.
|
||||
if sDisplayWidth <= maxWidth {
|
||||
if len(suffixStr) == 0 { // No suffix.
|
||||
return s
|
||||
}
|
||||
// Suffix is provided. Check if s + suffix fits.
|
||||
if sDisplayWidth+suffixDisplayWidth <= maxWidth {
|
||||
return s + suffixStr
|
||||
}
|
||||
// s fits, but s + suffix is too long. Return s.
|
||||
return s
|
||||
}
|
||||
|
||||
// Case 4: String needs truncation (sDisplayWidth > maxWidth).
|
||||
// maxWidth is the total budget for the final string (content + suffix).
|
||||
|
||||
// Capture the global EastAsianWidth setting once for consistent use
|
||||
mu.Lock()
|
||||
currentGlobalEastAsianWidth := condition.EastAsianWidth
|
||||
mu.Unlock()
|
||||
|
||||
// Special case for EastAsian true: if only suffix fits, return suffix.
|
||||
// This was derived from previous test behavior.
|
||||
if len(suffixStr) > 0 && currentGlobalEastAsianWidth {
|
||||
provisionalContentWidth := maxWidth - suffixDisplayWidth
|
||||
if provisionalContentWidth == 0 { // Exactly enough space for suffix only
|
||||
return suffixStr // <<<< MODIFIED: No ANSI reset here
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the budget for the content part, reserving space for the suffix.
|
||||
targetContentForIteration := maxWidth
|
||||
if len(suffixStr) > 0 {
|
||||
targetContentForIteration -= suffixDisplayWidth
|
||||
}
|
||||
|
||||
// If content budget is negative, means not even suffix fits (or no suffix and no space).
|
||||
// However, if only suffix fits, it should be handled.
|
||||
if targetContentForIteration < 0 {
|
||||
// Can we still fit just the suffix?
|
||||
if len(suffixStr) > 0 && suffixDisplayWidth <= maxWidth {
|
||||
if strings.Contains(s, "\x1b[") {
|
||||
return "\x1b[0m" + suffixStr
|
||||
}
|
||||
return suffixStr
|
||||
}
|
||||
return "" // Cannot fit anything.
|
||||
}
|
||||
// If targetContentForIteration is 0, loop won't run, result will be empty string, then suffix is added.
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
var currentContentDisplayWidth int
|
||||
var ansiSeqBuf bytes.Buffer
|
||||
inAnsiSequence := false
|
||||
ansiWrittenToContent := false
|
||||
|
||||
localRunewidthCond := runewidth.NewCondition()
|
||||
localRunewidthCond.EastAsianWidth = currentGlobalEastAsianWidth
|
||||
|
||||
for _, r := range s {
|
||||
if r == '\x1b' {
|
||||
inAnsiSequence = true
|
||||
ansiSeqBuf.Reset()
|
||||
ansiSeqBuf.WriteRune(r)
|
||||
} else if inAnsiSequence {
|
||||
ansiSeqBuf.WriteRune(r)
|
||||
seqBytes := ansiSeqBuf.Bytes()
|
||||
seqLen := len(seqBytes)
|
||||
terminated := false
|
||||
if seqLen >= 2 {
|
||||
introducer := seqBytes[1]
|
||||
if introducer == '[' {
|
||||
if seqLen >= 3 && r >= 0x40 && r <= 0x7E {
|
||||
terminated = true
|
||||
}
|
||||
} else if introducer == ']' {
|
||||
if r == '\x07' {
|
||||
terminated = true
|
||||
} else if seqLen > 1 && seqBytes[seqLen-2] == '\x1b' && r == '\\' { // Check for ST: \x1b\
|
||||
terminated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if terminated {
|
||||
inAnsiSequence = false
|
||||
contentBuf.Write(ansiSeqBuf.Bytes())
|
||||
ansiWrittenToContent = true
|
||||
ansiSeqBuf.Reset()
|
||||
}
|
||||
} else { // Normal character
|
||||
runeDisplayWidth := localRunewidthCond.RuneWidth(r)
|
||||
if targetContentForIteration == 0 { // No budget for content at all
|
||||
break
|
||||
}
|
||||
if currentContentDisplayWidth+runeDisplayWidth > targetContentForIteration {
|
||||
break
|
||||
}
|
||||
contentBuf.WriteRune(r)
|
||||
currentContentDisplayWidth += runeDisplayWidth
|
||||
}
|
||||
}
|
||||
|
||||
result := contentBuf.String()
|
||||
|
||||
// Suffix is added if:
|
||||
// 1. A suffix string is provided.
|
||||
// 2. Truncation actually happened (sDisplayWidth > maxWidth originally)
|
||||
// OR if the content part is empty but a suffix is meant to be shown
|
||||
// (e.g. targetContentForIteration was 0).
|
||||
if len(suffixStr) > 0 {
|
||||
// Add suffix if we are in the truncation path (sDisplayWidth > maxWidth)
|
||||
// OR if targetContentForIteration was 0 (meaning only suffix should be shown)
|
||||
// but we must ensure we don't exceed original maxWidth.
|
||||
// The logic above for targetContentForIteration already ensures space.
|
||||
|
||||
needsReset := false
|
||||
// Condition for reset: if styling was active in 's' and might affect suffix
|
||||
if (ansiWrittenToContent || (inAnsiSequence && strings.Contains(s, "\x1b["))) && (currentContentDisplayWidth > 0 || ansiWrittenToContent) {
|
||||
if !strings.HasSuffix(result, "\x1b[0m") {
|
||||
needsReset = true
|
||||
}
|
||||
} else if currentContentDisplayWidth > 0 && strings.Contains(result, "\x1b[") && !strings.HasSuffix(result, "\x1b[0m") && strings.Contains(s, "\x1b[") {
|
||||
// If result has content and ANSI, and original had ANSI, and result not already reset
|
||||
needsReset = true
|
||||
}
|
||||
|
||||
if needsReset {
|
||||
result += "\x1b[0m"
|
||||
}
|
||||
result += suffixStr
|
||||
}
|
||||
return result
|
||||
}
|
||||
594
vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go
generated
vendored
Normal file
594
vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go
generated
vendored
Normal file
@@ -0,0 +1,594 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// Blueprint implements a primary table rendering engine with customizable borders and alignments.
|
||||
type Blueprint struct {
|
||||
config tw.Rendition // Rendering configuration for table borders and symbols
|
||||
logger *ll.Logger // Logger for debug trace messages
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewBlueprint creates a new Blueprint instance with optional custom configurations.
|
||||
func NewBlueprint(configs ...tw.Rendition) *Blueprint {
|
||||
// Initialize with default configuration
|
||||
cfg := defaultBlueprint()
|
||||
if len(configs) > 0 {
|
||||
userCfg := configs[0]
|
||||
// Override default borders if provided
|
||||
if userCfg.Borders.Left != 0 {
|
||||
cfg.Borders.Left = userCfg.Borders.Left
|
||||
}
|
||||
if userCfg.Borders.Right != 0 {
|
||||
cfg.Borders.Right = userCfg.Borders.Right
|
||||
}
|
||||
if userCfg.Borders.Top != 0 {
|
||||
cfg.Borders.Top = userCfg.Borders.Top
|
||||
}
|
||||
if userCfg.Borders.Bottom != 0 {
|
||||
cfg.Borders.Bottom = userCfg.Borders.Bottom
|
||||
}
|
||||
// Override symbols if provided
|
||||
if userCfg.Symbols != nil {
|
||||
cfg.Symbols = userCfg.Symbols
|
||||
}
|
||||
|
||||
// Merge user settings with default settings
|
||||
cfg.Settings = mergeSettings(cfg.Settings, userCfg.Settings)
|
||||
}
|
||||
return &Blueprint{config: cfg, logger: ll.New("blueprint")}
|
||||
}
|
||||
|
||||
// Close performs cleanup (no-op in this implementation).
|
||||
func (f *Blueprint) Close() error {
|
||||
f.logger.Debug("Blueprint.Close() called (no-op).")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the renderer's current configuration.
|
||||
func (f *Blueprint) Config() tw.Rendition {
|
||||
return f.config
|
||||
}
|
||||
|
||||
// Footer renders the table footer section with configured formatting.
|
||||
func (f *Blueprint) Footer(footers [][]string, ctx tw.Formatting) {
|
||||
f.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s", ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position)
|
||||
// Render the footer line
|
||||
f.renderLine(ctx)
|
||||
f.logger.Debug("Completed Footer render")
|
||||
}
|
||||
|
||||
// Header renders the table header section with configured formatting.
|
||||
func (f *Blueprint) Header(headers [][]string, ctx tw.Formatting) {
|
||||
f.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v",
|
||||
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(ctx.Row.Current), ctx.Row.Widths)
|
||||
// Render the header line
|
||||
f.renderLine(ctx)
|
||||
f.logger.Debug("Completed Header render")
|
||||
}
|
||||
|
||||
// Line renders a full horizontal row line with junctions and segments.
|
||||
func (f *Blueprint) Line(ctx tw.Formatting) {
|
||||
// Initialize junction renderer
|
||||
jr := NewJunction(JunctionContext{
|
||||
Symbols: f.config.Symbols,
|
||||
Ctx: ctx,
|
||||
ColIdx: 0,
|
||||
Logger: f.logger,
|
||||
BorderTint: Tint{},
|
||||
SeparatorTint: Tint{},
|
||||
})
|
||||
|
||||
var line strings.Builder
|
||||
totalLineWidth := 0 // Track total display width
|
||||
// Get sorted column indices
|
||||
sortedKeys := ctx.Row.Widths.SortedKeys()
|
||||
numCols := 0
|
||||
if len(sortedKeys) > 0 {
|
||||
numCols = sortedKeys[len(sortedKeys)-1] + 1
|
||||
}
|
||||
|
||||
// Handle empty row case
|
||||
if numCols == 0 {
|
||||
prefix := tw.Empty
|
||||
suffix := tw.Empty
|
||||
if f.config.Borders.Left.Enabled() {
|
||||
prefix = jr.RenderLeft()
|
||||
}
|
||||
if f.config.Borders.Right.Enabled() {
|
||||
suffix = jr.RenderRight(-1)
|
||||
}
|
||||
if prefix != tw.Empty || suffix != tw.Empty {
|
||||
line.WriteString(prefix + suffix + tw.NewLine)
|
||||
totalLineWidth = twwidth.Width(prefix) + twwidth.Width(suffix)
|
||||
f.w.Write([]byte(line.String()))
|
||||
}
|
||||
f.logger.Debugf("Line: Handled empty row/widths case (total width %d)", totalLineWidth)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate target total width based on data rows
|
||||
targetTotalWidth := 0
|
||||
for _, colIdx := range sortedKeys {
|
||||
targetTotalWidth += ctx.Row.Widths.Get(colIdx)
|
||||
}
|
||||
if f.config.Borders.Left.Enabled() {
|
||||
targetTotalWidth += twwidth.Width(f.config.Symbols.Column())
|
||||
}
|
||||
if f.config.Borders.Right.Enabled() {
|
||||
targetTotalWidth += twwidth.Width(f.config.Symbols.Column())
|
||||
}
|
||||
if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 {
|
||||
targetTotalWidth += twwidth.Width(f.config.Symbols.Column()) * (len(sortedKeys) - 1)
|
||||
}
|
||||
|
||||
// Add left border if enabled
|
||||
leftBorderWidth := 0
|
||||
if f.config.Borders.Left.Enabled() {
|
||||
leftBorder := jr.RenderLeft()
|
||||
line.WriteString(leftBorder)
|
||||
leftBorderWidth = twwidth.Width(leftBorder)
|
||||
totalLineWidth += leftBorderWidth
|
||||
f.logger.Debugf("Line: Left border='%s' (f.width %d)", leftBorder, leftBorderWidth)
|
||||
}
|
||||
|
||||
visibleColIndices := make([]int, 0)
|
||||
// Calculate visible columns
|
||||
for _, colIdx := range sortedKeys {
|
||||
colWidth := ctx.Row.Widths.Get(colIdx)
|
||||
if colWidth > 0 {
|
||||
visibleColIndices = append(visibleColIndices, colIdx)
|
||||
}
|
||||
}
|
||||
|
||||
f.logger.Debugf("Line: sortedKeys=%v, Widths=%v, visibleColIndices=%v, targetTotalWidth=%d", sortedKeys, ctx.Row.Widths, visibleColIndices, targetTotalWidth)
|
||||
// Render each column segment
|
||||
for keyIndex, currentColIdx := range visibleColIndices {
|
||||
jr.colIdx = currentColIdx
|
||||
segment := jr.GetSegment()
|
||||
colWidth := ctx.Row.Widths.Get(currentColIdx)
|
||||
// Adjust colWidth to account for wider borders
|
||||
adjustedColWidth := colWidth
|
||||
if f.config.Borders.Left.Enabled() && keyIndex == 0 {
|
||||
adjustedColWidth -= leftBorderWidth - twwidth.Width(f.config.Symbols.Column())
|
||||
}
|
||||
if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 {
|
||||
rightBorderWidth := twwidth.Width(jr.RenderRight(currentColIdx))
|
||||
adjustedColWidth -= rightBorderWidth - twwidth.Width(f.config.Symbols.Column())
|
||||
}
|
||||
if adjustedColWidth < 0 {
|
||||
adjustedColWidth = 0
|
||||
}
|
||||
f.logger.Debugf("Line: colIdx=%d, segment='%s', adjusted colWidth=%d", currentColIdx, segment, adjustedColWidth)
|
||||
if segment == tw.Empty {
|
||||
spaces := strings.Repeat(tw.Space, adjustedColWidth)
|
||||
line.WriteString(spaces)
|
||||
totalLineWidth += adjustedColWidth
|
||||
f.logger.Debugf("Line: Rendered spaces='%s' (f.width %d) for col %d", spaces, adjustedColWidth, currentColIdx)
|
||||
} else {
|
||||
segmentWidth := twwidth.Width(segment)
|
||||
if segmentWidth == 0 {
|
||||
segmentWidth = 1 // Avoid division by zero
|
||||
f.logger.Warnf("Line: Segment='%s' has zero width, using 1", segment)
|
||||
}
|
||||
// Calculate how many full segments fit
|
||||
repeat := adjustedColWidth / segmentWidth
|
||||
if repeat < 1 && adjustedColWidth > 0 {
|
||||
repeat = 1
|
||||
}
|
||||
repeatedSegment := strings.Repeat(segment, repeat)
|
||||
actualWidth := twwidth.Width(repeatedSegment)
|
||||
if actualWidth > adjustedColWidth {
|
||||
// Truncate if too long
|
||||
repeatedSegment = twwidth.Truncate(repeatedSegment, adjustedColWidth)
|
||||
actualWidth = twwidth.Width(repeatedSegment)
|
||||
f.logger.Debugf("Line: Truncated segment='%s' to width %d", repeatedSegment, actualWidth)
|
||||
} else if actualWidth < adjustedColWidth {
|
||||
// Pad with segment character to match adjustedColWidth
|
||||
remainingWidth := adjustedColWidth - actualWidth
|
||||
for i := 0; i < remainingWidth/segmentWidth; i++ {
|
||||
repeatedSegment += segment
|
||||
}
|
||||
actualWidth = twwidth.Width(repeatedSegment)
|
||||
if actualWidth < adjustedColWidth {
|
||||
repeatedSegment = tw.PadRight(repeatedSegment, tw.Space, adjustedColWidth)
|
||||
actualWidth = adjustedColWidth
|
||||
f.logger.Debugf("Line: Padded segment with spaces='%s' to width %d", repeatedSegment, actualWidth)
|
||||
}
|
||||
f.logger.Debugf("Line: Padded segment='%s' to width %d", repeatedSegment, actualWidth)
|
||||
}
|
||||
line.WriteString(repeatedSegment)
|
||||
totalLineWidth += actualWidth
|
||||
f.logger.Debugf("Line: Rendered segment='%s' (f.width %d) for col %d", repeatedSegment, actualWidth, currentColIdx)
|
||||
}
|
||||
|
||||
// Add junction between columns if not the last column
|
||||
isLast := keyIndex == len(visibleColIndices)-1
|
||||
if !isLast && f.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
nextColIdx := visibleColIndices[keyIndex+1]
|
||||
junction := jr.RenderJunction(currentColIdx, nextColIdx)
|
||||
// Use center symbol (❀) or column separator (|) to match data rows
|
||||
if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) {
|
||||
junction = f.config.Symbols.Center()
|
||||
if twwidth.Width(junction) != twwidth.Width(f.config.Symbols.Column()) {
|
||||
junction = f.config.Symbols.Column()
|
||||
}
|
||||
}
|
||||
junctionWidth := twwidth.Width(junction)
|
||||
line.WriteString(junction)
|
||||
totalLineWidth += junctionWidth
|
||||
f.logger.Debugf("Line: Junction between %d and %d: '%s' (f.width %d)", currentColIdx, nextColIdx, junction, junctionWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Add right border
|
||||
rightBorderWidth := 0
|
||||
if f.config.Borders.Right.Enabled() && len(visibleColIndices) > 0 {
|
||||
lastIdx := visibleColIndices[len(visibleColIndices)-1]
|
||||
rightBorder := jr.RenderRight(lastIdx)
|
||||
rightBorderWidth = twwidth.Width(rightBorder)
|
||||
line.WriteString(rightBorder)
|
||||
totalLineWidth += rightBorderWidth
|
||||
f.logger.Debugf("Line: Right border='%s' (f.width %d)", rightBorder, rightBorderWidth)
|
||||
}
|
||||
|
||||
// Write the final line
|
||||
line.WriteString(tw.NewLine)
|
||||
f.w.Write([]byte(line.String()))
|
||||
f.logger.Debugf("Line rendered: '%s' (total width %d, target %d)", strings.TrimSuffix(line.String(), tw.NewLine), totalLineWidth, targetTotalWidth)
|
||||
}
|
||||
|
||||
// Logger sets the logger for the Blueprint instance.
|
||||
func (f *Blueprint) Logger(logger *ll.Logger) {
|
||||
f.logger = logger.Namespace("blueprint")
|
||||
}
|
||||
|
||||
// Row renders a table data row with configured formatting.
|
||||
func (f *Blueprint) Row(row []string, ctx tw.Formatting) {
|
||||
f.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v",
|
||||
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter)
|
||||
|
||||
// Render the row line
|
||||
f.renderLine(ctx)
|
||||
f.logger.Debug("Completed Row render")
|
||||
}
|
||||
|
||||
// Start initializes the rendering process (no-op in this implementation).
|
||||
func (f *Blueprint) Start(w io.Writer) error {
|
||||
f.w = w
|
||||
f.logger.Debug("Blueprint.Start() called (no-op).")
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatCell formats a cell's content with specified width, padding, and alignment, returning an empty string if width is non-positive.
|
||||
func (f *Blueprint) formatCell(content string, width int, padding tw.Padding, align tw.Align) string {
|
||||
if width <= 0 {
|
||||
return tw.Empty
|
||||
}
|
||||
|
||||
f.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, padding={L:'%s' R:'%s'}",
|
||||
content, width, align, padding.Left, padding.Right)
|
||||
|
||||
// Calculate display width of content
|
||||
runeWidth := twwidth.Width(content)
|
||||
|
||||
// Set default padding characters
|
||||
leftPadChar := padding.Left
|
||||
rightPadChar := padding.Right
|
||||
|
||||
//if f.config.Settings.Cushion.Enabled() || f.config.Settings.Cushion.Default() {
|
||||
// if leftPadChar == tw.Empty {
|
||||
// leftPadChar = tw.Space
|
||||
// }
|
||||
// if rightPadChar == tw.Empty {
|
||||
// rightPadChar = tw.Space
|
||||
// }
|
||||
//}
|
||||
|
||||
// Calculate padding widths
|
||||
padLeftWidth := twwidth.Width(leftPadChar)
|
||||
padRightWidth := twwidth.Width(rightPadChar)
|
||||
|
||||
// Calculate available width for content
|
||||
availableContentWidth := width - padLeftWidth - padRightWidth
|
||||
if availableContentWidth < 0 {
|
||||
availableContentWidth = 0
|
||||
}
|
||||
f.logger.Debugf("Available content width: %d", availableContentWidth)
|
||||
|
||||
// Truncate content if it exceeds available width
|
||||
if runeWidth > availableContentWidth {
|
||||
content = twwidth.Truncate(content, availableContentWidth)
|
||||
runeWidth = twwidth.Width(content)
|
||||
f.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableContentWidth, content, runeWidth)
|
||||
}
|
||||
|
||||
// Calculate total padding needed
|
||||
totalPaddingWidth := width - runeWidth
|
||||
if totalPaddingWidth < 0 {
|
||||
totalPaddingWidth = 0
|
||||
}
|
||||
f.logger.Debugf("Total padding width: %d", totalPaddingWidth)
|
||||
|
||||
var result strings.Builder
|
||||
var leftPaddingWidth, rightPaddingWidth int
|
||||
|
||||
// Apply alignment and padding
|
||||
switch align {
|
||||
case tw.AlignLeft:
|
||||
result.WriteString(leftPadChar)
|
||||
result.WriteString(content)
|
||||
rightPaddingWidth = totalPaddingWidth - padLeftWidth
|
||||
if rightPaddingWidth > 0 {
|
||||
result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth))
|
||||
f.logger.Debugf("Applied right padding: '%s' for %d width", rightPadChar, rightPaddingWidth)
|
||||
}
|
||||
case tw.AlignRight:
|
||||
leftPaddingWidth = totalPaddingWidth - padRightWidth
|
||||
if leftPaddingWidth > 0 {
|
||||
result.WriteString(tw.PadLeft(tw.Empty, leftPadChar, leftPaddingWidth))
|
||||
f.logger.Debugf("Applied left padding: '%s' for %d width", leftPadChar, leftPaddingWidth)
|
||||
}
|
||||
result.WriteString(content)
|
||||
result.WriteString(rightPadChar)
|
||||
case tw.AlignCenter:
|
||||
leftPaddingWidth = (totalPaddingWidth-padLeftWidth-padRightWidth)/2 + padLeftWidth
|
||||
rightPaddingWidth = totalPaddingWidth - leftPaddingWidth
|
||||
if leftPaddingWidth > padLeftWidth {
|
||||
result.WriteString(tw.PadLeft(tw.Empty, leftPadChar, leftPaddingWidth-padLeftWidth))
|
||||
f.logger.Debugf("Applied left centering padding: '%s' for %d width", leftPadChar, leftPaddingWidth-padLeftWidth)
|
||||
}
|
||||
result.WriteString(leftPadChar)
|
||||
result.WriteString(content)
|
||||
result.WriteString(rightPadChar)
|
||||
if rightPaddingWidth > padRightWidth {
|
||||
result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth-padRightWidth))
|
||||
f.logger.Debugf("Applied right centering padding: '%s' for %d width", rightPadChar, rightPaddingWidth-padRightWidth)
|
||||
}
|
||||
default:
|
||||
// Default to left alignment
|
||||
result.WriteString(leftPadChar)
|
||||
result.WriteString(content)
|
||||
rightPaddingWidth = totalPaddingWidth - padLeftWidth
|
||||
if rightPaddingWidth > 0 {
|
||||
result.WriteString(tw.PadRight(tw.Empty, rightPadChar, rightPaddingWidth))
|
||||
f.logger.Debugf("Applied right padding: '%s' for %d width", rightPadChar, rightPaddingWidth)
|
||||
}
|
||||
}
|
||||
|
||||
output := result.String()
|
||||
finalWidth := twwidth.Width(output)
|
||||
// Adjust output to match target width
|
||||
if finalWidth > width {
|
||||
output = twwidth.Truncate(output, width)
|
||||
f.logger.Debugf("formatCell: Truncated output to width %d", width)
|
||||
} else if finalWidth < width {
|
||||
output = tw.PadRight(output, tw.Space, width)
|
||||
f.logger.Debugf("formatCell: Padded output to meet width %d", width)
|
||||
}
|
||||
|
||||
// Log warning if final width doesn't match target
|
||||
if f.logger.Enabled() && twwidth.Width(output) != width {
|
||||
f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'",
|
||||
twwidth.Width(output), width, output)
|
||||
}
|
||||
|
||||
f.logger.Debugf("Formatted cell final result: '%s' (target width %d)", output, width)
|
||||
return output
|
||||
}
|
||||
|
||||
// renderLine renders a single line (header, row, or footer) with borders, separators, and merge handling.
|
||||
func (f *Blueprint) renderLine(ctx tw.Formatting) {
|
||||
// Get sorted column indices
|
||||
sortedKeys := ctx.Row.Widths.SortedKeys()
|
||||
numCols := 0
|
||||
if len(sortedKeys) > 0 {
|
||||
numCols = sortedKeys[len(sortedKeys)-1] + 1
|
||||
}
|
||||
|
||||
// Set column separator and borders
|
||||
columnSeparator := f.config.Symbols.Column()
|
||||
prefix := tw.Empty
|
||||
if f.config.Borders.Left.Enabled() {
|
||||
prefix = columnSeparator
|
||||
}
|
||||
suffix := tw.Empty
|
||||
if f.config.Borders.Right.Enabled() {
|
||||
suffix = columnSeparator
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
totalLineWidth := 0 // Track total display width
|
||||
if prefix != tw.Empty {
|
||||
output.WriteString(prefix)
|
||||
totalLineWidth += twwidth.Width(prefix)
|
||||
f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, twwidth.Width(prefix))
|
||||
}
|
||||
|
||||
colIndex := 0
|
||||
separatorDisplayWidth := 0
|
||||
if f.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
separatorDisplayWidth = twwidth.Width(columnSeparator)
|
||||
}
|
||||
|
||||
// Process each column
|
||||
for colIndex < numCols {
|
||||
visualWidth := ctx.Row.Widths.Get(colIndex)
|
||||
cellCtx, ok := ctx.Row.Current[colIndex]
|
||||
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
|
||||
if visualWidth == 0 && !isHMergeStart {
|
||||
f.logger.Debugf("renderLine: Skipping col %d (zero width, not HMerge start)", colIndex)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine if a separator is needed
|
||||
shouldAddSeparator := false
|
||||
if colIndex > 0 && f.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
prevWidth := ctx.Row.Widths.Get(colIndex - 1)
|
||||
prevCellCtx, prevOk := ctx.Row.Current[colIndex-1]
|
||||
prevIsHMergeEnd := prevOk && prevCellCtx.Merge.Horizontal.Present && prevCellCtx.Merge.Horizontal.End
|
||||
if (prevWidth > 0 || prevIsHMergeEnd) && (!ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start)) {
|
||||
shouldAddSeparator = true
|
||||
}
|
||||
}
|
||||
if shouldAddSeparator {
|
||||
output.WriteString(columnSeparator)
|
||||
totalLineWidth += separatorDisplayWidth
|
||||
f.logger.Debugf("renderLine: Added separator '%s' before col %d (f.width %d)", columnSeparator, colIndex, separatorDisplayWidth)
|
||||
} else if colIndex > 0 {
|
||||
f.logger.Debugf("renderLine: Skipped separator before col %d due to zero-width prev col or HMerge continuation", colIndex)
|
||||
}
|
||||
|
||||
// Handle merged cells
|
||||
span := 1
|
||||
if isHMergeStart {
|
||||
span = cellCtx.Merge.Horizontal.Span
|
||||
if ctx.Row.Position == tw.Row {
|
||||
dynamicTotalWidth := 0
|
||||
for k := 0; k < span && colIndex+k < numCols; k++ {
|
||||
normWidth := ctx.NormalizedWidths.Get(colIndex + k)
|
||||
if normWidth < 0 {
|
||||
normWidth = 0
|
||||
}
|
||||
dynamicTotalWidth += normWidth
|
||||
if k > 0 && separatorDisplayWidth > 0 && ctx.NormalizedWidths.Get(colIndex+k) > 0 {
|
||||
dynamicTotalWidth += separatorDisplayWidth
|
||||
}
|
||||
}
|
||||
visualWidth = dynamicTotalWidth
|
||||
f.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", colIndex, span, visualWidth)
|
||||
} else {
|
||||
visualWidth = ctx.Row.Widths.Get(colIndex)
|
||||
f.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", colIndex, span, visualWidth)
|
||||
}
|
||||
} else {
|
||||
visualWidth = ctx.Row.Widths.Get(colIndex)
|
||||
f.logger.Debugf("renderLine: Regular col %d, visualWidth %d", colIndex, visualWidth)
|
||||
}
|
||||
if visualWidth < 0 {
|
||||
visualWidth = 0
|
||||
}
|
||||
|
||||
// Skip processing for non-start merged cells
|
||||
if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
|
||||
f.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", colIndex)
|
||||
colIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle empty cell context
|
||||
if !ok {
|
||||
if visualWidth > 0 {
|
||||
spaces := strings.Repeat(tw.Space, visualWidth)
|
||||
output.WriteString(spaces)
|
||||
totalLineWidth += visualWidth
|
||||
f.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces (f.width %d)", colIndex, visualWidth, visualWidth)
|
||||
} else {
|
||||
f.logger.Debugf("renderLine: No cell context for col %d, visualWidth is 0, writing nothing", colIndex)
|
||||
}
|
||||
colIndex += span
|
||||
continue
|
||||
}
|
||||
|
||||
// Set cell padding and alignment
|
||||
padding := cellCtx.Padding
|
||||
align := cellCtx.Align
|
||||
if align == tw.AlignNone {
|
||||
if ctx.Row.Position == tw.Header {
|
||||
align = tw.AlignCenter
|
||||
} else if ctx.Row.Position == tw.Footer {
|
||||
align = tw.AlignRight
|
||||
} else {
|
||||
align = tw.AlignLeft
|
||||
}
|
||||
f.logger.Debugf("renderLine: col %d (data: '%s') using renderer default align '%s' for position %s.", colIndex, cellCtx.Data, align, ctx.Row.Position)
|
||||
} else if align == tw.Skip {
|
||||
if ctx.Row.Position == tw.Header {
|
||||
align = tw.AlignCenter
|
||||
} else if ctx.Row.Position == tw.Footer {
|
||||
align = tw.AlignRight
|
||||
} else {
|
||||
align = tw.AlignLeft
|
||||
}
|
||||
f.logger.Debugf("renderLine: col %d (data: '%s') cellCtx.Align was Skip/empty, falling back to basic default '%s'.", colIndex, cellCtx.Data, align)
|
||||
}
|
||||
|
||||
isTotalPattern := false
|
||||
|
||||
// Case-insensitive check for "total"
|
||||
if isHMergeStart && colIndex > 0 {
|
||||
if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok {
|
||||
if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") {
|
||||
isTotalPattern = true
|
||||
f.logger.Debugf("renderLine: total pattern in row in %d", colIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the alignment from the configuration
|
||||
align = cellCtx.Align
|
||||
|
||||
// Override alignment for footer merged cells
|
||||
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
|
||||
if align == tw.AlignNone {
|
||||
f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align)
|
||||
align = tw.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
// Handle vertical/hierarchical merges
|
||||
cellData := cellCtx.Data
|
||||
if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) ||
|
||||
(cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
|
||||
cellData = tw.Empty
|
||||
f.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", colIndex)
|
||||
}
|
||||
|
||||
// Format and render the cell
|
||||
formattedCell := f.formatCell(cellData, visualWidth, padding, align)
|
||||
if len(formattedCell) > 0 {
|
||||
output.WriteString(formattedCell)
|
||||
cellWidth := twwidth.Width(formattedCell)
|
||||
totalLineWidth += cellWidth
|
||||
f.logger.Debugf("renderLine: Rendered col %d, formattedCell='%s' (f.width %d), totalLineWidth=%d", colIndex, formattedCell, cellWidth, totalLineWidth)
|
||||
}
|
||||
|
||||
// Log rendering details
|
||||
if isHMergeStart {
|
||||
f.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %v): '%s'",
|
||||
colIndex, span, visualWidth, align, formattedCell)
|
||||
} else {
|
||||
f.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %v): '%s'",
|
||||
colIndex, visualWidth, align, formattedCell)
|
||||
}
|
||||
colIndex += span
|
||||
}
|
||||
|
||||
// Add suffix and adjust total width
|
||||
if output.Len() > len(prefix) || f.config.Borders.Right.Enabled() {
|
||||
output.WriteString(suffix)
|
||||
totalLineWidth += twwidth.Width(suffix)
|
||||
f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, twwidth.Width(suffix))
|
||||
}
|
||||
output.WriteString(tw.NewLine)
|
||||
f.w.Write([]byte(output.String()))
|
||||
f.logger.Debugf("renderLine: Final rendered line: '%s' (total width %d)", strings.TrimSuffix(output.String(), tw.NewLine), totalLineWidth)
|
||||
}
|
||||
|
||||
// Rendition updates the Blueprint's configuration.
|
||||
func (f *Blueprint) Rendition(config tw.Rendition) {
|
||||
f.config = mergeRendition(f.config, config)
|
||||
f.logger.Debugf("Blueprint.Rendition updated. New config: %+v", f.config)
|
||||
|
||||
}
|
||||
|
||||
// Ensure Blueprint implements tw.Renditioning
|
||||
var _ tw.Renditioning = (*Blueprint)(nil)
|
||||
719
vendor/github.com/olekukonko/tablewriter/renderer/colorized.go
generated
vendored
Normal file
719
vendor/github.com/olekukonko/tablewriter/renderer/colorized.go
generated
vendored
Normal file
@@ -0,0 +1,719 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/ll/lh"
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// ColorizedConfig holds configuration for the Colorized table renderer.
|
||||
type ColorizedConfig struct {
|
||||
Borders tw.Border // Border visibility settings
|
||||
Settings tw.Settings // Rendering behavior settings (e.g., separators, whitespace)
|
||||
Header Tint // Colors for header cells
|
||||
Column Tint // Colors for row cells
|
||||
Footer Tint // Colors for footer cells
|
||||
Border Tint // Colors for borders and lines
|
||||
Separator Tint // Colors for column separators
|
||||
Symbols tw.Symbols // Symbols for table drawing (e.g., corners, lines)
|
||||
}
|
||||
|
||||
// Colors is a slice of color attributes for use with fatih/color, such as color.FgWhite or color.Bold.
|
||||
type Colors []color.Attribute
|
||||
|
||||
// Tint defines foreground and background color settings for table elements, with optional per-column overrides.
|
||||
type Tint struct {
|
||||
FG Colors // Foreground color attributes
|
||||
BG Colors // Background color attributes
|
||||
Columns []Tint // Per-column color settings
|
||||
}
|
||||
|
||||
// Apply applies the Tint's foreground and background colors to the given text, returning the text unchanged if no colors are set.
|
||||
func (t Tint) Apply(text string) string {
|
||||
if len(t.FG) == 0 && len(t.BG) == 0 {
|
||||
return text
|
||||
}
|
||||
// Combine foreground and background colors
|
||||
combinedColors := append(t.FG, t.BG...)
|
||||
// Create a color function and apply it to the text
|
||||
c := color.New(combinedColors...).SprintFunc()
|
||||
return c(text)
|
||||
}
|
||||
|
||||
// Colorized renders colored ASCII tables with customizable borders, colors, and alignments.
|
||||
type Colorized struct {
|
||||
config ColorizedConfig // Renderer configuration
|
||||
trace []string // Debug trace messages
|
||||
newLine string // Newline character
|
||||
defaultAlign map[tw.Position]tw.Align // Default alignments for header, row, and footer
|
||||
logger *ll.Logger // Logger for debug messages
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewColorized creates a Colorized renderer with the specified configuration, falling back to defaults if none provided.
|
||||
// Only the first config is used if multiple are passed.
|
||||
func NewColorized(configs ...ColorizedConfig) *Colorized {
|
||||
// Initialize with default configuration
|
||||
baseCfg := defaultColorized()
|
||||
|
||||
if len(configs) > 0 {
|
||||
userCfg := configs[0]
|
||||
|
||||
// Override border settings if provided
|
||||
if userCfg.Borders.Left != 0 {
|
||||
baseCfg.Borders.Left = userCfg.Borders.Left
|
||||
}
|
||||
if userCfg.Borders.Right != 0 {
|
||||
baseCfg.Borders.Right = userCfg.Borders.Right
|
||||
}
|
||||
if userCfg.Borders.Top != 0 {
|
||||
baseCfg.Borders.Top = userCfg.Borders.Top
|
||||
}
|
||||
if userCfg.Borders.Bottom != 0 {
|
||||
baseCfg.Borders.Bottom = userCfg.Borders.Bottom
|
||||
}
|
||||
|
||||
// Merge separator and line settings
|
||||
baseCfg.Settings.Separators = mergeSeparators(baseCfg.Settings.Separators, userCfg.Settings.Separators)
|
||||
baseCfg.Settings.Lines = mergeLines(baseCfg.Settings.Lines, userCfg.Settings.Lines)
|
||||
|
||||
// Override compact mode if specified
|
||||
if userCfg.Settings.CompactMode != 0 {
|
||||
baseCfg.Settings.CompactMode = userCfg.Settings.CompactMode
|
||||
}
|
||||
|
||||
// Override color settings for various table elements
|
||||
if len(userCfg.Header.FG) > 0 || len(userCfg.Header.BG) > 0 || userCfg.Header.Columns != nil {
|
||||
baseCfg.Header = userCfg.Header
|
||||
}
|
||||
if len(userCfg.Column.FG) > 0 || len(userCfg.Column.BG) > 0 || userCfg.Column.Columns != nil {
|
||||
baseCfg.Column = userCfg.Column
|
||||
}
|
||||
if len(userCfg.Footer.FG) > 0 || len(userCfg.Footer.BG) > 0 || userCfg.Footer.Columns != nil {
|
||||
baseCfg.Footer = userCfg.Footer
|
||||
}
|
||||
if len(userCfg.Border.FG) > 0 || len(userCfg.Border.BG) > 0 || userCfg.Border.Columns != nil {
|
||||
baseCfg.Border = userCfg.Border
|
||||
}
|
||||
if len(userCfg.Separator.FG) > 0 || len(userCfg.Separator.BG) > 0 || userCfg.Separator.Columns != nil {
|
||||
baseCfg.Separator = userCfg.Separator
|
||||
}
|
||||
|
||||
// Override symbols if provided
|
||||
if userCfg.Symbols != nil {
|
||||
baseCfg.Symbols = userCfg.Symbols
|
||||
}
|
||||
}
|
||||
|
||||
cfg := baseCfg
|
||||
// Ensure symbols are initialized
|
||||
if cfg.Symbols == nil {
|
||||
cfg.Symbols = tw.NewSymbols(tw.StyleLight)
|
||||
}
|
||||
|
||||
// Initialize the Colorized renderer
|
||||
f := &Colorized{
|
||||
config: cfg,
|
||||
newLine: tw.NewLine,
|
||||
defaultAlign: map[tw.Position]tw.Align{
|
||||
tw.Header: tw.AlignCenter,
|
||||
tw.Row: tw.AlignLeft,
|
||||
tw.Footer: tw.AlignRight,
|
||||
},
|
||||
logger: ll.New("colorized", ll.WithHandler(lh.NewMemoryHandler())),
|
||||
}
|
||||
// Log initialization details
|
||||
f.logger.Debugf("Initialized Colorized renderer with symbols: Center=%q, Row=%q, Column=%q", f.config.Symbols.Center(), f.config.Symbols.Row(), f.config.Symbols.Column())
|
||||
f.logger.Debugf("Final ColorizedConfig.Settings.Lines: %+v", f.config.Settings.Lines)
|
||||
f.logger.Debugf("Final ColorizedConfig.Borders: %+v", f.config.Borders)
|
||||
return f
|
||||
}
|
||||
|
||||
// Close performs cleanup (no-op in this implementation).
|
||||
func (c *Colorized) Close() error {
|
||||
c.logger.Debug("Colorized.Close() called (no-op).")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the renderer's configuration as a Rendition.
|
||||
func (c *Colorized) Config() tw.Rendition {
|
||||
return tw.Rendition{
|
||||
Borders: c.config.Borders,
|
||||
Settings: c.config.Settings,
|
||||
Symbols: c.config.Symbols,
|
||||
Streaming: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Debug returns the accumulated debug trace messages.
|
||||
func (c *Colorized) Debug() []string {
|
||||
return c.trace
|
||||
}
|
||||
|
||||
// Footer renders the table footer with configured colors and formatting.
|
||||
func (c *Colorized) Footer(footers [][]string, ctx tw.Formatting) {
|
||||
c.logger.Debugf("Starting Footer render: IsSubRow=%v, Location=%v, Pos=%s",
|
||||
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position)
|
||||
|
||||
// Check if there are footers to render
|
||||
if len(footers) == 0 || len(footers[0]) == 0 {
|
||||
c.logger.Debug("Footer: No footers to render")
|
||||
return
|
||||
}
|
||||
|
||||
// Render the footer line
|
||||
c.renderLine(ctx, footers[0], c.config.Footer)
|
||||
c.logger.Debug("Completed Footer render")
|
||||
}
|
||||
|
||||
// Header renders the table header with configured colors and formatting.
|
||||
func (c *Colorized) Header(headers [][]string, ctx tw.Formatting) {
|
||||
c.logger.Debugf("Starting Header render: IsSubRow=%v, Location=%v, Pos=%s, lines=%d, widths=%v",
|
||||
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, len(headers), ctx.Row.Widths)
|
||||
|
||||
// Check if there are headers to render
|
||||
if len(headers) == 0 || len(headers[0]) == 0 {
|
||||
c.logger.Debug("Header: No headers to render")
|
||||
return
|
||||
}
|
||||
|
||||
// Render the header line
|
||||
c.renderLine(ctx, headers[0], c.config.Header)
|
||||
c.logger.Debug("Completed Header render")
|
||||
}
|
||||
|
||||
// Line renders a horizontal row line with colored junctions and segments, skipping zero-width columns.
|
||||
func (c *Colorized) Line(ctx tw.Formatting) {
|
||||
c.logger.Debugf("Line: Starting with Level=%v, Location=%v, IsSubRow=%v, Widths=%v", ctx.Level, ctx.Row.Location, ctx.IsSubRow, ctx.Row.Widths)
|
||||
|
||||
// Initialize junction renderer
|
||||
jr := NewJunction(JunctionContext{
|
||||
Symbols: c.config.Symbols,
|
||||
Ctx: ctx,
|
||||
ColIdx: 0,
|
||||
BorderTint: c.config.Border,
|
||||
SeparatorTint: c.config.Separator,
|
||||
Logger: c.logger,
|
||||
})
|
||||
|
||||
var line strings.Builder
|
||||
|
||||
// Get sorted column indices and filter out zero-width columns
|
||||
allSortedKeys := ctx.Row.Widths.SortedKeys()
|
||||
effectiveKeys := []int{}
|
||||
keyWidthMap := make(map[int]int)
|
||||
|
||||
for _, k := range allSortedKeys {
|
||||
width := ctx.Row.Widths.Get(k)
|
||||
keyWidthMap[k] = width
|
||||
if width > 0 {
|
||||
effectiveKeys = append(effectiveKeys, k)
|
||||
}
|
||||
}
|
||||
c.logger.Debugf("Line: All keys=%v, Effective keys (width>0)=%v", allSortedKeys, effectiveKeys)
|
||||
|
||||
// Handle case with no effective columns
|
||||
if len(effectiveKeys) == 0 {
|
||||
prefix := tw.Empty
|
||||
suffix := tw.Empty
|
||||
if c.config.Borders.Left.Enabled() {
|
||||
prefix = jr.RenderLeft()
|
||||
}
|
||||
if c.config.Borders.Right.Enabled() {
|
||||
originalLastColIdx := -1
|
||||
if len(allSortedKeys) > 0 {
|
||||
originalLastColIdx = allSortedKeys[len(allSortedKeys)-1]
|
||||
}
|
||||
suffix = jr.RenderRight(originalLastColIdx)
|
||||
}
|
||||
if prefix != tw.Empty || suffix != tw.Empty {
|
||||
line.WriteString(prefix + suffix + tw.NewLine)
|
||||
c.w.Write([]byte(line.String()))
|
||||
}
|
||||
c.logger.Debug("Line: Handled empty row/widths case (no effective keys)")
|
||||
return
|
||||
}
|
||||
|
||||
// Add left border if enabled
|
||||
if c.config.Borders.Left.Enabled() {
|
||||
line.WriteString(jr.RenderLeft())
|
||||
}
|
||||
|
||||
// Render segments for each effective column
|
||||
for keyIndex, currentColIdx := range effectiveKeys {
|
||||
jr.colIdx = currentColIdx
|
||||
segment := jr.GetSegment()
|
||||
colWidth := keyWidthMap[currentColIdx]
|
||||
c.logger.Debugf("Line: Drawing segment for Effective colIdx=%d, segment='%s', width=%d", currentColIdx, segment, colWidth)
|
||||
|
||||
if segment == tw.Empty {
|
||||
line.WriteString(strings.Repeat(tw.Space, colWidth))
|
||||
} else {
|
||||
// Calculate how many times to repeat the segment
|
||||
segmentWidth := twwidth.Width(segment)
|
||||
if segmentWidth <= 0 {
|
||||
segmentWidth = 1
|
||||
}
|
||||
repeat := 0
|
||||
if colWidth > 0 && segmentWidth > 0 {
|
||||
repeat = colWidth / segmentWidth
|
||||
}
|
||||
drawnSegment := strings.Repeat(segment, repeat)
|
||||
line.WriteString(drawnSegment)
|
||||
|
||||
// Adjust for width discrepancies
|
||||
actualDrawnWidth := twwidth.Width(drawnSegment)
|
||||
if actualDrawnWidth < colWidth {
|
||||
missingWidth := colWidth - actualDrawnWidth
|
||||
spaces := strings.Repeat(tw.Space, missingWidth)
|
||||
if len(c.config.Border.BG) > 0 {
|
||||
line.WriteString(Tint{BG: c.config.Border.BG}.Apply(spaces))
|
||||
} else {
|
||||
line.WriteString(spaces)
|
||||
}
|
||||
c.logger.Debugf("Line: colIdx=%d corrected segment width, added %d spaces", currentColIdx, missingWidth)
|
||||
} else if actualDrawnWidth > colWidth {
|
||||
c.logger.Debugf("Line: WARNING colIdx=%d segment draw width %d > target %d", currentColIdx, actualDrawnWidth, colWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Add junction between columns if not the last visible column
|
||||
isLastVisible := keyIndex == len(effectiveKeys)-1
|
||||
if !isLastVisible && c.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
nextVisibleColIdx := effectiveKeys[keyIndex+1]
|
||||
originalPrecedingCol := -1
|
||||
foundCurrent := false
|
||||
for _, k := range allSortedKeys {
|
||||
if k == currentColIdx {
|
||||
foundCurrent = true
|
||||
}
|
||||
if foundCurrent && k < nextVisibleColIdx {
|
||||
originalPrecedingCol = k
|
||||
}
|
||||
if k >= nextVisibleColIdx {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if originalPrecedingCol != -1 {
|
||||
jr.colIdx = originalPrecedingCol
|
||||
junction := jr.RenderJunction(originalPrecedingCol, nextVisibleColIdx)
|
||||
c.logger.Debugf("Line: Junction between visible %d (orig preceding %d) and next visible %d: '%s'", currentColIdx, originalPrecedingCol, nextVisibleColIdx, junction)
|
||||
line.WriteString(junction)
|
||||
} else {
|
||||
c.logger.Debugf("Line: Could not determine original preceding column for junction before visible %d", nextVisibleColIdx)
|
||||
line.WriteString(c.config.Separator.Apply(jr.sym.Center()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add right border if enabled
|
||||
if c.config.Borders.Right.Enabled() {
|
||||
originalLastColIdx := -1
|
||||
if len(allSortedKeys) > 0 {
|
||||
originalLastColIdx = allSortedKeys[len(allSortedKeys)-1]
|
||||
}
|
||||
jr.colIdx = originalLastColIdx
|
||||
line.WriteString(jr.RenderRight(originalLastColIdx))
|
||||
}
|
||||
|
||||
// Write the final line
|
||||
line.WriteString(c.newLine)
|
||||
c.w.Write([]byte(line.String()))
|
||||
c.logger.Debugf("Line rendered: %s", strings.TrimSuffix(line.String(), c.newLine))
|
||||
}
|
||||
|
||||
// Logger sets the logger for the Colorized instance.
|
||||
func (c *Colorized) Logger(logger *ll.Logger) {
|
||||
c.logger = logger.Namespace("colorized")
|
||||
}
|
||||
|
||||
// Reset clears the renderer's internal state, including debug traces.
|
||||
func (c *Colorized) Reset() {
|
||||
c.trace = nil
|
||||
c.logger.Debugf("Reset: Cleared debug trace")
|
||||
}
|
||||
|
||||
// Row renders a table data row with configured colors and formatting.
|
||||
func (c *Colorized) Row(row []string, ctx tw.Formatting) {
|
||||
c.logger.Debugf("Starting Row render: IsSubRow=%v, Location=%v, Pos=%s, hasFooter=%v",
|
||||
ctx.IsSubRow, ctx.Row.Location, ctx.Row.Position, ctx.HasFooter)
|
||||
|
||||
// Check if there is data to render
|
||||
if len(row) == 0 {
|
||||
c.logger.Debugf("Row: No data to render")
|
||||
return
|
||||
}
|
||||
|
||||
// Render the row line
|
||||
c.renderLine(ctx, row, c.config.Column)
|
||||
c.logger.Debugf("Completed Row render")
|
||||
}
|
||||
|
||||
// Start initializes the rendering process (no-op in this implementation).
|
||||
func (c *Colorized) Start(w io.Writer) error {
|
||||
c.w = w
|
||||
c.logger.Debugf("Colorized.Start() called (no-op).")
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatCell formats a cell's content with color, width, padding, and alignment, handling whitespace trimming and truncation.
|
||||
func (c *Colorized) formatCell(content string, width int, padding tw.Padding, align tw.Align, tint Tint) string {
|
||||
c.logger.Debugf("Formatting cell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s', tintFG=%v, tintBG=%v",
|
||||
content, width, align, padding.Left, padding.Right, tint.FG, tint.BG)
|
||||
|
||||
// Return empty string if width is non-positive
|
||||
if width <= 0 {
|
||||
c.logger.Debugf("formatCell: width %d <= 0, returning empty string", width)
|
||||
return tw.Empty
|
||||
}
|
||||
|
||||
// Calculate visual width of content
|
||||
contentVisualWidth := twwidth.Width(content)
|
||||
|
||||
// Set default padding characters
|
||||
padLeftCharStr := padding.Left
|
||||
if padLeftCharStr == tw.Empty {
|
||||
padLeftCharStr = tw.Space
|
||||
}
|
||||
padRightCharStr := padding.Right
|
||||
if padRightCharStr == tw.Empty {
|
||||
padRightCharStr = tw.Space
|
||||
}
|
||||
|
||||
// Calculate padding widths
|
||||
definedPadLeftWidth := twwidth.Width(padLeftCharStr)
|
||||
definedPadRightWidth := twwidth.Width(padRightCharStr)
|
||||
// Calculate available width for content and alignment
|
||||
availableForContentAndAlign := width - definedPadLeftWidth - definedPadRightWidth
|
||||
if availableForContentAndAlign < 0 {
|
||||
availableForContentAndAlign = 0
|
||||
}
|
||||
|
||||
// Truncate content if it exceeds available width
|
||||
if contentVisualWidth > availableForContentAndAlign {
|
||||
content = twwidth.Truncate(content, availableForContentAndAlign)
|
||||
contentVisualWidth = twwidth.Width(content)
|
||||
c.logger.Debugf("Truncated content to fit %d: '%s' (new width %d)", availableForContentAndAlign, content, contentVisualWidth)
|
||||
}
|
||||
|
||||
// Calculate remaining space for alignment
|
||||
remainingSpaceForAlignment := availableForContentAndAlign - contentVisualWidth
|
||||
if remainingSpaceForAlignment < 0 {
|
||||
remainingSpaceForAlignment = 0
|
||||
}
|
||||
|
||||
// Apply alignment padding
|
||||
leftAlignmentPadSpaces := tw.Empty
|
||||
rightAlignmentPadSpaces := tw.Empty
|
||||
switch align {
|
||||
case tw.AlignLeft:
|
||||
rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
|
||||
case tw.AlignRight:
|
||||
leftAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
|
||||
case tw.AlignCenter:
|
||||
leftSpacesCount := remainingSpaceForAlignment / 2
|
||||
rightSpacesCount := remainingSpaceForAlignment - leftSpacesCount
|
||||
leftAlignmentPadSpaces = strings.Repeat(tw.Space, leftSpacesCount)
|
||||
rightAlignmentPadSpaces = strings.Repeat(tw.Space, rightSpacesCount)
|
||||
default:
|
||||
// Default to left alignment
|
||||
rightAlignmentPadSpaces = strings.Repeat(tw.Space, remainingSpaceForAlignment)
|
||||
}
|
||||
|
||||
// Apply colors to content and padding
|
||||
coloredContent := tint.Apply(content)
|
||||
coloredPadLeft := padLeftCharStr
|
||||
coloredPadRight := padRightCharStr
|
||||
coloredAlignPadLeft := leftAlignmentPadSpaces
|
||||
coloredAlignPadRight := rightAlignmentPadSpaces
|
||||
|
||||
if len(tint.BG) > 0 {
|
||||
bgTint := Tint{BG: tint.BG}
|
||||
// Apply foreground color to non-space padding if foreground is defined
|
||||
if len(tint.FG) > 0 && padLeftCharStr != tw.Space {
|
||||
coloredPadLeft = tint.Apply(padLeftCharStr)
|
||||
} else {
|
||||
coloredPadLeft = bgTint.Apply(padLeftCharStr)
|
||||
}
|
||||
if len(tint.FG) > 0 && padRightCharStr != tw.Space {
|
||||
coloredPadRight = tint.Apply(padRightCharStr)
|
||||
} else {
|
||||
coloredPadRight = bgTint.Apply(padRightCharStr)
|
||||
}
|
||||
// Apply background color to alignment padding
|
||||
if leftAlignmentPadSpaces != tw.Empty {
|
||||
coloredAlignPadLeft = bgTint.Apply(leftAlignmentPadSpaces)
|
||||
}
|
||||
if rightAlignmentPadSpaces != tw.Empty {
|
||||
coloredAlignPadRight = bgTint.Apply(rightAlignmentPadSpaces)
|
||||
}
|
||||
} else if len(tint.FG) > 0 {
|
||||
// Apply foreground color to non-space padding
|
||||
if padLeftCharStr != tw.Space {
|
||||
coloredPadLeft = tint.Apply(padLeftCharStr)
|
||||
}
|
||||
if padRightCharStr != tw.Space {
|
||||
coloredPadRight = tint.Apply(padRightCharStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Build final cell string
|
||||
var sb strings.Builder
|
||||
sb.WriteString(coloredPadLeft)
|
||||
sb.WriteString(coloredAlignPadLeft)
|
||||
sb.WriteString(coloredContent)
|
||||
sb.WriteString(coloredAlignPadRight)
|
||||
sb.WriteString(coloredPadRight)
|
||||
output := sb.String()
|
||||
|
||||
// Adjust output width if necessary
|
||||
currentVisualWidth := twwidth.Width(output)
|
||||
if currentVisualWidth != width {
|
||||
c.logger.Debugf("formatCell MISMATCH: content='%s', target_w=%d. Calculated parts width = %d. String: '%s'",
|
||||
content, width, currentVisualWidth, output)
|
||||
if currentVisualWidth > width {
|
||||
output = twwidth.Truncate(output, width)
|
||||
} else {
|
||||
paddingSpacesStr := strings.Repeat(tw.Space, width-currentVisualWidth)
|
||||
if len(tint.BG) > 0 {
|
||||
output += Tint{BG: tint.BG}.Apply(paddingSpacesStr)
|
||||
} else {
|
||||
output += paddingSpacesStr
|
||||
}
|
||||
}
|
||||
c.logger.Debugf("formatCell Post-Correction: Target %d, New Visual width %d. Output: '%s'", width, twwidth.Width(output), output)
|
||||
}
|
||||
|
||||
c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, twwidth.Width(output))
|
||||
return output
|
||||
}
|
||||
|
||||
// renderLine renders a single line (header, row, or footer) with colors, handling merges and separators.
|
||||
func (c *Colorized) renderLine(ctx tw.Formatting, line []string, tint Tint) {
|
||||
// Determine number of columns
|
||||
numCols := 0
|
||||
if len(ctx.Row.Current) > 0 {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Current {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
} else {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Widths {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
|
||||
// Add left border if enabled
|
||||
prefix := tw.Empty
|
||||
if c.config.Borders.Left.Enabled() {
|
||||
prefix = c.config.Border.Apply(c.config.Symbols.Column())
|
||||
}
|
||||
output.WriteString(prefix)
|
||||
|
||||
// Set up separator
|
||||
separatorDisplayWidth := 0
|
||||
separatorString := tw.Empty
|
||||
if c.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
separatorString = c.config.Separator.Apply(c.config.Symbols.Column())
|
||||
separatorDisplayWidth = twwidth.Width(c.config.Symbols.Column())
|
||||
}
|
||||
|
||||
// Process each column
|
||||
for i := 0; i < numCols; {
|
||||
// Determine if a separator is needed
|
||||
shouldAddSeparator := false
|
||||
if i > 0 && c.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
cellCtx, ok := ctx.Row.Current[i]
|
||||
if !ok || !(cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start) {
|
||||
shouldAddSeparator = true
|
||||
}
|
||||
}
|
||||
if shouldAddSeparator {
|
||||
output.WriteString(separatorString)
|
||||
c.logger.Debugf("renderLine: Added separator '%s' before col %d", separatorString, i)
|
||||
} else if i > 0 {
|
||||
c.logger.Debugf("renderLine: Skipped separator before col %d due to HMerge continuation", i)
|
||||
}
|
||||
|
||||
// Get cell context, use default if not present
|
||||
cellCtx, ok := ctx.Row.Current[i]
|
||||
if !ok {
|
||||
cellCtx = tw.CellContext{
|
||||
Data: tw.Empty,
|
||||
Align: c.defaultAlign[ctx.Row.Position],
|
||||
Padding: tw.Padding{Left: tw.Space, Right: tw.Space},
|
||||
Width: ctx.Row.Widths.Get(i),
|
||||
Merge: tw.MergeState{},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle merged cells
|
||||
visualWidth := 0
|
||||
span := 1
|
||||
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
|
||||
|
||||
if isHMergeStart {
|
||||
span = cellCtx.Merge.Horizontal.Span
|
||||
if ctx.Row.Position == tw.Row {
|
||||
// Calculate dynamic width for row merges
|
||||
dynamicTotalWidth := 0
|
||||
for k := 0; k < span && i+k < numCols; k++ {
|
||||
colToSum := i + k
|
||||
normWidth := ctx.NormalizedWidths.Get(colToSum)
|
||||
if normWidth < 0 {
|
||||
normWidth = 0
|
||||
}
|
||||
dynamicTotalWidth += normWidth
|
||||
if k > 0 && separatorDisplayWidth > 0 {
|
||||
dynamicTotalWidth += separatorDisplayWidth
|
||||
}
|
||||
}
|
||||
visualWidth = dynamicTotalWidth
|
||||
c.logger.Debugf("renderLine: Row HMerge col %d, span %d, dynamic visualWidth %d", i, span, visualWidth)
|
||||
} else {
|
||||
visualWidth = ctx.Row.Widths.Get(i)
|
||||
c.logger.Debugf("renderLine: H/F HMerge col %d, span %d, pre-adjusted visualWidth %d", i, span, visualWidth)
|
||||
}
|
||||
} else {
|
||||
visualWidth = ctx.Row.Widths.Get(i)
|
||||
c.logger.Debugf("renderLine: Regular col %d, visualWidth %d", i, visualWidth)
|
||||
}
|
||||
if visualWidth < 0 {
|
||||
visualWidth = 0
|
||||
}
|
||||
|
||||
// Skip processing for non-start merged cells
|
||||
if ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
|
||||
c.logger.Debugf("renderLine: Skipping col %d processing (part of HMerge)", i)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle empty cell context with non-zero width
|
||||
if !ok && visualWidth > 0 {
|
||||
spaces := strings.Repeat(tw.Space, visualWidth)
|
||||
if len(tint.BG) > 0 {
|
||||
output.WriteString(Tint{BG: tint.BG}.Apply(spaces))
|
||||
} else {
|
||||
output.WriteString(spaces)
|
||||
}
|
||||
c.logger.Debugf("renderLine: No cell context for col %d, writing %d spaces", i, visualWidth)
|
||||
i += span
|
||||
continue
|
||||
}
|
||||
|
||||
// Set cell alignment
|
||||
padding := cellCtx.Padding
|
||||
align := cellCtx.Align
|
||||
if align == tw.AlignNone {
|
||||
align = c.defaultAlign[ctx.Row.Position]
|
||||
c.logger.Debugf("renderLine: col %d using default renderer align '%s' for position %s because cellCtx.Align was AlignNone", i, align, ctx.Row.Position)
|
||||
}
|
||||
|
||||
// Detect and handle TOTAL pattern
|
||||
isTotalPattern := false
|
||||
if i == 0 && isHMergeStart && cellCtx.Merge.Horizontal.Span >= 3 && strings.TrimSpace(cellCtx.Data) == "TOTAL" {
|
||||
isTotalPattern = true
|
||||
c.logger.Debugf("renderLine: Detected 'TOTAL' HMerge pattern at col 0")
|
||||
}
|
||||
// Override alignment for footer merges or TOTAL pattern
|
||||
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
|
||||
if align != tw.AlignRight {
|
||||
c.logger.Debugf("renderLine: Applying AlignRight override for Footer HMerge/TOTAL pattern at col %d. Original/default align was: %s", i, align)
|
||||
align = tw.AlignRight
|
||||
}
|
||||
}
|
||||
|
||||
// Handle vertical/hierarchical merges
|
||||
content := cellCtx.Data
|
||||
if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) ||
|
||||
(cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
|
||||
content = tw.Empty
|
||||
c.logger.Debugf("renderLine: Blanked data for col %d (non-start V/Hierarchical)", i)
|
||||
}
|
||||
|
||||
// Apply per-column tint if available
|
||||
cellTint := tint
|
||||
if i < len(tint.Columns) {
|
||||
columnTint := tint.Columns[i]
|
||||
if len(columnTint.FG) > 0 || len(columnTint.BG) > 0 {
|
||||
cellTint = columnTint
|
||||
}
|
||||
}
|
||||
|
||||
// Format and render the cell
|
||||
formattedCell := c.formatCell(content, visualWidth, padding, align, cellTint)
|
||||
if len(formattedCell) > 0 {
|
||||
output.WriteString(formattedCell)
|
||||
} else if visualWidth == 0 && isHMergeStart {
|
||||
c.logger.Debugf("renderLine: Rendered HMerge START col %d resulted in 0 visual width, wrote nothing.", i)
|
||||
} else if visualWidth == 0 {
|
||||
c.logger.Debugf("renderLine: Rendered regular col %d resulted in 0 visual width, wrote nothing.", i)
|
||||
}
|
||||
|
||||
// Log rendering details
|
||||
if isHMergeStart {
|
||||
c.logger.Debugf("renderLine: Rendered HMerge START col %d (span %d, visualWidth %d, align %s): '%s'",
|
||||
i, span, visualWidth, align, formattedCell)
|
||||
} else {
|
||||
c.logger.Debugf("renderLine: Rendered regular col %d (visualWidth %d, align %s): '%s'",
|
||||
i, visualWidth, align, formattedCell)
|
||||
}
|
||||
|
||||
i += span
|
||||
}
|
||||
|
||||
// Add right border if enabled
|
||||
suffix := tw.Empty
|
||||
if c.config.Borders.Right.Enabled() {
|
||||
suffix = c.config.Border.Apply(c.config.Symbols.Column())
|
||||
}
|
||||
output.WriteString(suffix)
|
||||
|
||||
// Write the final line
|
||||
output.WriteString(c.newLine)
|
||||
c.w.Write([]byte(output.String()))
|
||||
c.logger.Debugf("renderLine: Final rendered line: %s", strings.TrimSuffix(output.String(), c.newLine))
|
||||
}
|
||||
|
||||
// Rendition updates the parts of ColorizedConfig that correspond to tw.Rendition
|
||||
// by merging the provided newRendition. Color-specific Tints are not modified.
|
||||
func (c *Colorized) Rendition(newRendition tw.Rendition) { // Method name matches interface
|
||||
c.logger.Debug("Colorized.Rendition called. Current B/Sym/Set: B:%+v, Sym:%T, S:%+v. Override: %+v", c.config.Borders, c.config.Symbols, c.config.Settings, newRendition)
|
||||
|
||||
currentRenditionPart := tw.Rendition{
|
||||
Borders: c.config.Borders,
|
||||
Symbols: c.config.Symbols,
|
||||
Settings: c.config.Settings,
|
||||
}
|
||||
|
||||
mergedRenditionPart := mergeRendition(currentRenditionPart, newRendition)
|
||||
|
||||
c.config.Borders = mergedRenditionPart.Borders
|
||||
c.config.Symbols = mergedRenditionPart.Symbols
|
||||
if c.config.Symbols == nil {
|
||||
c.config.Symbols = tw.NewSymbols(tw.StyleLight)
|
||||
}
|
||||
c.config.Settings = mergedRenditionPart.Settings
|
||||
|
||||
c.logger.Debugf("Colorized.Rendition updated. New B/Sym/Set: B:%+v, Sym:%T, S:%+v",
|
||||
c.config.Borders, c.config.Symbols, c.config.Settings)
|
||||
}
|
||||
|
||||
// Ensure Colorized implements tw.Renditioning
|
||||
var _ tw.Renditioning = (*Colorized)(nil)
|
||||
236
vendor/github.com/olekukonko/tablewriter/renderer/fn.go
generated
vendored
Normal file
236
vendor/github.com/olekukonko/tablewriter/renderer/fn.go
generated
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// defaultBlueprint returns a default Rendition for ASCII table rendering with borders and light symbols.
|
||||
func defaultBlueprint() tw.Rendition {
|
||||
return tw.Rendition{
|
||||
Borders: tw.Border{
|
||||
Left: tw.On,
|
||||
Right: tw.On,
|
||||
Top: tw.On,
|
||||
Bottom: tw.On,
|
||||
},
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{
|
||||
ShowHeader: tw.On,
|
||||
ShowFooter: tw.On,
|
||||
BetweenRows: tw.Off,
|
||||
BetweenColumns: tw.On,
|
||||
},
|
||||
Lines: tw.Lines{
|
||||
ShowTop: tw.On,
|
||||
ShowBottom: tw.On,
|
||||
ShowHeaderLine: tw.On,
|
||||
ShowFooterLine: tw.On,
|
||||
},
|
||||
CompactMode: tw.Off,
|
||||
// Cushion: tw.On,
|
||||
},
|
||||
Symbols: tw.NewSymbols(tw.StyleLight),
|
||||
Streaming: true,
|
||||
}
|
||||
}
|
||||
|
||||
// defaultColorized returns a default ColorizedConfig optimized for dark terminal backgrounds with colored headers, rows, and borders.
|
||||
func defaultColorized() ColorizedConfig {
|
||||
return ColorizedConfig{
|
||||
Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On},
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{
|
||||
ShowHeader: tw.On,
|
||||
ShowFooter: tw.On,
|
||||
BetweenRows: tw.Off,
|
||||
BetweenColumns: tw.On,
|
||||
},
|
||||
Lines: tw.Lines{
|
||||
ShowTop: tw.On,
|
||||
ShowBottom: tw.On,
|
||||
ShowHeaderLine: tw.On,
|
||||
ShowFooterLine: tw.On,
|
||||
},
|
||||
|
||||
CompactMode: tw.Off,
|
||||
},
|
||||
Header: Tint{
|
||||
FG: Colors{color.FgWhite, color.Bold},
|
||||
BG: Colors{color.BgBlack},
|
||||
},
|
||||
Column: Tint{
|
||||
FG: Colors{color.FgCyan},
|
||||
BG: Colors{color.BgBlack},
|
||||
},
|
||||
Footer: Tint{
|
||||
FG: Colors{color.FgYellow},
|
||||
BG: Colors{color.BgBlack},
|
||||
},
|
||||
Border: Tint{
|
||||
FG: Colors{color.FgWhite},
|
||||
BG: Colors{color.BgBlack},
|
||||
},
|
||||
Separator: Tint{
|
||||
FG: Colors{color.FgWhite},
|
||||
BG: Colors{color.BgBlack},
|
||||
},
|
||||
Symbols: tw.NewSymbols(tw.StyleLight),
|
||||
}
|
||||
}
|
||||
|
||||
// defaultOceanRendererConfig returns a base tw.Rendition for the Ocean renderer.
|
||||
func defaultOceanRendererConfig() tw.Rendition {
|
||||
|
||||
return tw.Rendition{
|
||||
Borders: tw.Border{
|
||||
Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On,
|
||||
},
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{
|
||||
ShowHeader: tw.On,
|
||||
ShowFooter: tw.Off,
|
||||
BetweenRows: tw.Off,
|
||||
BetweenColumns: tw.On,
|
||||
},
|
||||
Lines: tw.Lines{
|
||||
ShowTop: tw.On,
|
||||
ShowBottom: tw.On,
|
||||
ShowHeaderLine: tw.On,
|
||||
ShowFooterLine: tw.Off,
|
||||
},
|
||||
|
||||
CompactMode: tw.Off,
|
||||
},
|
||||
Symbols: tw.NewSymbols(tw.StyleDefault),
|
||||
Streaming: true,
|
||||
}
|
||||
}
|
||||
|
||||
// getHTMLStyle remains the same
|
||||
func getHTMLStyle(align tw.Align) string {
|
||||
styleContent := tw.Empty
|
||||
switch align {
|
||||
case tw.AlignRight:
|
||||
styleContent = "text-align: right;"
|
||||
case tw.AlignCenter:
|
||||
styleContent = "text-align: center;"
|
||||
case tw.AlignLeft:
|
||||
styleContent = "text-align: left;"
|
||||
}
|
||||
if styleContent != tw.Empty {
|
||||
return fmt.Sprintf(` style="%s"`, styleContent)
|
||||
}
|
||||
return tw.Empty
|
||||
}
|
||||
|
||||
// mergeLines combines default and override line settings, preserving defaults for unset (zero) overrides.
|
||||
func mergeLines(defaults, overrides tw.Lines) tw.Lines {
|
||||
if overrides.ShowTop != 0 {
|
||||
defaults.ShowTop = overrides.ShowTop
|
||||
}
|
||||
if overrides.ShowBottom != 0 {
|
||||
defaults.ShowBottom = overrides.ShowBottom
|
||||
}
|
||||
if overrides.ShowHeaderLine != 0 {
|
||||
defaults.ShowHeaderLine = overrides.ShowHeaderLine
|
||||
}
|
||||
if overrides.ShowFooterLine != 0 {
|
||||
defaults.ShowFooterLine = overrides.ShowFooterLine
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
// mergeSeparators combines default and override separator settings, preserving defaults for unset (zero) overrides.
|
||||
func mergeSeparators(defaults, overrides tw.Separators) tw.Separators {
|
||||
if overrides.ShowHeader != 0 {
|
||||
defaults.ShowHeader = overrides.ShowHeader
|
||||
}
|
||||
if overrides.ShowFooter != 0 {
|
||||
defaults.ShowFooter = overrides.ShowFooter
|
||||
}
|
||||
if overrides.BetweenRows != 0 {
|
||||
defaults.BetweenRows = overrides.BetweenRows
|
||||
}
|
||||
if overrides.BetweenColumns != 0 {
|
||||
defaults.BetweenColumns = overrides.BetweenColumns
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
// mergeSettings combines default and override settings, preserving defaults for unset (zero) overrides.
|
||||
func mergeSettings(defaults, overrides tw.Settings) tw.Settings {
|
||||
if overrides.Separators.ShowHeader != tw.Unknown {
|
||||
defaults.Separators.ShowHeader = overrides.Separators.ShowHeader
|
||||
}
|
||||
if overrides.Separators.ShowFooter != tw.Unknown {
|
||||
defaults.Separators.ShowFooter = overrides.Separators.ShowFooter
|
||||
}
|
||||
if overrides.Separators.BetweenRows != tw.Unknown {
|
||||
defaults.Separators.BetweenRows = overrides.Separators.BetweenRows
|
||||
}
|
||||
if overrides.Separators.BetweenColumns != tw.Unknown {
|
||||
defaults.Separators.BetweenColumns = overrides.Separators.BetweenColumns
|
||||
}
|
||||
if overrides.Lines.ShowTop != tw.Unknown {
|
||||
defaults.Lines.ShowTop = overrides.Lines.ShowTop
|
||||
}
|
||||
if overrides.Lines.ShowBottom != tw.Unknown {
|
||||
defaults.Lines.ShowBottom = overrides.Lines.ShowBottom
|
||||
}
|
||||
if overrides.Lines.ShowHeaderLine != tw.Unknown {
|
||||
defaults.Lines.ShowHeaderLine = overrides.Lines.ShowHeaderLine
|
||||
}
|
||||
if overrides.Lines.ShowFooterLine != tw.Unknown {
|
||||
defaults.Lines.ShowFooterLine = overrides.Lines.ShowFooterLine
|
||||
}
|
||||
|
||||
if overrides.CompactMode != tw.Unknown {
|
||||
defaults.CompactMode = overrides.CompactMode
|
||||
}
|
||||
|
||||
//if overrides.Cushion != tw.Unknown {
|
||||
// defaults.Cushion = overrides.Cushion
|
||||
//}
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
// MergeRendition merges the 'override' rendition into the 'current' rendition.
|
||||
// It only updates fields in 'current' if they are explicitly set (non-zero/non-nil) in 'override'.
|
||||
// This allows for partial updates to a renderer's configuration.
|
||||
func mergeRendition(current, override tw.Rendition) tw.Rendition {
|
||||
// Merge Borders: Only update if override border states are explicitly set (not 0).
|
||||
// A tw.State's zero value is 0, which is distinct from tw.On (1) or tw.Off (-1).
|
||||
// So, if override.Borders.Left is 0, it means "not specified", so we keep current.
|
||||
if override.Borders.Left != 0 {
|
||||
current.Borders.Left = override.Borders.Left
|
||||
}
|
||||
if override.Borders.Right != 0 {
|
||||
current.Borders.Right = override.Borders.Right
|
||||
}
|
||||
if override.Borders.Top != 0 {
|
||||
current.Borders.Top = override.Borders.Top
|
||||
}
|
||||
if override.Borders.Bottom != 0 {
|
||||
current.Borders.Bottom = override.Borders.Bottom
|
||||
}
|
||||
|
||||
// Merge Symbols: Only update if override.Symbols is not nil.
|
||||
if override.Symbols != nil {
|
||||
current.Symbols = override.Symbols
|
||||
}
|
||||
|
||||
// Merge Settings: Use the existing mergeSettings for granular control.
|
||||
// mergeSettings already handles preserving defaults for unset (zero) overrides.
|
||||
current.Settings = mergeSettings(current.Settings, override.Settings)
|
||||
|
||||
// Streaming flag: typically set at renderer creation, but can be overridden if needed.
|
||||
// For now, let's assume it's not commonly changed post-creation by a generic rendition merge.
|
||||
// If override provides a different streaming capability, it might indicate a fundamental
|
||||
// change that a simple merge shouldn't handle without more context.
|
||||
// current.Streaming = override.Streaming // Or keep current.Streaming
|
||||
|
||||
return current
|
||||
}
|
||||
441
vendor/github.com/olekukonko/tablewriter/renderer/html.go
generated
vendored
Normal file
441
vendor/github.com/olekukonko/tablewriter/renderer/html.go
generated
vendored
Normal file
@@ -0,0 +1,441 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll"
|
||||
"html"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// HTMLConfig defines settings for the HTML table renderer.
|
||||
type HTMLConfig struct {
|
||||
EscapeContent bool // Whether to escape cell content
|
||||
AddLinesTag bool // Whether to wrap multiline content in <lines> tags
|
||||
TableClass string // CSS class for <table>
|
||||
HeaderClass string // CSS class for <thead>
|
||||
BodyClass string // CSS class for <tbody>
|
||||
FooterClass string // CSS class for <tfoot>
|
||||
RowClass string // CSS class for <tr> in body
|
||||
HeaderRowClass string // CSS class for <tr> in header
|
||||
FooterRowClass string // CSS class for <tr> in footer
|
||||
}
|
||||
|
||||
// HTML renders tables in HTML format with customizable classes and content handling.
|
||||
type HTML struct {
|
||||
config HTMLConfig // Renderer configuration
|
||||
w io.Writer // Output w
|
||||
trace []string // Debug trace messages
|
||||
debug bool // Enables debug logging
|
||||
tableStarted bool // Tracks if <table> tag is open
|
||||
tbodyStarted bool // Tracks if <tbody> tag is open
|
||||
tfootStarted bool // Tracks if <tfoot> tag is open
|
||||
vMergeTrack map[int]int // Tracks vertical merge spans by column index
|
||||
logger *ll.Logger
|
||||
}
|
||||
|
||||
// NewHTML initializes an HTML renderer with the given w, debug setting, and optional configuration.
|
||||
// It panics if the w is nil and applies defaults for unset config fields.
|
||||
// Update: see https://github.com/olekukonko/tablewriter/issues/258
|
||||
func NewHTML(configs ...HTMLConfig) *HTML {
|
||||
cfg := HTMLConfig{
|
||||
EscapeContent: true,
|
||||
AddLinesTag: false,
|
||||
}
|
||||
if len(configs) > 0 {
|
||||
userCfg := configs[0]
|
||||
cfg.EscapeContent = userCfg.EscapeContent
|
||||
cfg.AddLinesTag = userCfg.AddLinesTag
|
||||
cfg.TableClass = userCfg.TableClass
|
||||
cfg.HeaderClass = userCfg.HeaderClass
|
||||
cfg.BodyClass = userCfg.BodyClass
|
||||
cfg.FooterClass = userCfg.FooterClass
|
||||
cfg.RowClass = userCfg.RowClass
|
||||
cfg.HeaderRowClass = userCfg.HeaderRowClass
|
||||
cfg.FooterRowClass = userCfg.FooterRowClass
|
||||
}
|
||||
return &HTML{
|
||||
config: cfg,
|
||||
vMergeTrack: make(map[int]int),
|
||||
tableStarted: false,
|
||||
tbodyStarted: false,
|
||||
tfootStarted: false,
|
||||
logger: ll.New("html"),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTML) Logger(logger *ll.Logger) {
|
||||
h.logger = logger
|
||||
}
|
||||
|
||||
// Config returns a Rendition representation of the current configuration.
|
||||
func (h *HTML) Config() tw.Rendition {
|
||||
return tw.Rendition{
|
||||
Borders: tw.BorderNone,
|
||||
Symbols: tw.NewSymbols(tw.StyleNone),
|
||||
Settings: tw.Settings{},
|
||||
Streaming: false,
|
||||
}
|
||||
}
|
||||
|
||||
// debugLog appends a formatted message to the debug trace if debugging is enabled.
|
||||
//func (h *HTML) debugLog(format string, a ...interface{}) {
|
||||
// if h.debug {
|
||||
// msg := fmt.Sprintf(format, a...)
|
||||
// h.trace = append(h.trace, fmt.Sprintf("[HTML] %s", msg))
|
||||
// }
|
||||
//}
|
||||
|
||||
// Debug returns the accumulated debug trace messages.
|
||||
func (h *HTML) Debug() []string {
|
||||
return h.trace
|
||||
}
|
||||
|
||||
// Start begins the HTML table rendering by opening the <table> tag.
|
||||
func (h *HTML) Start(w io.Writer) error {
|
||||
h.w = w
|
||||
h.Reset()
|
||||
h.logger.Debug("HTML.Start() called.")
|
||||
|
||||
classAttr := tw.Empty
|
||||
if h.config.TableClass != tw.Empty {
|
||||
classAttr = fmt.Sprintf(` class="%s"`, h.config.TableClass)
|
||||
}
|
||||
h.logger.Debugf("Writing opening <table%s> tag", classAttr)
|
||||
_, err := fmt.Fprintf(h.w, "<table%s>\n", classAttr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.tableStarted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// closePreviousSection closes any open <tbody> or <tfoot> sections.
|
||||
func (h *HTML) closePreviousSection() {
|
||||
if h.tbodyStarted {
|
||||
h.logger.Debug("Closing <tbody> tag")
|
||||
fmt.Fprintln(h.w, "</tbody>")
|
||||
h.tbodyStarted = false
|
||||
}
|
||||
if h.tfootStarted {
|
||||
h.logger.Debug("Closing <tfoot> tag")
|
||||
fmt.Fprintln(h.w, "</tfoot>")
|
||||
h.tfootStarted = false
|
||||
}
|
||||
}
|
||||
|
||||
// Header renders the <thead> section with header rows, supporting horizontal merges.
|
||||
func (h *HTML) Header(headers [][]string, ctx tw.Formatting) {
|
||||
if !h.tableStarted {
|
||||
h.logger.Debug("WARN: Header called before Start")
|
||||
return
|
||||
}
|
||||
if len(headers) == 0 || len(headers[0]) == 0 {
|
||||
h.logger.Debug("Header: No headers")
|
||||
return
|
||||
}
|
||||
|
||||
h.closePreviousSection()
|
||||
classAttr := tw.Empty
|
||||
if h.config.HeaderClass != tw.Empty {
|
||||
classAttr = fmt.Sprintf(` class="%s"`, h.config.HeaderClass)
|
||||
}
|
||||
fmt.Fprintf(h.w, "<thead%s>\n", classAttr)
|
||||
|
||||
headerRow := headers[0]
|
||||
numCols := 0
|
||||
if len(ctx.Row.Current) > 0 {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Current {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
} else if len(headerRow) > 0 {
|
||||
numCols = len(headerRow)
|
||||
}
|
||||
|
||||
indent := " "
|
||||
rowClassAttr := tw.Empty
|
||||
if h.config.HeaderRowClass != tw.Empty {
|
||||
rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.HeaderRowClass)
|
||||
}
|
||||
fmt.Fprintf(h.w, "%s<tr%s>", indent, rowClassAttr)
|
||||
|
||||
renderedCols := 0
|
||||
for colIdx := 0; renderedCols < numCols && colIdx < numCols; {
|
||||
// Skip columns consumed by vertical merges
|
||||
if remainingSpan, merging := h.vMergeTrack[colIdx]; merging && remainingSpan > 1 {
|
||||
h.logger.Debugf("Header: Skipping col %d due to vmerge", colIdx)
|
||||
h.vMergeTrack[colIdx]--
|
||||
if h.vMergeTrack[colIdx] <= 1 {
|
||||
delete(h.vMergeTrack, colIdx)
|
||||
}
|
||||
colIdx++
|
||||
continue
|
||||
}
|
||||
|
||||
// Render cell
|
||||
cellCtx, ok := ctx.Row.Current[colIdx]
|
||||
if !ok {
|
||||
cellCtx = tw.CellContext{Align: tw.AlignCenter}
|
||||
}
|
||||
originalContent := tw.Empty
|
||||
if colIdx < len(headerRow) {
|
||||
originalContent = headerRow[colIdx]
|
||||
}
|
||||
|
||||
tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, true, colIdx)
|
||||
fmt.Fprintf(h.w, "<%s%s>%s</%s>", tag, attributes, processedContent, tag)
|
||||
renderedCols++
|
||||
|
||||
// Handle horizontal merges
|
||||
hSpan := 1
|
||||
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
|
||||
hSpan = cellCtx.Merge.Horizontal.Span
|
||||
renderedCols += (hSpan - 1)
|
||||
}
|
||||
colIdx += hSpan
|
||||
}
|
||||
fmt.Fprintf(h.w, "</tr>\n")
|
||||
fmt.Fprintln(h.w, "</thead>")
|
||||
}
|
||||
|
||||
// Row renders a <tr> element within <tbody>, supporting horizontal and vertical merges.
|
||||
func (h *HTML) Row(row []string, ctx tw.Formatting) {
|
||||
if !h.tableStarted {
|
||||
h.logger.Debug("WARN: Row called before Start")
|
||||
return
|
||||
}
|
||||
|
||||
if !h.tbodyStarted {
|
||||
h.closePreviousSection()
|
||||
classAttr := tw.Empty
|
||||
if h.config.BodyClass != tw.Empty {
|
||||
classAttr = fmt.Sprintf(` class="%s"`, h.config.BodyClass)
|
||||
}
|
||||
h.logger.Debugf("Writing opening <tbody%s> tag", classAttr)
|
||||
fmt.Fprintf(h.w, "<tbody%s>\n", classAttr)
|
||||
h.tbodyStarted = true
|
||||
}
|
||||
|
||||
h.logger.Debugf("Rendering row data: %v", row)
|
||||
numCols := 0
|
||||
if len(ctx.Row.Current) > 0 {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Current {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
} else if len(row) > 0 {
|
||||
numCols = len(row)
|
||||
}
|
||||
|
||||
indent := " "
|
||||
rowClassAttr := tw.Empty
|
||||
if h.config.RowClass != tw.Empty {
|
||||
rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.RowClass)
|
||||
}
|
||||
fmt.Fprintf(h.w, "%s<tr%s>", indent, rowClassAttr)
|
||||
|
||||
renderedCols := 0
|
||||
for colIdx := 0; renderedCols < numCols && colIdx < numCols; {
|
||||
// Skip columns consumed by vertical merges
|
||||
if remainingSpan, merging := h.vMergeTrack[colIdx]; merging && remainingSpan > 1 {
|
||||
h.logger.Debugf("Row: Skipping render for col %d due to vertical merge (remaining %d)", colIdx, remainingSpan-1)
|
||||
h.vMergeTrack[colIdx]--
|
||||
if h.vMergeTrack[colIdx] <= 1 {
|
||||
delete(h.vMergeTrack, colIdx)
|
||||
}
|
||||
colIdx++
|
||||
continue
|
||||
}
|
||||
|
||||
// Render cell
|
||||
cellCtx, ok := ctx.Row.Current[colIdx]
|
||||
if !ok {
|
||||
cellCtx = tw.CellContext{Align: tw.AlignLeft}
|
||||
}
|
||||
originalContent := tw.Empty
|
||||
if colIdx < len(row) {
|
||||
originalContent = row[colIdx]
|
||||
}
|
||||
|
||||
tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, false, colIdx)
|
||||
fmt.Fprintf(h.w, "<%s%s>%s</%s>", tag, attributes, processedContent, tag)
|
||||
renderedCols++
|
||||
|
||||
// Handle horizontal merges
|
||||
hSpan := 1
|
||||
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
|
||||
hSpan = cellCtx.Merge.Horizontal.Span
|
||||
renderedCols += (hSpan - 1)
|
||||
}
|
||||
colIdx += hSpan
|
||||
}
|
||||
fmt.Fprintf(h.w, "</tr>\n")
|
||||
}
|
||||
|
||||
// Footer renders the <tfoot> section with footer rows, supporting horizontal merges.
|
||||
func (h *HTML) Footer(footers [][]string, ctx tw.Formatting) {
|
||||
if !h.tableStarted {
|
||||
h.logger.Debug("WARN: Footer called before Start")
|
||||
return
|
||||
}
|
||||
if len(footers) == 0 || len(footers[0]) == 0 {
|
||||
h.logger.Debug("Footer: No footers")
|
||||
return
|
||||
}
|
||||
|
||||
h.closePreviousSection()
|
||||
classAttr := tw.Empty
|
||||
if h.config.FooterClass != tw.Empty {
|
||||
classAttr = fmt.Sprintf(` class="%s"`, h.config.FooterClass)
|
||||
}
|
||||
fmt.Fprintf(h.w, "<tfoot%s>\n", classAttr)
|
||||
h.tfootStarted = true
|
||||
|
||||
footerRow := footers[0]
|
||||
numCols := 0
|
||||
if len(ctx.Row.Current) > 0 {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Current {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
} else if len(footerRow) > 0 {
|
||||
numCols = len(footerRow)
|
||||
}
|
||||
|
||||
indent := " "
|
||||
rowClassAttr := tw.Empty
|
||||
if h.config.FooterRowClass != tw.Empty {
|
||||
rowClassAttr = fmt.Sprintf(` class="%s"`, h.config.FooterRowClass)
|
||||
}
|
||||
fmt.Fprintf(h.w, "%s<tr%s>", indent, rowClassAttr)
|
||||
|
||||
renderedCols := 0
|
||||
for colIdx := 0; renderedCols < numCols && colIdx < numCols; {
|
||||
cellCtx, ok := ctx.Row.Current[colIdx]
|
||||
if !ok {
|
||||
cellCtx = tw.CellContext{Align: tw.AlignRight}
|
||||
}
|
||||
originalContent := tw.Empty
|
||||
if colIdx < len(footerRow) {
|
||||
originalContent = footerRow[colIdx]
|
||||
}
|
||||
|
||||
tag, attributes, processedContent := h.renderRowCell(originalContent, cellCtx, false, colIdx)
|
||||
fmt.Fprintf(h.w, "<%s%s>%s</%s>", tag, attributes, processedContent, tag)
|
||||
renderedCols++
|
||||
|
||||
hSpan := 1
|
||||
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
|
||||
hSpan = cellCtx.Merge.Horizontal.Span
|
||||
renderedCols += (hSpan - 1)
|
||||
}
|
||||
colIdx += hSpan
|
||||
}
|
||||
fmt.Fprintf(h.w, "</tr>\n")
|
||||
fmt.Fprintln(h.w, "</tfoot>")
|
||||
h.tfootStarted = false
|
||||
}
|
||||
|
||||
// renderRowCell generates HTML for a single cell, handling content escaping, merges, and alignment.
|
||||
func (h *HTML) renderRowCell(originalContent string, cellCtx tw.CellContext, isHeader bool, colIdx int) (tag, attributes, processedContent string) {
|
||||
tag = "td"
|
||||
if isHeader {
|
||||
tag = "th"
|
||||
}
|
||||
|
||||
// Process content
|
||||
processedContent = originalContent
|
||||
containsNewline := strings.Contains(originalContent, "\n")
|
||||
|
||||
if h.config.EscapeContent {
|
||||
if containsNewline {
|
||||
const newlinePlaceholder = "[[--HTML_RENDERER_BR_PLACEHOLDER--]]"
|
||||
tempContent := strings.ReplaceAll(originalContent, "\n", newlinePlaceholder)
|
||||
escapedContent := html.EscapeString(tempContent)
|
||||
processedContent = strings.ReplaceAll(escapedContent, newlinePlaceholder, "<br>")
|
||||
} else {
|
||||
processedContent = html.EscapeString(originalContent)
|
||||
}
|
||||
} else if containsNewline {
|
||||
processedContent = strings.ReplaceAll(originalContent, "\n", "<br>")
|
||||
}
|
||||
|
||||
if containsNewline && h.config.AddLinesTag {
|
||||
processedContent = "<lines>" + processedContent + "</lines>"
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
var attrBuilder strings.Builder
|
||||
merge := cellCtx.Merge
|
||||
|
||||
if merge.Horizontal.Present && merge.Horizontal.Start && merge.Horizontal.Span > 1 {
|
||||
fmt.Fprintf(&attrBuilder, ` colspan="%d"`, merge.Horizontal.Span)
|
||||
}
|
||||
|
||||
vSpan := 0
|
||||
if !isHeader {
|
||||
if merge.Vertical.Present && merge.Vertical.Start {
|
||||
vSpan = merge.Vertical.Span
|
||||
} else if merge.Hierarchical.Present && merge.Hierarchical.Start {
|
||||
vSpan = merge.Hierarchical.Span
|
||||
}
|
||||
if vSpan > 1 {
|
||||
fmt.Fprintf(&attrBuilder, ` rowspan="%d"`, vSpan)
|
||||
h.vMergeTrack[colIdx] = vSpan
|
||||
h.logger.Debugf("renderRowCell: Tracking rowspan=%d for col %d", vSpan, colIdx)
|
||||
}
|
||||
}
|
||||
|
||||
if style := getHTMLStyle(cellCtx.Align); style != tw.Empty {
|
||||
attrBuilder.WriteString(style)
|
||||
}
|
||||
attributes = attrBuilder.String()
|
||||
return
|
||||
}
|
||||
|
||||
// Line is a no-op for HTML rendering, as structural lines are handled by tags.
|
||||
func (h *HTML) Line(ctx tw.Formatting) {}
|
||||
|
||||
// Reset clears the renderer's internal state, including debug traces and merge tracking.
|
||||
func (h *HTML) Reset() {
|
||||
h.logger.Debug("HTML.Reset() called.")
|
||||
h.tableStarted = false
|
||||
h.tbodyStarted = false
|
||||
h.tfootStarted = false
|
||||
h.vMergeTrack = make(map[int]int)
|
||||
h.trace = nil
|
||||
}
|
||||
|
||||
// Close ensures all open HTML tags (<table>, <tbody>, <tfoot>) are properly closed.
|
||||
func (h *HTML) Close() error {
|
||||
if h.w == nil {
|
||||
return errors.New("HTML Renderer Close called on nil internal w")
|
||||
}
|
||||
|
||||
if h.tableStarted {
|
||||
h.logger.Debug("HTML.Close() called.")
|
||||
h.closePreviousSection()
|
||||
h.logger.Debug("Closing <table> tag.")
|
||||
_, err := fmt.Fprintln(h.w, "</table>")
|
||||
h.tableStarted = false
|
||||
h.tbodyStarted = false
|
||||
h.tfootStarted = false
|
||||
h.vMergeTrack = make(map[int]int)
|
||||
return err
|
||||
}
|
||||
h.logger.Debug("HTML.Close() called, but table was not started (no-op).")
|
||||
return nil
|
||||
}
|
||||
273
vendor/github.com/olekukonko/tablewriter/renderer/junction.go
generated
vendored
Normal file
273
vendor/github.com/olekukonko/tablewriter/renderer/junction.go
generated
vendored
Normal file
@@ -0,0 +1,273 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// Junction handles rendering of table junction points (corners, intersections) with color support.
|
||||
type Junction struct {
|
||||
sym tw.Symbols // Symbols used for rendering junctions and lines
|
||||
ctx tw.Formatting // Current table formatting context
|
||||
colIdx int // Index of the column being processed
|
||||
debugging bool // Enables debug logging
|
||||
borderTint Tint // Colors for border symbols
|
||||
separatorTint Tint // Colors for separator symbols
|
||||
logger *ll.Logger
|
||||
}
|
||||
|
||||
type JunctionContext struct {
|
||||
Symbols tw.Symbols
|
||||
Ctx tw.Formatting
|
||||
ColIdx int
|
||||
Logger *ll.Logger
|
||||
BorderTint Tint
|
||||
SeparatorTint Tint
|
||||
}
|
||||
|
||||
// NewJunction initializes a Junction with the given symbols, context, and tints.
|
||||
// If debug is nil, a no-op debug function is used.
|
||||
func NewJunction(ctx JunctionContext) *Junction {
|
||||
return &Junction{
|
||||
sym: ctx.Symbols,
|
||||
ctx: ctx.Ctx,
|
||||
colIdx: ctx.ColIdx,
|
||||
logger: ctx.Logger.Namespace("junction"),
|
||||
borderTint: ctx.BorderTint,
|
||||
separatorTint: ctx.SeparatorTint,
|
||||
}
|
||||
}
|
||||
|
||||
// getMergeState retrieves the merge state for a specific column in a row, returning an empty state if not found.
|
||||
func (jr *Junction) getMergeState(row map[int]tw.CellContext, colIdx int) tw.MergeState {
|
||||
if row == nil || colIdx < 0 {
|
||||
return tw.MergeState{}
|
||||
}
|
||||
return row[colIdx].Merge
|
||||
}
|
||||
|
||||
// GetSegment determines whether to render a colored horizontal line or an empty space based on merge states.
|
||||
func (jr *Junction) GetSegment() string {
|
||||
currentMerge := jr.getMergeState(jr.ctx.Row.Current, jr.colIdx)
|
||||
nextMerge := jr.getMergeState(jr.ctx.Row.Next, jr.colIdx)
|
||||
|
||||
vPassThruStrict := (currentMerge.Vertical.Present && nextMerge.Vertical.Present && !currentMerge.Vertical.End && !nextMerge.Vertical.Start) ||
|
||||
(currentMerge.Hierarchical.Present && nextMerge.Hierarchical.Present && !currentMerge.Hierarchical.End && !nextMerge.Hierarchical.Start)
|
||||
|
||||
if vPassThruStrict {
|
||||
jr.logger.Debugf("GetSegment col %d: VPassThruStrict=%v -> Empty segment", jr.colIdx, vPassThruStrict)
|
||||
return tw.Empty
|
||||
}
|
||||
symbol := jr.sym.Row()
|
||||
coloredSymbol := jr.borderTint.Apply(symbol)
|
||||
jr.logger.Debugf("GetSegment col %d: VPassThruStrict=%v -> Colored row symbol '%s'", jr.colIdx, vPassThruStrict, coloredSymbol)
|
||||
return coloredSymbol
|
||||
}
|
||||
|
||||
// RenderLeft selects and colors the leftmost junction symbol for the current row line based on position and merges.
|
||||
func (jr *Junction) RenderLeft() string {
|
||||
mergeAbove := jr.getMergeState(jr.ctx.Row.Current, 0)
|
||||
mergeBelow := jr.getMergeState(jr.ctx.Row.Next, 0)
|
||||
|
||||
jr.logger.Debugf("RenderLeft: Level=%v, Location=%v, Previous=%v", jr.ctx.Level, jr.ctx.Row.Location, jr.ctx.Row.Previous)
|
||||
|
||||
isTopBorder := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) ||
|
||||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && jr.ctx.Row.Previous == nil)
|
||||
if isTopBorder {
|
||||
symbol := jr.sym.TopLeft()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
isBottom := jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter
|
||||
isFooter := jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd
|
||||
if isBottom || isFooter {
|
||||
symbol := jr.sym.BottomLeft()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
isVPassThruStrict := (mergeAbove.Vertical.Present && mergeBelow.Vertical.Present && !mergeAbove.Vertical.End && !mergeBelow.Vertical.Start) ||
|
||||
(mergeAbove.Hierarchical.Present && mergeBelow.Hierarchical.Present && !mergeAbove.Hierarchical.End && !mergeBelow.Hierarchical.Start)
|
||||
if isVPassThruStrict {
|
||||
symbol := jr.sym.Column()
|
||||
return jr.separatorTint.Apply(symbol)
|
||||
}
|
||||
|
||||
symbol := jr.sym.MidLeft()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
// RenderRight selects and colors the rightmost junction symbol for the row line based on position, merges, and last column index.
|
||||
func (jr *Junction) RenderRight(lastColIdx int) string {
|
||||
jr.logger.Debugf("RenderRight: lastColIdx=%d, Level=%v, Location=%v, Previous=%v", lastColIdx, jr.ctx.Level, jr.ctx.Row.Location, jr.ctx.Row.Previous)
|
||||
|
||||
if lastColIdx < 0 {
|
||||
switch jr.ctx.Level {
|
||||
case tw.LevelHeader:
|
||||
symbol := jr.sym.TopRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
case tw.LevelFooter:
|
||||
symbol := jr.sym.BottomRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
default:
|
||||
if jr.ctx.Row.Location == tw.LocationFirst {
|
||||
symbol := jr.sym.TopRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if jr.ctx.Row.Location == tw.LocationEnd {
|
||||
symbol := jr.sym.BottomRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
symbol := jr.sym.MidRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
mergeAbove := jr.getMergeState(jr.ctx.Row.Current, lastColIdx)
|
||||
mergeBelow := jr.getMergeState(jr.ctx.Row.Next, lastColIdx)
|
||||
|
||||
isTopBorder := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) ||
|
||||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && jr.ctx.Row.Previous == nil)
|
||||
if isTopBorder {
|
||||
symbol := jr.sym.TopRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
isBottom := jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter
|
||||
isFooter := jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd
|
||||
if isBottom || isFooter {
|
||||
symbol := jr.sym.BottomRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
isVPassThruStrict := (mergeAbove.Vertical.Present && mergeBelow.Vertical.Present && !mergeAbove.Vertical.End && !mergeBelow.Vertical.Start) ||
|
||||
(mergeAbove.Hierarchical.Present && mergeBelow.Hierarchical.Present && !mergeAbove.Hierarchical.End && !mergeBelow.Hierarchical.Start)
|
||||
if isVPassThruStrict {
|
||||
symbol := jr.sym.Column()
|
||||
return jr.separatorTint.Apply(symbol)
|
||||
}
|
||||
|
||||
symbol := jr.sym.MidRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
// RenderJunction selects and colors the junction symbol between two adjacent columns based on merge states and table position.
|
||||
func (jr *Junction) RenderJunction(leftColIdx, rightColIdx int) string {
|
||||
mergeCurrentL := jr.getMergeState(jr.ctx.Row.Current, leftColIdx)
|
||||
mergeCurrentR := jr.getMergeState(jr.ctx.Row.Current, rightColIdx)
|
||||
mergeNextL := jr.getMergeState(jr.ctx.Row.Next, leftColIdx)
|
||||
mergeNextR := jr.getMergeState(jr.ctx.Row.Next, rightColIdx)
|
||||
|
||||
isSpannedCurrent := mergeCurrentL.Horizontal.Present && !mergeCurrentL.Horizontal.End
|
||||
isSpannedNext := mergeNextL.Horizontal.Present && !mergeNextL.Horizontal.End
|
||||
|
||||
vPassThruLStrict := (mergeCurrentL.Vertical.Present && mergeNextL.Vertical.Present && !mergeCurrentL.Vertical.End && !mergeNextL.Vertical.Start) ||
|
||||
(mergeCurrentL.Hierarchical.Present && mergeNextL.Hierarchical.Present && !mergeCurrentL.Hierarchical.End && !mergeNextL.Hierarchical.Start)
|
||||
vPassThruRStrict := (mergeCurrentR.Vertical.Present && mergeNextR.Vertical.Present && !mergeCurrentR.Vertical.End && !mergeNextR.Vertical.Start) ||
|
||||
(mergeCurrentR.Hierarchical.Present && mergeNextR.Hierarchical.Present && !mergeCurrentR.Hierarchical.End && !mergeNextR.Hierarchical.Start)
|
||||
|
||||
isTop := (jr.ctx.Level == tw.LevelHeader && jr.ctx.Row.Location == tw.LocationFirst) ||
|
||||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationFirst && len(jr.ctx.Row.Previous) == 0)
|
||||
isBottom := (jr.ctx.Level == tw.LevelFooter && jr.ctx.Row.Location == tw.LocationEnd) ||
|
||||
(jr.ctx.Level == tw.LevelBody && jr.ctx.Row.Location == tw.LocationEnd && !jr.ctx.HasFooter)
|
||||
isPreFooter := jr.ctx.Level == tw.LevelFooter && (jr.ctx.Row.Position == tw.Row || jr.ctx.Row.Position == tw.Header)
|
||||
|
||||
if isTop {
|
||||
if isSpannedNext {
|
||||
symbol := jr.sym.Row()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
symbol := jr.sym.TopMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
if isBottom {
|
||||
if vPassThruLStrict && vPassThruRStrict {
|
||||
symbol := jr.sym.Column()
|
||||
return jr.separatorTint.Apply(symbol)
|
||||
}
|
||||
if vPassThruLStrict {
|
||||
symbol := jr.sym.MidLeft()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if vPassThruRStrict {
|
||||
symbol := jr.sym.MidRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if isSpannedCurrent {
|
||||
symbol := jr.sym.Row()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
symbol := jr.sym.BottomMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
if isPreFooter {
|
||||
if vPassThruLStrict && vPassThruRStrict {
|
||||
symbol := jr.sym.Column()
|
||||
return jr.separatorTint.Apply(symbol)
|
||||
}
|
||||
if vPassThruLStrict {
|
||||
symbol := jr.sym.MidLeft()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if vPassThruRStrict {
|
||||
symbol := jr.sym.MidRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if mergeCurrentL.Horizontal.Present {
|
||||
if !mergeCurrentL.Horizontal.End && mergeCurrentR.Horizontal.Present && !mergeCurrentR.Horizontal.End {
|
||||
jr.logger.Debugf("Footer separator: H-merge continues from col %d to %d (mid-span), using BottomMid", leftColIdx, rightColIdx)
|
||||
symbol := jr.sym.BottomMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if !mergeCurrentL.Horizontal.End && mergeCurrentR.Horizontal.Present && mergeCurrentR.Horizontal.End {
|
||||
jr.logger.Debugf("Footer separator: H-merge ends at col %d, using BottomMid", rightColIdx)
|
||||
symbol := jr.sym.BottomMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if mergeCurrentL.Horizontal.End && !mergeCurrentR.Horizontal.Present {
|
||||
jr.logger.Debugf("Footer separator: H-merge ends at col %d, next col %d not merged, using Center", leftColIdx, rightColIdx)
|
||||
symbol := jr.sym.Center()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
}
|
||||
if isSpannedNext {
|
||||
symbol := jr.sym.BottomMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if isSpannedCurrent {
|
||||
symbol := jr.sym.TopMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
symbol := jr.sym.Center()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
if vPassThruLStrict && vPassThruRStrict {
|
||||
symbol := jr.sym.Column()
|
||||
return jr.separatorTint.Apply(symbol)
|
||||
}
|
||||
if vPassThruLStrict {
|
||||
symbol := jr.sym.MidLeft()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if vPassThruRStrict {
|
||||
symbol := jr.sym.MidRight()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if isSpannedCurrent && isSpannedNext {
|
||||
symbol := jr.sym.Row()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if isSpannedCurrent {
|
||||
symbol := jr.sym.TopMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
if isSpannedNext {
|
||||
symbol := jr.sym.BottomMid()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
|
||||
symbol := jr.sym.Center()
|
||||
return jr.borderTint.Apply(symbol)
|
||||
}
|
||||
419
vendor/github.com/olekukonko/tablewriter/renderer/markdown.go
generated
vendored
Normal file
419
vendor/github.com/olekukonko/tablewriter/renderer/markdown.go
generated
vendored
Normal file
@@ -0,0 +1,419 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// Markdown renders tables in Markdown format with customizable settings.
|
||||
type Markdown struct {
|
||||
config tw.Rendition // Rendering configuration
|
||||
logger *ll.Logger // Debug trace messages
|
||||
alignment tw.Alignment // alias of []tw.Align
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// NewMarkdown initializes a Markdown renderer with defaults tailored for Markdown (e.g., pipes, header separator).
|
||||
// Only the first config is used if multiple are provided.
|
||||
func NewMarkdown(configs ...tw.Rendition) *Markdown {
|
||||
cfg := defaultBlueprint()
|
||||
// Configure Markdown-specific defaults
|
||||
cfg.Symbols = tw.NewSymbols(tw.StyleMarkdown)
|
||||
cfg.Borders = tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off}
|
||||
cfg.Settings.Separators.BetweenColumns = tw.On
|
||||
cfg.Settings.Separators.BetweenRows = tw.Off
|
||||
cfg.Settings.Lines.ShowHeaderLine = tw.On
|
||||
cfg.Settings.Lines.ShowTop = tw.Off
|
||||
cfg.Settings.Lines.ShowBottom = tw.Off
|
||||
cfg.Settings.Lines.ShowFooterLine = tw.Off
|
||||
// cfg.Settings.TrimWhitespace = tw.On
|
||||
|
||||
// Apply user overrides
|
||||
if len(configs) > 0 {
|
||||
cfg = mergeMarkdownConfig(cfg, configs[0])
|
||||
}
|
||||
return &Markdown{config: cfg, logger: ll.New("markdown")}
|
||||
}
|
||||
|
||||
// mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings.
|
||||
func mergeMarkdownConfig(defaults, overrides tw.Rendition) tw.Rendition {
|
||||
if overrides.Borders.Left != 0 {
|
||||
defaults.Borders.Left = overrides.Borders.Left
|
||||
}
|
||||
if overrides.Borders.Right != 0 {
|
||||
defaults.Borders.Right = overrides.Borders.Right
|
||||
}
|
||||
if overrides.Symbols != nil {
|
||||
defaults.Symbols = overrides.Symbols
|
||||
}
|
||||
defaults.Settings = mergeSettings(defaults.Settings, overrides.Settings)
|
||||
// Enforce Markdown requirements
|
||||
defaults.Settings.Lines.ShowHeaderLine = tw.On
|
||||
defaults.Settings.Separators.BetweenColumns = tw.On
|
||||
// defaults.Settings.TrimWhitespace = tw.On
|
||||
return defaults
|
||||
}
|
||||
|
||||
func (m *Markdown) Logger(logger *ll.Logger) {
|
||||
m.logger = logger.Namespace("markdown")
|
||||
}
|
||||
|
||||
// Config returns the renderer's current configuration.
|
||||
func (m *Markdown) Config() tw.Rendition {
|
||||
return m.config
|
||||
}
|
||||
|
||||
// Header renders the Markdown table header and its separator line.
|
||||
func (m *Markdown) Header(headers [][]string, ctx tw.Formatting) {
|
||||
m.resolveAlignment(ctx)
|
||||
if len(headers) == 0 || len(headers[0]) == 0 {
|
||||
m.logger.Debug("Header: No headers to render")
|
||||
return
|
||||
}
|
||||
m.logger.Debugf("Rendering header with %d lines, widths=%v, current=%v, next=%v", len(headers), ctx.Row.Widths, ctx.Row.Current, ctx.Row.Next)
|
||||
|
||||
// Render header content
|
||||
m.renderMarkdownLine(headers[0], ctx, false)
|
||||
|
||||
// Render separator if enabled
|
||||
if m.config.Settings.Lines.ShowHeaderLine.Enabled() {
|
||||
sepCtx := ctx
|
||||
sepCtx.Row.Widths = ctx.Row.Widths
|
||||
sepCtx.Row.Current = ctx.Row.Current
|
||||
sepCtx.Row.Previous = ctx.Row.Current
|
||||
sepCtx.IsSubRow = true
|
||||
m.renderMarkdownLine(nil, sepCtx, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Row renders a Markdown table data row.
|
||||
func (m *Markdown) Row(row []string, ctx tw.Formatting) {
|
||||
m.resolveAlignment(ctx)
|
||||
m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
|
||||
m.renderMarkdownLine(row, ctx, false)
|
||||
|
||||
}
|
||||
|
||||
// Footer renders the Markdown table footer.
|
||||
func (m *Markdown) Footer(footers [][]string, ctx tw.Formatting) {
|
||||
m.resolveAlignment(ctx)
|
||||
if len(footers) == 0 || len(footers[0]) == 0 {
|
||||
m.logger.Debug("Footer: No footers to render")
|
||||
return
|
||||
}
|
||||
m.logger.Debugf("Rendering footer with %d lines, widths=%v, previous=%v, current=%v, next=%v",
|
||||
len(footers), ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
|
||||
m.renderMarkdownLine(footers[0], ctx, false)
|
||||
}
|
||||
|
||||
// Line is a no-op for Markdown, as only the header separator is rendered (handled by Header).
|
||||
func (m *Markdown) Line(ctx tw.Formatting) {
|
||||
m.logger.Debugf("Line: Generic Line call received (pos: %s, loc: %s). Markdown ignores these.", ctx.Row.Position, ctx.Row.Location)
|
||||
}
|
||||
|
||||
// Reset clears the renderer's internal state, including debug traces.
|
||||
func (m *Markdown) Reset() {
|
||||
m.logger.Info("Reset: Cleared debug trace")
|
||||
}
|
||||
|
||||
func (m *Markdown) Start(w io.Writer) error {
|
||||
m.w = w
|
||||
m.logger.Warn("Markdown.Start() called (no-op).")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Markdown) Close() error {
|
||||
m.logger.Warn("Markdown.Close() called (no-op).")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
|
||||
if len(m.alignment) != 0 {
|
||||
return m.alignment
|
||||
}
|
||||
|
||||
// get total columns
|
||||
total := len(ctx.Row.Current)
|
||||
|
||||
// build default alignment
|
||||
for i := 0; i < total; i++ {
|
||||
m.alignment = append(m.alignment, tw.AlignNone) // Default to AlignNone
|
||||
}
|
||||
|
||||
// add per column alignment if it exists
|
||||
for i := 0; i < total; i++ {
|
||||
m.alignment[i] = ctx.Row.Current[i].Align
|
||||
}
|
||||
|
||||
m.logger.Debugf(" → Align Resolved %s", m.alignment)
|
||||
return m.alignment
|
||||
}
|
||||
|
||||
// formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide.
|
||||
func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string {
|
||||
//if m.config.Settings.TrimWhitespace.Enabled() {
|
||||
// content = strings.TrimSpace(content)
|
||||
//}
|
||||
contentVisualWidth := twwidth.Width(content)
|
||||
|
||||
// Use specified padding characters or default to spaces
|
||||
padLeftChar := padding.Left
|
||||
if padLeftChar == tw.Empty {
|
||||
padLeftChar = tw.Space
|
||||
}
|
||||
padRightChar := padding.Right
|
||||
if padRightChar == tw.Empty {
|
||||
padRightChar = tw.Space
|
||||
}
|
||||
|
||||
// Calculate padding widths
|
||||
padLeftCharWidth := twwidth.Width(padLeftChar)
|
||||
padRightCharWidth := twwidth.Width(padRightChar)
|
||||
minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth)
|
||||
targetWidth := tw.Max(width, minWidth)
|
||||
|
||||
// Calculate padding
|
||||
totalPaddingNeeded := targetWidth - contentVisualWidth
|
||||
if totalPaddingNeeded < 0 {
|
||||
totalPaddingNeeded = 0
|
||||
}
|
||||
|
||||
var leftPadStr, rightPadStr string
|
||||
switch align {
|
||||
case tw.AlignRight:
|
||||
leftPadCount := tw.Max(0, totalPaddingNeeded-padRightCharWidth)
|
||||
rightPadCount := totalPaddingNeeded - leftPadCount
|
||||
leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
|
||||
rightPadStr = strings.Repeat(padRightChar, rightPadCount)
|
||||
case tw.AlignCenter:
|
||||
leftPadCount := totalPaddingNeeded / 2
|
||||
rightPadCount := totalPaddingNeeded - leftPadCount
|
||||
if leftPadCount < padLeftCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth {
|
||||
leftPadCount = padLeftCharWidth
|
||||
rightPadCount = totalPaddingNeeded - leftPadCount
|
||||
}
|
||||
if rightPadCount < padRightCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth {
|
||||
rightPadCount = padRightCharWidth
|
||||
leftPadCount = totalPaddingNeeded - rightPadCount
|
||||
}
|
||||
leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
|
||||
rightPadStr = strings.Repeat(padRightChar, rightPadCount)
|
||||
default: // AlignLeft
|
||||
rightPadCount := tw.Max(0, totalPaddingNeeded-padLeftCharWidth)
|
||||
leftPadCount := totalPaddingNeeded - rightPadCount
|
||||
leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
|
||||
rightPadStr = strings.Repeat(padRightChar, rightPadCount)
|
||||
}
|
||||
|
||||
// Build result
|
||||
result := leftPadStr + content + rightPadStr
|
||||
|
||||
// Adjust width if needed
|
||||
finalWidth := twwidth.Width(result)
|
||||
if finalWidth != targetWidth {
|
||||
m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d",
|
||||
content, targetWidth, padding.Left, padding.Right, align, result, finalWidth)
|
||||
adjNeeded := targetWidth - finalWidth
|
||||
if adjNeeded > 0 {
|
||||
adjStr := strings.Repeat(tw.Space, adjNeeded)
|
||||
if align == tw.AlignRight {
|
||||
result = adjStr + result
|
||||
} else if align == tw.AlignCenter {
|
||||
leftAdj := adjNeeded / 2
|
||||
rightAdj := adjNeeded - leftAdj
|
||||
result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj)
|
||||
} else {
|
||||
result += adjStr
|
||||
}
|
||||
} else {
|
||||
result = twwidth.Truncate(result, targetWidth)
|
||||
}
|
||||
m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, twwidth.Width(result))
|
||||
}
|
||||
|
||||
m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)",
|
||||
content, width, align, padding.Left, padding.Right, result, targetWidth)
|
||||
return result
|
||||
}
|
||||
|
||||
// formatSeparator generates a Markdown separator (e.g., `---`, `:--`, `:-:`) with alignment indicators.
|
||||
func (m *Markdown) formatSeparator(width int, align tw.Align) string {
|
||||
targetWidth := tw.Max(3, width)
|
||||
var sb strings.Builder
|
||||
|
||||
switch align {
|
||||
case tw.AlignLeft:
|
||||
sb.WriteRune(':')
|
||||
sb.WriteString(strings.Repeat("-", targetWidth-1))
|
||||
case tw.AlignRight:
|
||||
sb.WriteString(strings.Repeat("-", targetWidth-1))
|
||||
sb.WriteRune(':')
|
||||
case tw.AlignCenter:
|
||||
sb.WriteRune(':')
|
||||
sb.WriteString(strings.Repeat("-", targetWidth-2))
|
||||
sb.WriteRune(':')
|
||||
case tw.AlignNone:
|
||||
sb.WriteString(strings.Repeat("-", targetWidth))
|
||||
default:
|
||||
sb.WriteString(strings.Repeat("-", targetWidth)) // Fallback
|
||||
}
|
||||
|
||||
result := sb.String()
|
||||
currentLen := twwidth.Width(result)
|
||||
if currentLen < targetWidth {
|
||||
result += strings.Repeat("-", targetWidth-currentLen)
|
||||
} else if currentLen > targetWidth {
|
||||
result = twwidth.Truncate(result, targetWidth)
|
||||
}
|
||||
|
||||
m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result)
|
||||
return result
|
||||
}
|
||||
|
||||
// renderMarkdownLine renders a single Markdown line (header, row, footer, or separator) with pipes and alignment.
|
||||
func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeaderSep bool) {
|
||||
numCols := 0
|
||||
if len(ctx.Row.Widths) > 0 {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Widths {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
} else if len(ctx.Row.Current) > 0 {
|
||||
maxKey := -1
|
||||
for k := range ctx.Row.Current {
|
||||
if k > maxKey {
|
||||
maxKey = k
|
||||
}
|
||||
}
|
||||
numCols = maxKey + 1
|
||||
} else if len(line) > 0 && !isHeaderSep {
|
||||
numCols = len(line)
|
||||
}
|
||||
|
||||
if numCols == 0 && !isHeaderSep {
|
||||
m.logger.Debug("renderMarkdownLine: Skipping line with zero columns.")
|
||||
return
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
prefix := m.config.Symbols.Column()
|
||||
if m.config.Borders.Left == tw.Off {
|
||||
prefix = tw.Empty
|
||||
}
|
||||
suffix := m.config.Symbols.Column()
|
||||
if m.config.Borders.Right == tw.Off {
|
||||
suffix = tw.Empty
|
||||
}
|
||||
separator := m.config.Symbols.Column()
|
||||
output.WriteString(prefix)
|
||||
|
||||
colIndex := 0
|
||||
separatorWidth := twwidth.Width(separator)
|
||||
|
||||
for colIndex < numCols {
|
||||
cellCtx, ok := ctx.Row.Current[colIndex]
|
||||
align := m.alignment[colIndex]
|
||||
|
||||
defaultPadding := tw.Padding{Left: tw.Space, Right: tw.Space}
|
||||
if !ok {
|
||||
cellCtx = tw.CellContext{
|
||||
Data: tw.Empty, Align: align, Padding: defaultPadding,
|
||||
Width: ctx.Row.Widths.Get(colIndex), Merge: tw.MergeState{},
|
||||
}
|
||||
} else if !cellCtx.Padding.Paddable() {
|
||||
cellCtx.Padding = defaultPadding
|
||||
}
|
||||
|
||||
// Add separator
|
||||
isContinuation := ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start
|
||||
if colIndex > 0 && !isContinuation {
|
||||
output.WriteString(separator)
|
||||
m.logger.Debugf("renderMarkdownLine: Added separator '%s' before col %d", separator, colIndex)
|
||||
}
|
||||
|
||||
// Calculate width and span
|
||||
span := 1
|
||||
visualWidth := 0
|
||||
isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
|
||||
if isHMergeStart {
|
||||
span = cellCtx.Merge.Horizontal.Span
|
||||
totalWidth := 0
|
||||
for k := 0; k < span && colIndex+k < numCols; k++ {
|
||||
colWidth := ctx.NormalizedWidths.Get(colIndex + k)
|
||||
if colWidth < 0 {
|
||||
colWidth = 0
|
||||
}
|
||||
totalWidth += colWidth
|
||||
if k > 0 && separatorWidth > 0 {
|
||||
totalWidth += separatorWidth
|
||||
}
|
||||
}
|
||||
visualWidth = totalWidth
|
||||
m.logger.Debugf("renderMarkdownLine: HMerge col %d, span %d, visualWidth %d", colIndex, span, visualWidth)
|
||||
} else {
|
||||
visualWidth = ctx.Row.Widths.Get(colIndex)
|
||||
m.logger.Debugf("renderMarkdownLine: Regular col %d, visualWidth %d", colIndex, visualWidth)
|
||||
}
|
||||
if visualWidth < 0 {
|
||||
visualWidth = 0
|
||||
}
|
||||
|
||||
// Render segment
|
||||
if isContinuation {
|
||||
m.logger.Debugf("renderMarkdownLine: Skipping col %d (HMerge continuation)", colIndex)
|
||||
} else {
|
||||
var formattedSegment string
|
||||
if isHeaderSep {
|
||||
// Use header's alignment from ctx.Row.Previous
|
||||
headerAlign := align
|
||||
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK {
|
||||
headerAlign = headerCellCtx.Align
|
||||
// Preserve tw.AlignNone for separator
|
||||
if headerAlign != tw.AlignNone && (headerAlign == tw.Empty || headerAlign == tw.Skip) {
|
||||
headerAlign = tw.AlignCenter
|
||||
}
|
||||
}
|
||||
formattedSegment = m.formatSeparator(visualWidth, headerAlign)
|
||||
} else {
|
||||
content := tw.Empty
|
||||
if colIndex < len(line) {
|
||||
content = line[colIndex]
|
||||
}
|
||||
// For rows, use the header's alignment if specified
|
||||
rowAlign := align
|
||||
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false {
|
||||
if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty {
|
||||
rowAlign = headerCellCtx.Align
|
||||
}
|
||||
}
|
||||
if rowAlign == tw.AlignNone || rowAlign == tw.Empty {
|
||||
if ctx.Row.Position == tw.Header {
|
||||
rowAlign = tw.AlignCenter
|
||||
} else if ctx.Row.Position == tw.Footer {
|
||||
rowAlign = tw.AlignRight
|
||||
} else {
|
||||
rowAlign = tw.AlignLeft
|
||||
}
|
||||
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign)
|
||||
}
|
||||
formattedSegment = m.formatCell(content, visualWidth, rowAlign, cellCtx.Padding)
|
||||
}
|
||||
output.WriteString(formattedSegment)
|
||||
m.logger.Debugf("renderMarkdownLine: Wrote col %d (span %d, width %d): '%s'", colIndex, span, visualWidth, formattedSegment)
|
||||
}
|
||||
|
||||
colIndex += span
|
||||
}
|
||||
|
||||
output.WriteString(suffix)
|
||||
output.WriteString(tw.NewLine)
|
||||
m.w.Write([]byte(output.String()))
|
||||
m.logger.Debugf("renderMarkdownLine: Final line: %s", strings.TrimSuffix(output.String(), tw.NewLine))
|
||||
}
|
||||
470
vendor/github.com/olekukonko/tablewriter/renderer/ocean.go
generated
vendored
Normal file
470
vendor/github.com/olekukonko/tablewriter/renderer/ocean.go
generated
vendored
Normal file
@@ -0,0 +1,470 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/ll"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// OceanConfig defines configuration specific to the Ocean renderer.
|
||||
type OceanConfig struct {
|
||||
}
|
||||
|
||||
// Ocean is a streaming table renderer that writes ASCII tables.
|
||||
type Ocean struct {
|
||||
config tw.Rendition
|
||||
oceanConfig OceanConfig
|
||||
fixedWidths tw.Mapper[int, int]
|
||||
widthsFinalized bool
|
||||
tableOutputStarted bool
|
||||
headerContentRendered bool // True if actual header *content* has been rendered by Ocean.Header
|
||||
logger *ll.Logger
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewOcean(oceanConfig ...OceanConfig) *Ocean {
|
||||
cfg := defaultOceanRendererConfig()
|
||||
oCfg := OceanConfig{}
|
||||
if len(oceanConfig) > 0 {
|
||||
// Apply user-provided OceanConfig if necessary
|
||||
}
|
||||
r := &Ocean{
|
||||
config: cfg,
|
||||
oceanConfig: oCfg,
|
||||
fixedWidths: tw.NewMapper[int, int](),
|
||||
logger: ll.New("ocean"),
|
||||
}
|
||||
r.resetState()
|
||||
return r
|
||||
}
|
||||
|
||||
func (o *Ocean) resetState() {
|
||||
o.fixedWidths = tw.NewMapper[int, int]()
|
||||
o.widthsFinalized = false
|
||||
o.tableOutputStarted = false
|
||||
o.headerContentRendered = false
|
||||
o.logger.Debug("State reset.")
|
||||
}
|
||||
|
||||
func (o *Ocean) Logger(logger *ll.Logger) {
|
||||
o.logger = logger.Namespace("ocean")
|
||||
}
|
||||
|
||||
func (o *Ocean) Config() tw.Rendition {
|
||||
return o.config
|
||||
}
|
||||
|
||||
func (o *Ocean) tryFinalizeWidths(ctx tw.Formatting) {
|
||||
if o.widthsFinalized {
|
||||
return
|
||||
}
|
||||
if ctx.Row.Widths != nil && ctx.Row.Widths.Len() > 0 {
|
||||
o.fixedWidths = ctx.Row.Widths.Clone()
|
||||
o.widthsFinalized = true
|
||||
o.logger.Debugf("Widths finalized from context: %v", o.fixedWidths)
|
||||
} else {
|
||||
o.logger.Warn("Attempted to finalize widths, but no width data in context.")
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Ocean) Start(w io.Writer) error {
|
||||
o.w = w
|
||||
o.logger.Debug("Start() called.")
|
||||
o.resetState()
|
||||
// Top border is drawn by the first component (Header or Row) that has widths
|
||||
// OR by an explicit Line() call from table.go's batch renderer.
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderTopBorderIfNeeded is called by Header or Row if it's the first to render
|
||||
// and tableOutputStarted is false.
|
||||
func (o *Ocean) renderTopBorderIfNeeded(currentPosition tw.Position, ctx tw.Formatting) {
|
||||
if !o.tableOutputStarted && o.widthsFinalized {
|
||||
// This renderer's config for Top border
|
||||
if o.config.Borders.Top.Enabled() && o.config.Settings.Lines.ShowTop.Enabled() {
|
||||
o.logger.Debugf("Ocean itself rendering top border (triggered by %s)", currentPosition)
|
||||
lineCtx := tw.Formatting{ // Construct specific context for this line
|
||||
Row: tw.RowContext{
|
||||
Widths: o.fixedWidths,
|
||||
Location: tw.LocationFirst,
|
||||
Position: currentPosition,
|
||||
Next: ctx.Row.Current, // The actual first content is "Next" to the top border
|
||||
},
|
||||
Level: tw.LevelHeader,
|
||||
}
|
||||
o.Line(lineCtx)
|
||||
o.tableOutputStarted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Ocean) Header(headers [][]string, ctx tw.Formatting) {
|
||||
o.logger.Debugf("Ocean.Header called: IsSubRow=%v, Location=%v, NumLines=%d", ctx.IsSubRow, ctx.Row.Location, len(headers))
|
||||
|
||||
if !o.widthsFinalized {
|
||||
o.tryFinalizeWidths(ctx)
|
||||
}
|
||||
|
||||
if !o.widthsFinalized {
|
||||
o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.")
|
||||
return
|
||||
}
|
||||
|
||||
if len(headers) > 0 && len(headers[0]) > 0 {
|
||||
for i, headerLineData := range headers {
|
||||
currentLineCtx := ctx
|
||||
currentLineCtx.Row.Widths = o.fixedWidths
|
||||
if i > 0 {
|
||||
currentLineCtx.IsSubRow = true
|
||||
}
|
||||
o.renderContentLine(currentLineCtx, headerLineData)
|
||||
o.tableOutputStarted = true // Content was written
|
||||
}
|
||||
o.headerContentRendered = true
|
||||
} else {
|
||||
o.logger.Debug("Ocean.Header: No actual header content lines to render.")
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Ocean) Row(row []string, ctx tw.Formatting) {
|
||||
o.logger.Debugf("Ocean.Row called: IsSubRow=%v, Location=%v, DataItems=%d", ctx.IsSubRow, ctx.Row.Location, len(row))
|
||||
|
||||
if !o.widthsFinalized {
|
||||
o.tryFinalizeWidths(ctx)
|
||||
}
|
||||
if !o.widthsFinalized {
|
||||
o.logger.Error("Ocean.Row: Cannot render content, widths are not finalized.")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Row.Widths = o.fixedWidths
|
||||
o.renderContentLine(ctx, row)
|
||||
o.tableOutputStarted = true
|
||||
}
|
||||
|
||||
func (o *Ocean) Footer(footers [][]string, ctx tw.Formatting) {
|
||||
o.logger.Debugf("Ocean.Footer called: IsSubRow=%v, Location=%v, NumLines=%d", ctx.IsSubRow, ctx.Row.Location, len(footers))
|
||||
|
||||
if !o.widthsFinalized {
|
||||
o.tryFinalizeWidths(ctx)
|
||||
o.logger.Warn("Ocean.Footer: Widths finalized at Footer stage (unusual).")
|
||||
}
|
||||
if !o.widthsFinalized {
|
||||
o.logger.Error("Ocean.Footer: Cannot render content, widths are not finalized.")
|
||||
return
|
||||
}
|
||||
|
||||
if len(footers) > 0 && len(footers[0]) > 0 {
|
||||
for i, footerLineData := range footers {
|
||||
currentLineCtx := ctx
|
||||
currentLineCtx.Row.Widths = o.fixedWidths
|
||||
if i > 0 {
|
||||
currentLineCtx.IsSubRow = true
|
||||
}
|
||||
o.renderContentLine(currentLineCtx, footerLineData)
|
||||
o.tableOutputStarted = true
|
||||
}
|
||||
} else {
|
||||
o.logger.Debug("Ocean.Footer: No actual footer content lines to render.")
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Ocean) Line(ctx tw.Formatting) {
|
||||
if !o.widthsFinalized {
|
||||
o.tryFinalizeWidths(ctx)
|
||||
if !o.widthsFinalized {
|
||||
o.logger.Error("Ocean.Line: Called but widths could not be finalized. Skipping line rendering.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Line uses the consistent fixedWidths for drawing
|
||||
ctx.Row.Widths = o.fixedWidths
|
||||
o.logger.Debugf("Ocean.Line DRAWING: Level=%v, Loc=%s, Pos=%s, IsSubRow=%t, WidthsLen=%d", ctx.Level, ctx.Row.Location, ctx.Row.Position, ctx.IsSubRow, ctx.Row.Widths.Len())
|
||||
|
||||
jr := NewJunction(JunctionContext{
|
||||
Symbols: o.config.Symbols,
|
||||
Ctx: ctx,
|
||||
ColIdx: 0,
|
||||
Logger: o.logger,
|
||||
BorderTint: Tint{},
|
||||
SeparatorTint: Tint{},
|
||||
})
|
||||
|
||||
var line strings.Builder
|
||||
sortedColIndices := o.fixedWidths.SortedKeys()
|
||||
|
||||
if len(sortedColIndices) == 0 {
|
||||
drewEmptyBorders := false
|
||||
if o.config.Borders.Left.Enabled() {
|
||||
line.WriteString(jr.RenderLeft())
|
||||
drewEmptyBorders = true
|
||||
}
|
||||
if o.config.Borders.Right.Enabled() {
|
||||
line.WriteString(jr.RenderRight(-1))
|
||||
drewEmptyBorders = true
|
||||
}
|
||||
if drewEmptyBorders {
|
||||
line.WriteString(tw.NewLine)
|
||||
o.w.Write([]byte(line.String()))
|
||||
o.logger.Debug("Line: Drew empty table borders based on Line call.")
|
||||
} else {
|
||||
o.logger.Debug("Line: Handled empty table case (no columns, no borders).")
|
||||
}
|
||||
o.tableOutputStarted = drewEmptyBorders // A line counts as output
|
||||
return
|
||||
}
|
||||
|
||||
if o.config.Borders.Left.Enabled() {
|
||||
line.WriteString(jr.RenderLeft())
|
||||
}
|
||||
|
||||
for i, colIdx := range sortedColIndices {
|
||||
jr.colIdx = colIdx
|
||||
segmentChar := jr.GetSegment()
|
||||
colVisualWidth := o.fixedWidths.Get(colIdx)
|
||||
|
||||
if colVisualWidth <= 0 {
|
||||
// Still need to consider separators after zero-width columns
|
||||
} else {
|
||||
if segmentChar == tw.Empty {
|
||||
segmentChar = o.config.Symbols.Row()
|
||||
}
|
||||
segmentDisplayWidth := twwidth.Width(segmentChar)
|
||||
if segmentDisplayWidth <= 0 {
|
||||
segmentDisplayWidth = 1
|
||||
}
|
||||
|
||||
repeatCount := 0
|
||||
if colVisualWidth > 0 {
|
||||
repeatCount = colVisualWidth / segmentDisplayWidth
|
||||
if repeatCount == 0 {
|
||||
repeatCount = 1
|
||||
}
|
||||
}
|
||||
line.WriteString(strings.Repeat(segmentChar, repeatCount))
|
||||
}
|
||||
|
||||
if i < len(sortedColIndices)-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
nextColIdx := sortedColIndices[i+1]
|
||||
line.WriteString(jr.RenderJunction(colIdx, nextColIdx))
|
||||
}
|
||||
}
|
||||
|
||||
if o.config.Borders.Right.Enabled() {
|
||||
lastColIdx := sortedColIndices[len(sortedColIndices)-1]
|
||||
line.WriteString(jr.RenderRight(lastColIdx))
|
||||
}
|
||||
|
||||
line.WriteString(tw.NewLine)
|
||||
o.w.Write([]byte(line.String()))
|
||||
o.tableOutputStarted = true
|
||||
o.logger.Debugf("Line rendered by explicit call: %s", strings.TrimSuffix(line.String(), tw.NewLine))
|
||||
}
|
||||
|
||||
func (o *Ocean) Close() error {
|
||||
o.logger.Debug("Ocean.Close() called.")
|
||||
o.resetState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Ocean) renderContentLine(ctx tw.Formatting, lineData []string) {
|
||||
if !o.widthsFinalized || o.fixedWidths.Len() == 0 {
|
||||
o.logger.Error("renderContentLine: Cannot render, fixedWidths not set or empty.")
|
||||
return
|
||||
}
|
||||
|
||||
var output strings.Builder
|
||||
if o.config.Borders.Left.Enabled() {
|
||||
output.WriteString(o.config.Symbols.Column())
|
||||
}
|
||||
|
||||
sortedColIndices := o.fixedWidths.SortedKeys()
|
||||
|
||||
for i, colIdx := range sortedColIndices {
|
||||
cellVisualWidth := o.fixedWidths.Get(colIdx)
|
||||
cellContent := tw.Empty
|
||||
align := tw.AlignDefault
|
||||
padding := tw.Padding{Left: tw.Space, Right: tw.Space}
|
||||
|
||||
switch ctx.Row.Position {
|
||||
case tw.Header:
|
||||
align = tw.AlignCenter
|
||||
case tw.Footer:
|
||||
align = tw.AlignRight
|
||||
default:
|
||||
align = tw.AlignLeft
|
||||
}
|
||||
|
||||
cellCtx, hasCellCtx := ctx.Row.Current[colIdx]
|
||||
if hasCellCtx {
|
||||
cellContent = cellCtx.Data
|
||||
if cellCtx.Align.Validate() == nil && cellCtx.Align != tw.AlignNone {
|
||||
align = cellCtx.Align
|
||||
}
|
||||
if cellCtx.Padding.Paddable() {
|
||||
padding = cellCtx.Padding
|
||||
}
|
||||
} else if colIdx < len(lineData) {
|
||||
cellContent = lineData[colIdx]
|
||||
}
|
||||
|
||||
actualCellWidthToRender := cellVisualWidth
|
||||
isHMergeContinuation := false
|
||||
|
||||
if hasCellCtx && cellCtx.Merge.Horizontal.Present {
|
||||
if cellCtx.Merge.Horizontal.Start {
|
||||
hSpan := cellCtx.Merge.Horizontal.Span
|
||||
if hSpan <= 0 {
|
||||
hSpan = 1
|
||||
}
|
||||
|
||||
currentMergeTotalRenderWidth := 0
|
||||
for k := 0; k < hSpan; k++ {
|
||||
idxInMergeSpan := colIdx + k
|
||||
// Check if idxInMergeSpan is a defined column in fixedWidths
|
||||
foundInFixedWidths := false
|
||||
for _, sortedCIdx_inner := range sortedColIndices {
|
||||
if sortedCIdx_inner == idxInMergeSpan {
|
||||
currentMergeTotalRenderWidth += o.fixedWidths.Get(idxInMergeSpan)
|
||||
foundInFixedWidths = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundInFixedWidths && idxInMergeSpan <= sortedColIndices[len(sortedColIndices)-1] {
|
||||
o.logger.Debugf("Col %d in HMerge span not found in fixedWidths, assuming 0-width contribution.", idxInMergeSpan)
|
||||
}
|
||||
|
||||
if k < hSpan-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
currentMergeTotalRenderWidth += twwidth.Width(o.config.Symbols.Column())
|
||||
}
|
||||
}
|
||||
actualCellWidthToRender = currentMergeTotalRenderWidth
|
||||
} else {
|
||||
isHMergeContinuation = true
|
||||
}
|
||||
}
|
||||
|
||||
if isHMergeContinuation {
|
||||
o.logger.Debugf("renderContentLine: Col %d is HMerge continuation, skipping content render.", colIdx)
|
||||
// The separator logic below needs to handle this correctly.
|
||||
// If the *previous* column was the start of a merge that spans *this* column,
|
||||
// then the separator after the previous column should have been suppressed.
|
||||
} else if actualCellWidthToRender > 0 {
|
||||
formattedCell := o.formatCellContent(cellContent, actualCellWidthToRender, padding, align)
|
||||
output.WriteString(formattedCell)
|
||||
} else {
|
||||
o.logger.Debugf("renderContentLine: col %d has 0 render width, writing no content.", colIdx)
|
||||
}
|
||||
|
||||
// Add column separator if:
|
||||
// 1. It's not the last column in sortedColIndices
|
||||
// 2. Separators are enabled
|
||||
// 3. This cell is NOT a horizontal merge start that spans over the next column.
|
||||
if i < len(sortedColIndices)-1 && o.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
shouldAddSeparator := true
|
||||
if hasCellCtx && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
|
||||
// If this merge start spans beyond the current colIdx into the next sortedColIndex
|
||||
if colIdx+cellCtx.Merge.Horizontal.Span > sortedColIndices[i+1] {
|
||||
shouldAddSeparator = false // Separator is part of the merged cell's width
|
||||
o.logger.Debugf("renderContentLine: Suppressed separator after HMerge col %d.", colIdx)
|
||||
}
|
||||
}
|
||||
if shouldAddSeparator {
|
||||
output.WriteString(o.config.Symbols.Column())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if o.config.Borders.Right.Enabled() {
|
||||
output.WriteString(o.config.Symbols.Column())
|
||||
}
|
||||
|
||||
output.WriteString(tw.NewLine)
|
||||
o.w.Write([]byte(output.String()))
|
||||
o.logger.Debugf("Content line rendered: %s", strings.TrimSuffix(output.String(), tw.NewLine))
|
||||
}
|
||||
|
||||
func (o *Ocean) formatCellContent(content string, cellVisualWidth int, padding tw.Padding, align tw.Align) string {
|
||||
if cellVisualWidth <= 0 {
|
||||
return tw.Empty
|
||||
}
|
||||
|
||||
contentDisplayWidth := twwidth.Width(content)
|
||||
|
||||
padLeftChar := padding.Left
|
||||
if padLeftChar == tw.Empty {
|
||||
padLeftChar = tw.Space
|
||||
}
|
||||
padRightChar := padding.Right
|
||||
if padRightChar == tw.Empty {
|
||||
padRightChar = tw.Space
|
||||
}
|
||||
|
||||
padLeftDisplayWidth := twwidth.Width(padLeftChar)
|
||||
padRightDisplayWidth := twwidth.Width(padRightChar)
|
||||
|
||||
spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth
|
||||
if spaceForContentAndAlignment < 0 {
|
||||
spaceForContentAndAlignment = 0
|
||||
}
|
||||
|
||||
if contentDisplayWidth > spaceForContentAndAlignment {
|
||||
content = twwidth.Truncate(content, spaceForContentAndAlignment)
|
||||
contentDisplayWidth = twwidth.Width(content)
|
||||
}
|
||||
|
||||
remainingSpace := spaceForContentAndAlignment - contentDisplayWidth
|
||||
if remainingSpace < 0 {
|
||||
remainingSpace = 0
|
||||
}
|
||||
|
||||
var PL, PR string
|
||||
switch align {
|
||||
case tw.AlignRight:
|
||||
PL = strings.Repeat(tw.Space, remainingSpace)
|
||||
case tw.AlignCenter:
|
||||
leftSpaces := remainingSpace / 2
|
||||
rightSpaces := remainingSpace - leftSpaces
|
||||
PL = strings.Repeat(tw.Space, leftSpaces)
|
||||
PR = strings.Repeat(tw.Space, rightSpaces)
|
||||
default:
|
||||
PR = strings.Repeat(tw.Space, remainingSpace)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(padLeftChar)
|
||||
sb.WriteString(PL)
|
||||
sb.WriteString(content)
|
||||
sb.WriteString(PR)
|
||||
sb.WriteString(padRightChar)
|
||||
|
||||
currentFormattedWidth := twwidth.Width(sb.String())
|
||||
if currentFormattedWidth < cellVisualWidth {
|
||||
if align == tw.AlignRight {
|
||||
prefixSpaces := strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth)
|
||||
finalStr := prefixSpaces + sb.String()
|
||||
sb.Reset()
|
||||
sb.WriteString(finalStr)
|
||||
} else {
|
||||
sb.WriteString(strings.Repeat(tw.Space, cellVisualWidth-currentFormattedWidth))
|
||||
}
|
||||
} else if currentFormattedWidth > cellVisualWidth {
|
||||
tempStr := sb.String()
|
||||
sb.Reset()
|
||||
sb.WriteString(twwidth.Truncate(tempStr, cellVisualWidth))
|
||||
o.logger.Warnf("formatCellContent: Final string '%s' (width %d) exceeded target %d. Force truncated.", tempStr, currentFormattedWidth, cellVisualWidth)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (o *Ocean) Rendition(config tw.Rendition) {
|
||||
o.config = mergeRendition(o.config, config)
|
||||
o.logger.Debugf("Blueprint.Rendition updated. New internal config: %+v", o.config)
|
||||
}
|
||||
|
||||
// Ensure Blueprint implements tw.Renditioning
|
||||
var _ tw.Renditioning = (*Ocean)(nil)
|
||||
703
vendor/github.com/olekukonko/tablewriter/renderer/svg.go
generated
vendored
Normal file
703
vendor/github.com/olekukonko/tablewriter/renderer/svg.go
generated
vendored
Normal file
@@ -0,0 +1,703 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll"
|
||||
"html"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// SVGConfig holds configuration for the SVG renderer.
|
||||
// Fields include font, colors, padding, and merge rendering options.
|
||||
// Used to customize SVG output appearance and behavior.
|
||||
type SVGConfig struct {
|
||||
FontFamily string // e.g., "Arial, sans-serif"
|
||||
FontSize float64 // Base font size in SVG units
|
||||
LineHeightFactor float64 // Factor for line height (e.g., 1.2)
|
||||
Padding float64 // Padding inside cells
|
||||
StrokeWidth float64 // Line width for borders
|
||||
StrokeColor string // Color for strokes (e.g., "black")
|
||||
HeaderBG string // Background color for header
|
||||
RowBG string // Background color for rows
|
||||
RowAltBG string // Alternating row background color
|
||||
FooterBG string // Background color for footer
|
||||
HeaderColor string // Text color for header
|
||||
RowColor string // Text color for rows
|
||||
FooterColor string // Text color for footer
|
||||
ApproxCharWidthFactor float64 // Char width relative to FontSize
|
||||
MinColWidth float64 // Minimum column width
|
||||
RenderTWConfigOverrides bool // Override SVG alignments with tablewriter
|
||||
Debug bool // Enable debug logging
|
||||
ScaleFactor float64 // Scaling factor for SVG
|
||||
}
|
||||
|
||||
// SVG implements tw.Renderer for SVG output.
|
||||
// Manages SVG element generation and merge tracking.
|
||||
type SVG struct {
|
||||
config SVGConfig
|
||||
trace []string
|
||||
|
||||
allVisualLineData [][][]string // [section][line][cell]
|
||||
allVisualLineCtx [][]tw.Formatting // [section][line]Formatting
|
||||
|
||||
maxCols int
|
||||
calculatedColWidths []float64
|
||||
svgElements strings.Builder
|
||||
currentY float64
|
||||
dataRowCounter int
|
||||
vMergeTrack map[int]int // Tracks vertical merge spans
|
||||
numVisualRowsDrawn int
|
||||
logger *ll.Logger
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
const (
|
||||
sectionTypeHeader = 0
|
||||
sectionTypeRow = 1
|
||||
sectionTypeFooter = 2
|
||||
)
|
||||
|
||||
// NewSVG creates a new SVG renderer with configuration.
|
||||
// Parameter configs provides optional SVGConfig; defaults used if empty.
|
||||
// Returns a configured SVG instance.
|
||||
func NewSVG(configs ...SVGConfig) *SVG {
|
||||
cfg := SVGConfig{
|
||||
FontFamily: "sans-serif",
|
||||
FontSize: 12.0,
|
||||
LineHeightFactor: 1.4,
|
||||
Padding: 5.0,
|
||||
StrokeWidth: 1.0,
|
||||
StrokeColor: "black",
|
||||
HeaderBG: "#F0F0F0",
|
||||
RowBG: "white",
|
||||
RowAltBG: "#F9F9F9",
|
||||
FooterBG: "#F0F0F0",
|
||||
HeaderColor: "black",
|
||||
RowColor: "black",
|
||||
FooterColor: "black",
|
||||
ApproxCharWidthFactor: 0.6,
|
||||
MinColWidth: 30.0,
|
||||
ScaleFactor: 1.0,
|
||||
RenderTWConfigOverrides: true,
|
||||
Debug: false,
|
||||
}
|
||||
if len(configs) > 0 {
|
||||
userCfg := configs[0]
|
||||
if userCfg.FontFamily != tw.Empty {
|
||||
cfg.FontFamily = userCfg.FontFamily
|
||||
}
|
||||
if userCfg.FontSize > 0 {
|
||||
cfg.FontSize = userCfg.FontSize
|
||||
}
|
||||
if userCfg.LineHeightFactor > 0 {
|
||||
cfg.LineHeightFactor = userCfg.LineHeightFactor
|
||||
}
|
||||
if userCfg.Padding >= 0 {
|
||||
cfg.Padding = userCfg.Padding
|
||||
}
|
||||
if userCfg.StrokeWidth > 0 {
|
||||
cfg.StrokeWidth = userCfg.StrokeWidth
|
||||
}
|
||||
if userCfg.StrokeColor != tw.Empty {
|
||||
cfg.StrokeColor = userCfg.StrokeColor
|
||||
}
|
||||
if userCfg.HeaderBG != tw.Empty {
|
||||
cfg.HeaderBG = userCfg.HeaderBG
|
||||
}
|
||||
if userCfg.RowBG != tw.Empty {
|
||||
cfg.RowBG = userCfg.RowBG
|
||||
}
|
||||
cfg.RowAltBG = userCfg.RowAltBG
|
||||
if userCfg.FooterBG != tw.Empty {
|
||||
cfg.FooterBG = userCfg.FooterBG
|
||||
}
|
||||
if userCfg.HeaderColor != tw.Empty {
|
||||
cfg.HeaderColor = userCfg.HeaderColor
|
||||
}
|
||||
if userCfg.RowColor != tw.Empty {
|
||||
cfg.RowColor = userCfg.RowColor
|
||||
}
|
||||
if userCfg.FooterColor != tw.Empty {
|
||||
cfg.FooterColor = userCfg.FooterColor
|
||||
}
|
||||
if userCfg.ApproxCharWidthFactor > 0 {
|
||||
cfg.ApproxCharWidthFactor = userCfg.ApproxCharWidthFactor
|
||||
}
|
||||
if userCfg.MinColWidth >= 0 {
|
||||
cfg.MinColWidth = userCfg.MinColWidth
|
||||
}
|
||||
cfg.RenderTWConfigOverrides = userCfg.RenderTWConfigOverrides
|
||||
cfg.Debug = userCfg.Debug
|
||||
}
|
||||
r := &SVG{
|
||||
config: cfg,
|
||||
trace: make([]string, 0, 50),
|
||||
allVisualLineData: make([][][]string, 3),
|
||||
allVisualLineCtx: make([][]tw.Formatting, 3),
|
||||
vMergeTrack: make(map[int]int),
|
||||
logger: ll.New("svg"),
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
r.allVisualLineData[i] = make([][]string, 0)
|
||||
r.allVisualLineCtx[i] = make([]tw.Formatting, 0)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// calculateAllColumnWidths computes column widths based on content and merges.
|
||||
// Uses content length and merge spans; handles horizontal merges by distributing width.
|
||||
func (s *SVG) calculateAllColumnWidths() {
|
||||
s.debug("Calculating column widths")
|
||||
tempMaxCols := 0
|
||||
for sectionIdx := 0; sectionIdx < 3; sectionIdx++ {
|
||||
for lineIdx, lineCtx := range s.allVisualLineCtx[sectionIdx] {
|
||||
if lineCtx.Row.Current != nil {
|
||||
visualColCount := 0
|
||||
for colIdx := 0; colIdx < len(lineCtx.Row.Current); {
|
||||
cellCtx := lineCtx.Row.Current[colIdx]
|
||||
if cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start {
|
||||
colIdx++ // Skip non-start merged cells
|
||||
continue
|
||||
}
|
||||
visualColCount++
|
||||
span := 1
|
||||
if cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start {
|
||||
span = cellCtx.Merge.Horizontal.Span
|
||||
if span <= 0 {
|
||||
span = 1
|
||||
}
|
||||
}
|
||||
colIdx += span
|
||||
}
|
||||
s.debug("Section %d, line %d: Visual columns = %d", sectionIdx, lineIdx, visualColCount)
|
||||
if visualColCount > tempMaxCols {
|
||||
tempMaxCols = visualColCount
|
||||
}
|
||||
} else if lineIdx < len(s.allVisualLineData[sectionIdx]) {
|
||||
if rawDataLen := len(s.allVisualLineData[sectionIdx][lineIdx]); rawDataLen > tempMaxCols {
|
||||
tempMaxCols = rawDataLen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.maxCols = tempMaxCols
|
||||
s.debug("Max columns: %d", s.maxCols)
|
||||
if s.maxCols == 0 {
|
||||
s.calculatedColWidths = []float64{}
|
||||
return
|
||||
}
|
||||
s.calculatedColWidths = make([]float64, s.maxCols)
|
||||
for i := range s.calculatedColWidths {
|
||||
s.calculatedColWidths[i] = s.config.MinColWidth
|
||||
}
|
||||
|
||||
// Structure to track max width for each merge group
|
||||
type mergeKey struct {
|
||||
startCol int
|
||||
span int
|
||||
}
|
||||
maxMergeWidths := make(map[mergeKey]float64)
|
||||
|
||||
processSectionForWidth := func(sectionIdx int) {
|
||||
for lineIdx, visualLineData := range s.allVisualLineData[sectionIdx] {
|
||||
if lineIdx >= len(s.allVisualLineCtx[sectionIdx]) {
|
||||
s.debug("Warning: Missing context for section %d line %d", sectionIdx, lineIdx)
|
||||
continue
|
||||
}
|
||||
lineCtx := s.allVisualLineCtx[sectionIdx][lineIdx]
|
||||
currentTableCol := 0
|
||||
currentVisualCol := 0
|
||||
for currentVisualCol < len(visualLineData) && currentTableCol < s.maxCols {
|
||||
cellContent := visualLineData[currentVisualCol]
|
||||
cellCtx := tw.CellContext{}
|
||||
if lineCtx.Row.Current != nil {
|
||||
if c, ok := lineCtx.Row.Current[currentTableCol]; ok {
|
||||
cellCtx = c
|
||||
}
|
||||
}
|
||||
hSpan := 1
|
||||
if cellCtx.Merge.Horizontal.Present {
|
||||
if cellCtx.Merge.Horizontal.Start {
|
||||
hSpan = cellCtx.Merge.Horizontal.Span
|
||||
if hSpan <= 0 {
|
||||
hSpan = 1
|
||||
}
|
||||
} else {
|
||||
currentTableCol++
|
||||
continue
|
||||
}
|
||||
}
|
||||
textPixelWidth := s.estimateTextWidth(cellContent)
|
||||
contentAndPaddingWidth := textPixelWidth + (2 * s.config.Padding)
|
||||
if hSpan == 1 {
|
||||
if currentTableCol < len(s.calculatedColWidths) && contentAndPaddingWidth > s.calculatedColWidths[currentTableCol] {
|
||||
s.calculatedColWidths[currentTableCol] = contentAndPaddingWidth
|
||||
}
|
||||
} else {
|
||||
totalMergedWidth := contentAndPaddingWidth + (float64(hSpan-1) * s.config.Padding * 2)
|
||||
if totalMergedWidth < s.config.MinColWidth*float64(hSpan) {
|
||||
totalMergedWidth = s.config.MinColWidth * float64(hSpan)
|
||||
}
|
||||
if currentTableCol < len(s.calculatedColWidths) {
|
||||
key := mergeKey{currentTableCol, hSpan}
|
||||
if currentWidth, ok := maxMergeWidths[key]; ok {
|
||||
if totalMergedWidth > currentWidth {
|
||||
maxMergeWidths[key] = totalMergedWidth
|
||||
}
|
||||
} else {
|
||||
maxMergeWidths[key] = totalMergedWidth
|
||||
}
|
||||
s.debug("Horizontal merge at col %d, span %d: Total width %.2f", currentTableCol, hSpan, totalMergedWidth)
|
||||
}
|
||||
}
|
||||
currentTableCol += hSpan
|
||||
currentVisualCol++
|
||||
}
|
||||
}
|
||||
}
|
||||
processSectionForWidth(sectionTypeHeader)
|
||||
processSectionForWidth(sectionTypeRow)
|
||||
processSectionForWidth(sectionTypeFooter)
|
||||
|
||||
// Apply maximum widths for merged cells
|
||||
for key, width := range maxMergeWidths {
|
||||
s.calculatedColWidths[key.startCol] = width
|
||||
for i := 1; i < key.span && (key.startCol+i) < len(s.calculatedColWidths); i++ {
|
||||
s.calculatedColWidths[key.startCol+i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
for i := range s.calculatedColWidths {
|
||||
if s.calculatedColWidths[i] < s.config.MinColWidth && s.calculatedColWidths[i] != 0 {
|
||||
s.calculatedColWidths[i] = s.config.MinColWidth
|
||||
}
|
||||
}
|
||||
s.debug("Column widths: %v", s.calculatedColWidths)
|
||||
}
|
||||
|
||||
// Close finalizes SVG rendering and writes output.
|
||||
// Parameter w is the output w.
|
||||
// Returns an error if writing fails.
|
||||
func (s *SVG) Close() error {
|
||||
s.debug("Finalizing SVG output")
|
||||
s.calculateAllColumnWidths()
|
||||
s.renderBufferedData()
|
||||
if s.numVisualRowsDrawn == 0 && s.maxCols == 0 {
|
||||
fmt.Fprintf(s.w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2f" height="%.2f"></svg>`, s.config.StrokeWidth*2, s.config.StrokeWidth*2)
|
||||
return nil
|
||||
}
|
||||
totalWidth := s.config.StrokeWidth
|
||||
if len(s.calculatedColWidths) > 0 {
|
||||
for _, cw := range s.calculatedColWidths {
|
||||
colWidth := cw
|
||||
if colWidth <= 0 {
|
||||
colWidth = s.config.MinColWidth
|
||||
}
|
||||
totalWidth += colWidth + s.config.StrokeWidth
|
||||
}
|
||||
} else if s.maxCols > 0 {
|
||||
for i := 0; i < s.maxCols; i++ {
|
||||
totalWidth += s.config.MinColWidth + s.config.StrokeWidth
|
||||
}
|
||||
} else {
|
||||
totalWidth = s.config.StrokeWidth * 2
|
||||
}
|
||||
totalHeight := s.currentY
|
||||
singleVisualRowHeight := s.config.FontSize*s.config.LineHeightFactor + (2 * s.config.Padding)
|
||||
if s.numVisualRowsDrawn == 0 {
|
||||
if s.maxCols > 0 {
|
||||
totalHeight = s.config.StrokeWidth + singleVisualRowHeight + s.config.StrokeWidth
|
||||
} else {
|
||||
totalHeight = s.config.StrokeWidth * 2
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(s.w, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2f" height="%.2f" font-family="%s" font-size="%.2f">`,
|
||||
totalWidth, totalHeight, html.EscapeString(s.config.FontFamily), s.config.FontSize)
|
||||
fmt.Fprintln(s.w)
|
||||
fmt.Fprintln(s.w, "<style>text { stroke: none; }</style>")
|
||||
if _, err := io.WriteString(s.w, s.svgElements.String()); err != nil {
|
||||
fmt.Fprintln(s.w, `</svg>`)
|
||||
return fmt.Errorf("failed to write SVG elements: %w", err)
|
||||
}
|
||||
if s.maxCols > 0 || s.numVisualRowsDrawn > 0 {
|
||||
fmt.Fprintf(s.w, ` <g class="table-borders" stroke="%s" stroke-width="%.2f" stroke-linecap="square">`,
|
||||
html.EscapeString(s.config.StrokeColor), s.config.StrokeWidth)
|
||||
fmt.Fprintln(s.w)
|
||||
yPos := s.config.StrokeWidth / 2.0
|
||||
borderRowsToDraw := s.numVisualRowsDrawn
|
||||
if borderRowsToDraw == 0 && s.maxCols > 0 {
|
||||
borderRowsToDraw = 1
|
||||
}
|
||||
lineStartX := s.config.StrokeWidth / 2.0
|
||||
lineEndX := s.config.StrokeWidth / 2.0
|
||||
for _, width := range s.calculatedColWidths {
|
||||
lineEndX += width + s.config.StrokeWidth
|
||||
}
|
||||
for i := 0; i <= borderRowsToDraw; i++ {
|
||||
fmt.Fprintf(s.w, ` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" />%s`,
|
||||
lineStartX, yPos, lineEndX, yPos, "\n")
|
||||
if i < borderRowsToDraw {
|
||||
yPos += singleVisualRowHeight + s.config.StrokeWidth
|
||||
}
|
||||
}
|
||||
xPos := s.config.StrokeWidth / 2.0
|
||||
borderLineStartY := s.config.StrokeWidth / 2.0
|
||||
borderLineEndY := totalHeight - (s.config.StrokeWidth / 2.0)
|
||||
for visualColIdx := 0; visualColIdx <= s.maxCols; visualColIdx++ {
|
||||
fmt.Fprintf(s.w, ` <line x1="%.2f" y1="%.2f" x2="%.2f" y2="%.2f" />%s`,
|
||||
xPos, borderLineStartY, xPos, borderLineEndY, "\n")
|
||||
if visualColIdx < s.maxCols {
|
||||
colWidth := s.config.MinColWidth
|
||||
if visualColIdx < len(s.calculatedColWidths) && s.calculatedColWidths[visualColIdx] > 0 {
|
||||
colWidth = s.calculatedColWidths[visualColIdx]
|
||||
}
|
||||
xPos += colWidth + s.config.StrokeWidth
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(s.w, " </g>")
|
||||
}
|
||||
fmt.Fprintln(s.w, `</svg>`)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the renderer's configuration.
|
||||
// No parameters are required.
|
||||
// Returns a Rendition with border and debug settings.
|
||||
func (s *SVG) Config() tw.Rendition {
|
||||
return tw.Rendition{
|
||||
Borders: tw.Border{Left: tw.On, Right: tw.On, Top: tw.On, Bottom: tw.On},
|
||||
Settings: tw.Settings{},
|
||||
Streaming: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Debug returns the renderer's debug trace.
|
||||
// No parameters are required.
|
||||
// Returns a slice of debug messages.
|
||||
func (s *SVG) Debug() []string {
|
||||
return s.trace
|
||||
}
|
||||
|
||||
// estimateTextWidth estimates text width in SVG units.
|
||||
// Parameter text is the input string to measure.
|
||||
// Returns the estimated width based on font size and char factor.
|
||||
func (s *SVG) estimateTextWidth(text string) float64 {
|
||||
runeCount := float64(len([]rune(text)))
|
||||
return runeCount * s.config.FontSize * s.config.ApproxCharWidthFactor
|
||||
}
|
||||
|
||||
// Footer buffers footer lines for SVG rendering.
|
||||
// Parameters include w (w), footers (lines), and ctx (formatting).
|
||||
// No return value; stores data for later rendering.
|
||||
func (s *SVG) Footer(footers [][]string, ctx tw.Formatting) {
|
||||
s.debug("Buffering %d footer lines", len(footers))
|
||||
for i, line := range footers {
|
||||
currentCtx := ctx
|
||||
currentCtx.IsSubRow = (i > 0)
|
||||
s.storeVisualLine(sectionTypeFooter, line, currentCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// getSVGAnchorFromTW maps tablewriter alignment to SVG text-anchor.
|
||||
// Parameter align is the tablewriter alignment setting.
|
||||
// Returns the corresponding SVG text-anchor value or empty string.
|
||||
func (s *SVG) getSVGAnchorFromTW(align tw.Align) string {
|
||||
switch align {
|
||||
case tw.AlignLeft:
|
||||
return "start"
|
||||
case tw.AlignCenter:
|
||||
return "middle"
|
||||
case tw.AlignRight:
|
||||
return "end"
|
||||
case tw.AlignNone, tw.Skip:
|
||||
return tw.Empty
|
||||
}
|
||||
return tw.Empty
|
||||
}
|
||||
|
||||
// Header buffers header lines for SVG rendering.
|
||||
// Parameters include w (w), headers (lines), and ctx (formatting).
|
||||
// No return value; stores data for later rendering.
|
||||
func (s *SVG) Header(headers [][]string, ctx tw.Formatting) {
|
||||
s.debug("Buffering %d header lines", len(headers))
|
||||
for i, line := range headers {
|
||||
currentCtx := ctx
|
||||
currentCtx.IsSubRow = i > 0
|
||||
s.storeVisualLine(sectionTypeHeader, line, currentCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// Line handles border rendering (ignored in SVG renderer).
|
||||
// Parameters include w (w) and ctx (formatting).
|
||||
// No return value; SVG borders are drawn in Close.
|
||||
func (s *SVG) Line(ctx tw.Formatting) {
|
||||
s.debug("Line rendering ignored")
|
||||
}
|
||||
|
||||
// padLineSVG pads a line to the specified column count.
|
||||
// Parameters include line (input strings) and numCols (target length).
|
||||
// Returns the padded line with empty strings as needed.
|
||||
func padLineSVG(line []string, numCols int) []string {
|
||||
if numCols <= 0 {
|
||||
return []string{}
|
||||
}
|
||||
currentLen := len(line)
|
||||
if currentLen == numCols {
|
||||
return line
|
||||
}
|
||||
if currentLen > numCols {
|
||||
return line[:numCols]
|
||||
}
|
||||
padded := make([]string, numCols)
|
||||
copy(padded, line)
|
||||
return padded
|
||||
}
|
||||
|
||||
// renderBufferedData renders all buffered lines to SVG elements.
|
||||
// No parameters are required.
|
||||
// No return value; populates svgElements buffer.
|
||||
func (s *SVG) renderBufferedData() {
|
||||
s.debug("Rendering buffered data")
|
||||
s.currentY = s.config.StrokeWidth
|
||||
s.dataRowCounter = 0
|
||||
s.vMergeTrack = make(map[int]int)
|
||||
s.numVisualRowsDrawn = 0
|
||||
renderSection := func(sectionIdx int, position tw.Position) {
|
||||
for visualLineIdx, visualLineData := range s.allVisualLineData[sectionIdx] {
|
||||
if visualLineIdx >= len(s.allVisualLineCtx[sectionIdx]) {
|
||||
s.debug("Error: Missing context for section %d line %d", sectionIdx, visualLineIdx)
|
||||
continue
|
||||
}
|
||||
s.renderVisualLine(visualLineData, s.allVisualLineCtx[sectionIdx][visualLineIdx], position)
|
||||
}
|
||||
}
|
||||
renderSection(sectionTypeHeader, tw.Header)
|
||||
renderSection(sectionTypeRow, tw.Row)
|
||||
renderSection(sectionTypeFooter, tw.Footer)
|
||||
}
|
||||
|
||||
// renderVisualLine renders a single visual line as SVG elements.
|
||||
// Parameters include lineData (cell content), ctx (formatting), and position (section type).
|
||||
// No return value; handles horizontal and vertical merges.
|
||||
func (s *SVG) renderVisualLine(visualLineData []string, ctx tw.Formatting, position tw.Position) {
|
||||
if s.maxCols == 0 || len(s.calculatedColWidths) == 0 {
|
||||
s.debug("Skipping line rendering: maxCols=%d, widths=%d", s.maxCols, len(s.calculatedColWidths))
|
||||
return
|
||||
}
|
||||
s.numVisualRowsDrawn++
|
||||
s.debug("Rendering visual row %d", s.numVisualRowsDrawn)
|
||||
singleVisualRowHeight := s.config.FontSize*s.config.LineHeightFactor + (2 * s.config.Padding)
|
||||
bgColor := tw.Empty
|
||||
textColor := tw.Empty
|
||||
defaultTextAnchor := "start"
|
||||
switch position {
|
||||
case tw.Header:
|
||||
bgColor = s.config.HeaderBG
|
||||
textColor = s.config.HeaderColor
|
||||
defaultTextAnchor = "middle"
|
||||
case tw.Footer:
|
||||
bgColor = s.config.FooterBG
|
||||
textColor = s.config.FooterColor
|
||||
defaultTextAnchor = "end"
|
||||
default:
|
||||
textColor = s.config.RowColor
|
||||
if !ctx.IsSubRow {
|
||||
if s.config.RowAltBG != tw.Empty && s.dataRowCounter%2 != 0 {
|
||||
bgColor = s.config.RowAltBG
|
||||
} else {
|
||||
bgColor = s.config.RowBG
|
||||
}
|
||||
s.dataRowCounter++
|
||||
} else {
|
||||
parentDataRowStripeIndex := s.dataRowCounter - 1
|
||||
if parentDataRowStripeIndex < 0 {
|
||||
parentDataRowStripeIndex = 0
|
||||
}
|
||||
if s.config.RowAltBG != tw.Empty && parentDataRowStripeIndex%2 != 0 {
|
||||
bgColor = s.config.RowAltBG
|
||||
} else {
|
||||
bgColor = s.config.RowBG
|
||||
}
|
||||
}
|
||||
}
|
||||
currentX := s.config.StrokeWidth
|
||||
currentVisualCellIdx := 0
|
||||
for tableColIdx := 0; tableColIdx < s.maxCols; {
|
||||
if tableColIdx >= len(s.calculatedColWidths) {
|
||||
s.debug("Table Col %d out of bounds for widths", tableColIdx)
|
||||
tableColIdx++
|
||||
continue
|
||||
}
|
||||
if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 {
|
||||
s.vMergeTrack[tableColIdx]--
|
||||
if s.vMergeTrack[tableColIdx] <= 1 {
|
||||
delete(s.vMergeTrack, tableColIdx)
|
||||
}
|
||||
currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth
|
||||
tableColIdx++
|
||||
continue
|
||||
}
|
||||
cellContentFromVisualLine := tw.Empty
|
||||
if currentVisualCellIdx < len(visualLineData) {
|
||||
cellContentFromVisualLine = visualLineData[currentVisualCellIdx]
|
||||
}
|
||||
cellCtx := tw.CellContext{}
|
||||
if ctx.Row.Current != nil {
|
||||
if c, ok := ctx.Row.Current[tableColIdx]; ok {
|
||||
cellCtx = c
|
||||
}
|
||||
}
|
||||
textToRender := cellContentFromVisualLine
|
||||
if cellCtx.Data != tw.Empty {
|
||||
if !((cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start)) {
|
||||
textToRender = cellCtx.Data
|
||||
} else {
|
||||
textToRender = tw.Empty
|
||||
}
|
||||
} else if (cellCtx.Merge.Vertical.Present && !cellCtx.Merge.Vertical.Start) || (cellCtx.Merge.Hierarchical.Present && !cellCtx.Merge.Hierarchical.Start) {
|
||||
textToRender = tw.Empty
|
||||
}
|
||||
hSpan := 1
|
||||
if cellCtx.Merge.Horizontal.Present {
|
||||
if cellCtx.Merge.Horizontal.Start {
|
||||
hSpan = cellCtx.Merge.Horizontal.Span
|
||||
if hSpan <= 0 {
|
||||
hSpan = 1
|
||||
}
|
||||
} else {
|
||||
currentX += s.calculatedColWidths[tableColIdx] + s.config.StrokeWidth
|
||||
tableColIdx++
|
||||
continue
|
||||
}
|
||||
}
|
||||
vSpan := 1
|
||||
isVSpanStart := false
|
||||
if cellCtx.Merge.Vertical.Present && cellCtx.Merge.Vertical.Start {
|
||||
vSpan = cellCtx.Merge.Vertical.Span
|
||||
isVSpanStart = true
|
||||
} else if cellCtx.Merge.Hierarchical.Present && cellCtx.Merge.Hierarchical.Start {
|
||||
vSpan = cellCtx.Merge.Hierarchical.Span
|
||||
isVSpanStart = true
|
||||
}
|
||||
if vSpan <= 0 {
|
||||
vSpan = 1
|
||||
}
|
||||
rectWidth := 0.0
|
||||
for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ {
|
||||
if (tableColIdx + hs) < len(s.calculatedColWidths) {
|
||||
rectWidth += s.calculatedColWidths[tableColIdx+hs]
|
||||
} else {
|
||||
rectWidth += s.config.MinColWidth
|
||||
}
|
||||
}
|
||||
if hSpan > 1 {
|
||||
rectWidth += float64(hSpan-1) * s.config.StrokeWidth
|
||||
}
|
||||
if rectWidth <= 0 {
|
||||
tableColIdx += hSpan
|
||||
if hSpan > 0 {
|
||||
currentVisualCellIdx++
|
||||
}
|
||||
continue
|
||||
}
|
||||
rectHeight := singleVisualRowHeight
|
||||
if isVSpanStart && vSpan > 1 {
|
||||
rectHeight = float64(vSpan)*singleVisualRowHeight + float64(vSpan-1)*s.config.StrokeWidth
|
||||
for hs := 0; hs < hSpan && (tableColIdx+hs) < s.maxCols; hs++ {
|
||||
s.vMergeTrack[tableColIdx+hs] = vSpan
|
||||
}
|
||||
s.debug("Vertical merge at col %d, span %d, height %.2f", tableColIdx, vSpan, rectHeight)
|
||||
} else if remainingVSpan, isMerging := s.vMergeTrack[tableColIdx]; isMerging && remainingVSpan > 1 {
|
||||
rectHeight = singleVisualRowHeight
|
||||
textToRender = tw.Empty
|
||||
}
|
||||
fmt.Fprintf(&s.svgElements, ` <rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s"/>%s`,
|
||||
currentX, s.currentY, rectWidth, rectHeight, html.EscapeString(bgColor), "\n")
|
||||
cellTextAnchor := defaultTextAnchor
|
||||
if s.config.RenderTWConfigOverrides {
|
||||
if al := s.getSVGAnchorFromTW(cellCtx.Align); al != tw.Empty {
|
||||
cellTextAnchor = al
|
||||
}
|
||||
}
|
||||
textX := currentX + s.config.Padding
|
||||
if cellTextAnchor == "middle" {
|
||||
textX = currentX + s.config.Padding + (rectWidth-2*s.config.Padding)/2.0
|
||||
} else if cellTextAnchor == "end" {
|
||||
textX = currentX + rectWidth - s.config.Padding
|
||||
}
|
||||
textY := s.currentY + rectHeight/2.0
|
||||
escapedCell := html.EscapeString(textToRender)
|
||||
fmt.Fprintf(&s.svgElements, ` <text x="%.2f" y="%.2f" fill="%s" text-anchor="%s" dominant-baseline="middle">%s</text>%s`,
|
||||
textX, textY, html.EscapeString(textColor), cellTextAnchor, escapedCell, "\n")
|
||||
currentX += rectWidth + s.config.StrokeWidth
|
||||
tableColIdx += hSpan
|
||||
currentVisualCellIdx++
|
||||
}
|
||||
s.currentY += singleVisualRowHeight + s.config.StrokeWidth
|
||||
}
|
||||
|
||||
// Reset clears the renderer's internal state.
|
||||
// No parameters are required.
|
||||
// No return value; prepares for new rendering.
|
||||
func (s *SVG) Reset() {
|
||||
s.debug("Resetting state")
|
||||
s.trace = make([]string, 0, 50)
|
||||
for i := 0; i < 3; i++ {
|
||||
s.allVisualLineData[i] = s.allVisualLineData[i][:0]
|
||||
s.allVisualLineCtx[i] = s.allVisualLineCtx[i][:0]
|
||||
}
|
||||
s.maxCols = 0
|
||||
s.calculatedColWidths = nil
|
||||
s.svgElements.Reset()
|
||||
s.currentY = 0
|
||||
s.dataRowCounter = 0
|
||||
s.vMergeTrack = make(map[int]int)
|
||||
s.numVisualRowsDrawn = 0
|
||||
}
|
||||
|
||||
// Row buffers a row line for SVG rendering.
|
||||
// Parameters include w (w), rowLine (cells), and ctx (formatting).
|
||||
// No return value; stores data for later rendering.
|
||||
func (s *SVG) Row(rowLine []string, ctx tw.Formatting) {
|
||||
s.debug("Buffering row line, IsSubRow: %v", ctx.IsSubRow)
|
||||
s.storeVisualLine(sectionTypeRow, rowLine, ctx)
|
||||
}
|
||||
|
||||
func (s *SVG) Logger(logger *ll.Logger) {
|
||||
s.logger = logger.Namespace("svg")
|
||||
}
|
||||
|
||||
// Start initializes SVG rendering.
|
||||
// Parameter w is the output w.
|
||||
// Returns nil; prepares internal state.
|
||||
func (s *SVG) Start(w io.Writer) error {
|
||||
s.w = w
|
||||
s.debug("Starting SVG rendering")
|
||||
s.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// debug logs a message if debugging is enabled.
|
||||
// Parameters include format string and variadic arguments.
|
||||
// No return value; appends to trace.
|
||||
func (s *SVG) debug(format string, a ...interface{}) {
|
||||
if s.config.Debug {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
s.trace = append(s.trace, fmt.Sprintf("[SVG] %s", msg))
|
||||
}
|
||||
}
|
||||
|
||||
// storeVisualLine stores a visual line for rendering.
|
||||
// Parameters include sectionIdx, lineData (cells), and ctx (formatting).
|
||||
// No return value; buffers data and context.
|
||||
func (s *SVG) storeVisualLine(sectionIdx int, lineData []string, ctx tw.Formatting) {
|
||||
copiedLineData := make([]string, len(lineData))
|
||||
copy(copiedLineData, lineData)
|
||||
s.allVisualLineData[sectionIdx] = append(s.allVisualLineData[sectionIdx], copiedLineData)
|
||||
s.allVisualLineCtx[sectionIdx] = append(s.allVisualLineCtx[sectionIdx], ctx)
|
||||
hasCurrent := ctx.Row.Current != nil
|
||||
s.debug("Stored line in section %d, has context: %v", sectionIdx, hasCurrent)
|
||||
}
|
||||
1163
vendor/github.com/olekukonko/tablewriter/stream.go
generated
vendored
Normal file
1163
vendor/github.com/olekukonko/tablewriter/stream.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
967
vendor/github.com/olekukonko/tablewriter/table.go
generated
vendored
967
vendor/github.com/olekukonko/tablewriter/table.go
generated
vendored
@@ -1,967 +0,0 @@
|
||||
// Copyright 2014 Oleku Konko All rights reserved.
|
||||
// Use of this source code is governed by a MIT
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This module is a Table Writer API for the Go Programming Language.
|
||||
// The protocols were written in pure Go and works on windows and unix systems
|
||||
|
||||
// Create & Generate text based table
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_ROW_WIDTH = 30
|
||||
)
|
||||
|
||||
const (
|
||||
CENTER = "+"
|
||||
ROW = "-"
|
||||
COLUMN = "|"
|
||||
SPACE = " "
|
||||
NEWLINE = "\n"
|
||||
)
|
||||
|
||||
const (
|
||||
ALIGN_DEFAULT = iota
|
||||
ALIGN_CENTER
|
||||
ALIGN_RIGHT
|
||||
ALIGN_LEFT
|
||||
)
|
||||
|
||||
var (
|
||||
decimal = regexp.MustCompile(`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$`)
|
||||
percent = regexp.MustCompile(`^-?\d+\.?\d*$%$`)
|
||||
)
|
||||
|
||||
type Border struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Top bool
|
||||
Bottom bool
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
out io.Writer
|
||||
rows [][]string
|
||||
lines [][][]string
|
||||
cs map[int]int
|
||||
rs map[int]int
|
||||
headers [][]string
|
||||
footers [][]string
|
||||
caption bool
|
||||
captionText string
|
||||
autoFmt bool
|
||||
autoWrap bool
|
||||
reflowText bool
|
||||
mW int
|
||||
pCenter string
|
||||
pRow string
|
||||
pColumn string
|
||||
tColumn int
|
||||
tRow int
|
||||
hAlign int
|
||||
fAlign int
|
||||
align int
|
||||
newLine string
|
||||
rowLine bool
|
||||
autoMergeCells bool
|
||||
columnsToAutoMergeCells map[int]bool
|
||||
noWhiteSpace bool
|
||||
tablePadding string
|
||||
hdrLine bool
|
||||
borders Border
|
||||
colSize int
|
||||
headerParams []string
|
||||
columnsParams []string
|
||||
footerParams []string
|
||||
columnsAlign []int
|
||||
}
|
||||
|
||||
// Start New Table
|
||||
// Take io.Writer Directly
|
||||
func NewWriter(writer io.Writer) *Table {
|
||||
t := &Table{
|
||||
out: writer,
|
||||
rows: [][]string{},
|
||||
lines: [][][]string{},
|
||||
cs: make(map[int]int),
|
||||
rs: make(map[int]int),
|
||||
headers: [][]string{},
|
||||
footers: [][]string{},
|
||||
caption: false,
|
||||
captionText: "Table caption.",
|
||||
autoFmt: true,
|
||||
autoWrap: true,
|
||||
reflowText: true,
|
||||
mW: MAX_ROW_WIDTH,
|
||||
pCenter: CENTER,
|
||||
pRow: ROW,
|
||||
pColumn: COLUMN,
|
||||
tColumn: -1,
|
||||
tRow: -1,
|
||||
hAlign: ALIGN_DEFAULT,
|
||||
fAlign: ALIGN_DEFAULT,
|
||||
align: ALIGN_DEFAULT,
|
||||
newLine: NEWLINE,
|
||||
rowLine: false,
|
||||
hdrLine: true,
|
||||
borders: Border{Left: true, Right: true, Bottom: true, Top: true},
|
||||
colSize: -1,
|
||||
headerParams: []string{},
|
||||
columnsParams: []string{},
|
||||
footerParams: []string{},
|
||||
columnsAlign: []int{}}
|
||||
return t
|
||||
}
|
||||
|
||||
// Render table output
|
||||
func (t *Table) Render() {
|
||||
if t.borders.Top {
|
||||
t.printLine(true)
|
||||
}
|
||||
t.printHeading()
|
||||
if t.autoMergeCells {
|
||||
t.printRowsMergeCells()
|
||||
} else {
|
||||
t.printRows()
|
||||
}
|
||||
if !t.rowLine && t.borders.Bottom {
|
||||
t.printLine(true)
|
||||
}
|
||||
t.printFooter()
|
||||
|
||||
if t.caption {
|
||||
t.printCaption()
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
headerRowIdx = -1
|
||||
footerRowIdx = -2
|
||||
)
|
||||
|
||||
// Set table header
|
||||
func (t *Table) SetHeader(keys []string) {
|
||||
t.colSize = len(keys)
|
||||
for i, v := range keys {
|
||||
lines := t.parseDimension(v, i, headerRowIdx)
|
||||
t.headers = append(t.headers, lines)
|
||||
}
|
||||
}
|
||||
|
||||
// Set table Footer
|
||||
func (t *Table) SetFooter(keys []string) {
|
||||
//t.colSize = len(keys)
|
||||
for i, v := range keys {
|
||||
lines := t.parseDimension(v, i, footerRowIdx)
|
||||
t.footers = append(t.footers, lines)
|
||||
}
|
||||
}
|
||||
|
||||
// Set table Caption
|
||||
func (t *Table) SetCaption(caption bool, captionText ...string) {
|
||||
t.caption = caption
|
||||
if len(captionText) == 1 {
|
||||
t.captionText = captionText[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Turn header autoformatting on/off. Default is on (true).
|
||||
func (t *Table) SetAutoFormatHeaders(auto bool) {
|
||||
t.autoFmt = auto
|
||||
}
|
||||
|
||||
// Turn automatic multiline text adjustment on/off. Default is on (true).
|
||||
func (t *Table) SetAutoWrapText(auto bool) {
|
||||
t.autoWrap = auto
|
||||
}
|
||||
|
||||
// Turn automatic reflowing of multiline text when rewrapping. Default is on (true).
|
||||
func (t *Table) SetReflowDuringAutoWrap(auto bool) {
|
||||
t.reflowText = auto
|
||||
}
|
||||
|
||||
// Set the Default column width
|
||||
func (t *Table) SetColWidth(width int) {
|
||||
t.mW = width
|
||||
}
|
||||
|
||||
// Set the minimal width for a column
|
||||
func (t *Table) SetColMinWidth(column int, width int) {
|
||||
t.cs[column] = width
|
||||
}
|
||||
|
||||
// Set the Column Separator
|
||||
func (t *Table) SetColumnSeparator(sep string) {
|
||||
t.pColumn = sep
|
||||
}
|
||||
|
||||
// Set the Row Separator
|
||||
func (t *Table) SetRowSeparator(sep string) {
|
||||
t.pRow = sep
|
||||
}
|
||||
|
||||
// Set the center Separator
|
||||
func (t *Table) SetCenterSeparator(sep string) {
|
||||
t.pCenter = sep
|
||||
}
|
||||
|
||||
// Set Header Alignment
|
||||
func (t *Table) SetHeaderAlignment(hAlign int) {
|
||||
t.hAlign = hAlign
|
||||
}
|
||||
|
||||
// Set Footer Alignment
|
||||
func (t *Table) SetFooterAlignment(fAlign int) {
|
||||
t.fAlign = fAlign
|
||||
}
|
||||
|
||||
// Set Table Alignment
|
||||
func (t *Table) SetAlignment(align int) {
|
||||
t.align = align
|
||||
}
|
||||
|
||||
// Set No White Space
|
||||
func (t *Table) SetNoWhiteSpace(allow bool) {
|
||||
t.noWhiteSpace = allow
|
||||
}
|
||||
|
||||
// Set Table Padding
|
||||
func (t *Table) SetTablePadding(padding string) {
|
||||
t.tablePadding = padding
|
||||
}
|
||||
|
||||
func (t *Table) SetColumnAlignment(keys []int) {
|
||||
for _, v := range keys {
|
||||
switch v {
|
||||
case ALIGN_CENTER:
|
||||
break
|
||||
case ALIGN_LEFT:
|
||||
break
|
||||
case ALIGN_RIGHT:
|
||||
break
|
||||
default:
|
||||
v = ALIGN_DEFAULT
|
||||
}
|
||||
t.columnsAlign = append(t.columnsAlign, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set New Line
|
||||
func (t *Table) SetNewLine(nl string) {
|
||||
t.newLine = nl
|
||||
}
|
||||
|
||||
// Set Header Line
|
||||
// This would enable / disable a line after the header
|
||||
func (t *Table) SetHeaderLine(line bool) {
|
||||
t.hdrLine = line
|
||||
}
|
||||
|
||||
// Set Row Line
|
||||
// This would enable / disable a line on each row of the table
|
||||
func (t *Table) SetRowLine(line bool) {
|
||||
t.rowLine = line
|
||||
}
|
||||
|
||||
// Set Auto Merge Cells
|
||||
// This would enable / disable the merge of cells with identical values
|
||||
func (t *Table) SetAutoMergeCells(auto bool) {
|
||||
t.autoMergeCells = auto
|
||||
}
|
||||
|
||||
// Set Auto Merge Cells By Column Index
|
||||
// This would enable / disable the merge of cells with identical values for specific columns
|
||||
// If cols is empty, it is the same as `SetAutoMergeCells(true)`.
|
||||
func (t *Table) SetAutoMergeCellsByColumnIndex(cols []int) {
|
||||
t.autoMergeCells = true
|
||||
|
||||
if len(cols) > 0 {
|
||||
m := make(map[int]bool)
|
||||
for _, col := range cols {
|
||||
m[col] = true
|
||||
}
|
||||
t.columnsToAutoMergeCells = m
|
||||
}
|
||||
}
|
||||
|
||||
// Set Table Border
|
||||
// This would enable / disable line around the table
|
||||
func (t *Table) SetBorder(border bool) {
|
||||
t.SetBorders(Border{border, border, border, border})
|
||||
}
|
||||
|
||||
func (t *Table) SetBorders(border Border) {
|
||||
t.borders = border
|
||||
}
|
||||
|
||||
// Append row to table
|
||||
func (t *Table) Append(row []string) {
|
||||
rowSize := len(t.headers)
|
||||
if rowSize > t.colSize {
|
||||
t.colSize = rowSize
|
||||
}
|
||||
|
||||
n := len(t.lines)
|
||||
line := [][]string{}
|
||||
for i, v := range row {
|
||||
|
||||
// Detect string width
|
||||
// Detect String height
|
||||
// Break strings into words
|
||||
out := t.parseDimension(v, i, n)
|
||||
|
||||
// Append broken words
|
||||
line = append(line, out)
|
||||
}
|
||||
t.lines = append(t.lines, line)
|
||||
}
|
||||
|
||||
// Append row to table with color attributes
|
||||
func (t *Table) Rich(row []string, colors []Colors) {
|
||||
rowSize := len(t.headers)
|
||||
if rowSize > t.colSize {
|
||||
t.colSize = rowSize
|
||||
}
|
||||
|
||||
n := len(t.lines)
|
||||
line := [][]string{}
|
||||
for i, v := range row {
|
||||
|
||||
// Detect string width
|
||||
// Detect String height
|
||||
// Break strings into words
|
||||
out := t.parseDimension(v, i, n)
|
||||
|
||||
if len(colors) > i {
|
||||
color := colors[i]
|
||||
out[0] = format(out[0], color)
|
||||
}
|
||||
|
||||
// Append broken words
|
||||
line = append(line, out)
|
||||
}
|
||||
t.lines = append(t.lines, line)
|
||||
}
|
||||
|
||||
// Allow Support for Bulk Append
|
||||
// Eliminates repeated for loops
|
||||
func (t *Table) AppendBulk(rows [][]string) {
|
||||
for _, row := range rows {
|
||||
t.Append(row)
|
||||
}
|
||||
}
|
||||
|
||||
// NumLines to get the number of lines
|
||||
func (t *Table) NumLines() int {
|
||||
return len(t.lines)
|
||||
}
|
||||
|
||||
// Clear rows
|
||||
func (t *Table) ClearRows() {
|
||||
t.lines = [][][]string{}
|
||||
}
|
||||
|
||||
// Clear footer
|
||||
func (t *Table) ClearFooter() {
|
||||
t.footers = [][]string{}
|
||||
}
|
||||
|
||||
// Center based on position and border.
|
||||
func (t *Table) center(i int) string {
|
||||
if i == -1 && !t.borders.Left {
|
||||
return t.pRow
|
||||
}
|
||||
|
||||
if i == len(t.cs)-1 && !t.borders.Right {
|
||||
return t.pRow
|
||||
}
|
||||
|
||||
return t.pCenter
|
||||
}
|
||||
|
||||
// Print line based on row width
|
||||
func (t *Table) printLine(nl bool) {
|
||||
fmt.Fprint(t.out, t.center(-1))
|
||||
for i := 0; i < len(t.cs); i++ {
|
||||
v := t.cs[i]
|
||||
fmt.Fprintf(t.out, "%s%s%s%s",
|
||||
t.pRow,
|
||||
strings.Repeat(string(t.pRow), v),
|
||||
t.pRow,
|
||||
t.center(i))
|
||||
}
|
||||
if nl {
|
||||
fmt.Fprint(t.out, t.newLine)
|
||||
}
|
||||
}
|
||||
|
||||
// Print line based on row width with our without cell separator
|
||||
func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) {
|
||||
fmt.Fprint(t.out, t.pCenter)
|
||||
for i := 0; i < len(t.cs); i++ {
|
||||
v := t.cs[i]
|
||||
if i > len(displayCellSeparator) || displayCellSeparator[i] {
|
||||
// Display the cell separator
|
||||
fmt.Fprintf(t.out, "%s%s%s%s",
|
||||
t.pRow,
|
||||
strings.Repeat(string(t.pRow), v),
|
||||
t.pRow,
|
||||
t.pCenter)
|
||||
} else {
|
||||
// Don't display the cell separator for this cell
|
||||
fmt.Fprintf(t.out, "%s%s",
|
||||
strings.Repeat(" ", v+2),
|
||||
t.pCenter)
|
||||
}
|
||||
}
|
||||
if nl {
|
||||
fmt.Fprint(t.out, t.newLine)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the PadRight function if align is left, PadLeft if align is right,
|
||||
// and Pad by default
|
||||
func pad(align int) func(string, string, int) string {
|
||||
padFunc := Pad
|
||||
switch align {
|
||||
case ALIGN_LEFT:
|
||||
padFunc = PadRight
|
||||
case ALIGN_RIGHT:
|
||||
padFunc = PadLeft
|
||||
}
|
||||
return padFunc
|
||||
}
|
||||
|
||||
// Print heading information
|
||||
func (t *Table) printHeading() {
|
||||
// Check if headers is available
|
||||
if len(t.headers) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// Identify last column
|
||||
end := len(t.cs) - 1
|
||||
|
||||
// Get pad function
|
||||
padFunc := pad(t.hAlign)
|
||||
|
||||
// Checking for ANSI escape sequences for header
|
||||
is_esc_seq := false
|
||||
if len(t.headerParams) > 0 {
|
||||
is_esc_seq = true
|
||||
}
|
||||
|
||||
// Maximum height.
|
||||
max := t.rs[headerRowIdx]
|
||||
|
||||
// Print Heading
|
||||
for x := 0; x < max; x++ {
|
||||
// Check if border is set
|
||||
// Replace with space if not set
|
||||
if !t.noWhiteSpace {
|
||||
fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
|
||||
}
|
||||
|
||||
for y := 0; y <= end; y++ {
|
||||
v := t.cs[y]
|
||||
h := ""
|
||||
|
||||
if y < len(t.headers) && x < len(t.headers[y]) {
|
||||
h = t.headers[y][x]
|
||||
}
|
||||
if t.autoFmt {
|
||||
h = Title(h)
|
||||
}
|
||||
pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn)
|
||||
if t.noWhiteSpace {
|
||||
pad = ConditionString((y == end && !t.borders.Left), SPACE, t.tablePadding)
|
||||
}
|
||||
if is_esc_seq {
|
||||
if !t.noWhiteSpace {
|
||||
fmt.Fprintf(t.out, " %s %s",
|
||||
format(padFunc(h, SPACE, v),
|
||||
t.headerParams[y]), pad)
|
||||
} else {
|
||||
fmt.Fprintf(t.out, "%s %s",
|
||||
format(padFunc(h, SPACE, v),
|
||||
t.headerParams[y]), pad)
|
||||
}
|
||||
} else {
|
||||
if !t.noWhiteSpace {
|
||||
fmt.Fprintf(t.out, " %s %s",
|
||||
padFunc(h, SPACE, v),
|
||||
pad)
|
||||
} else {
|
||||
// the spaces between breaks the kube formatting
|
||||
fmt.Fprintf(t.out, "%s%s",
|
||||
padFunc(h, SPACE, v),
|
||||
pad)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Next line
|
||||
fmt.Fprint(t.out, t.newLine)
|
||||
}
|
||||
if t.hdrLine {
|
||||
t.printLine(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Print heading information
|
||||
func (t *Table) printFooter() {
|
||||
// Check if headers is available
|
||||
if len(t.footers) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// Only print line if border is not set
|
||||
if !t.borders.Bottom {
|
||||
t.printLine(true)
|
||||
}
|
||||
|
||||
// Identify last column
|
||||
end := len(t.cs) - 1
|
||||
|
||||
// Get pad function
|
||||
padFunc := pad(t.fAlign)
|
||||
|
||||
// Checking for ANSI escape sequences for header
|
||||
is_esc_seq := false
|
||||
if len(t.footerParams) > 0 {
|
||||
is_esc_seq = true
|
||||
}
|
||||
|
||||
// Maximum height.
|
||||
max := t.rs[footerRowIdx]
|
||||
|
||||
// Print Footer
|
||||
erasePad := make([]bool, len(t.footers))
|
||||
for x := 0; x < max; x++ {
|
||||
// Check if border is set
|
||||
// Replace with space if not set
|
||||
fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE))
|
||||
|
||||
for y := 0; y <= end; y++ {
|
||||
v := t.cs[y]
|
||||
f := ""
|
||||
if y < len(t.footers) && x < len(t.footers[y]) {
|
||||
f = t.footers[y][x]
|
||||
}
|
||||
if t.autoFmt {
|
||||
f = Title(f)
|
||||
}
|
||||
pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn)
|
||||
|
||||
if erasePad[y] || (x == 0 && len(f) == 0) {
|
||||
pad = SPACE
|
||||
erasePad[y] = true
|
||||
}
|
||||
|
||||
if is_esc_seq {
|
||||
fmt.Fprintf(t.out, " %s %s",
|
||||
format(padFunc(f, SPACE, v),
|
||||
t.footerParams[y]), pad)
|
||||
} else {
|
||||
fmt.Fprintf(t.out, " %s %s",
|
||||
padFunc(f, SPACE, v),
|
||||
pad)
|
||||
}
|
||||
|
||||
//fmt.Fprintf(t.out, " %s %s",
|
||||
// padFunc(f, SPACE, v),
|
||||
// pad)
|
||||
}
|
||||
// Next line
|
||||
fmt.Fprint(t.out, t.newLine)
|
||||
//t.printLine(true)
|
||||
}
|
||||
|
||||
hasPrinted := false
|
||||
|
||||
for i := 0; i <= end; i++ {
|
||||
v := t.cs[i]
|
||||
pad := t.pRow
|
||||
center := t.pCenter
|
||||
length := len(t.footers[i][0])
|
||||
|
||||
if length > 0 {
|
||||
hasPrinted = true
|
||||
}
|
||||
|
||||
// Set center to be space if length is 0
|
||||
if length == 0 && !t.borders.Right {
|
||||
center = SPACE
|
||||
}
|
||||
|
||||
// Print first junction
|
||||
if i == 0 {
|
||||
if length > 0 && !t.borders.Left {
|
||||
center = t.pRow
|
||||
}
|
||||
fmt.Fprint(t.out, center)
|
||||
}
|
||||
|
||||
// Pad With space of length is 0
|
||||
if length == 0 {
|
||||
pad = SPACE
|
||||
}
|
||||
// Ignore left space as it has printed before
|
||||
if hasPrinted || t.borders.Left {
|
||||
pad = t.pRow
|
||||
center = t.pCenter
|
||||
}
|
||||
|
||||
// Change Center end position
|
||||
if center != SPACE {
|
||||
if i == end && !t.borders.Right {
|
||||
center = t.pRow
|
||||
}
|
||||
}
|
||||
|
||||
// Change Center start position
|
||||
if center == SPACE {
|
||||
if i < end && len(t.footers[i+1][0]) != 0 {
|
||||
if !t.borders.Left {
|
||||
center = t.pRow
|
||||
} else {
|
||||
center = t.pCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print the footer
|
||||
fmt.Fprintf(t.out, "%s%s%s%s",
|
||||
pad,
|
||||
strings.Repeat(string(pad), v),
|
||||
pad,
|
||||
center)
|
||||
|
||||
}
|
||||
|
||||
fmt.Fprint(t.out, t.newLine)
|
||||
}
|
||||
|
||||
// Print caption text
|
||||
func (t Table) printCaption() {
|
||||
width := t.getTableWidth()
|
||||
paragraph, _ := WrapString(t.captionText, width)
|
||||
for linecount := 0; linecount < len(paragraph); linecount++ {
|
||||
fmt.Fprintln(t.out, paragraph[linecount])
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the total number of characters in a row
|
||||
func (t Table) getTableWidth() int {
|
||||
var chars int
|
||||
for _, v := range t.cs {
|
||||
chars += v
|
||||
}
|
||||
|
||||
// Add chars, spaces, seperators to calculate the total width of the table.
|
||||
// ncols := t.colSize
|
||||
// spaces := ncols * 2
|
||||
// seps := ncols + 1
|
||||
|
||||
return (chars + (3 * t.colSize) + 2)
|
||||
}
|
||||
|
||||
func (t Table) printRows() {
|
||||
for i, lines := range t.lines {
|
||||
t.printRow(lines, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) fillAlignment(num int) {
|
||||
if len(t.columnsAlign) < num {
|
||||
t.columnsAlign = make([]int, num)
|
||||
for i := range t.columnsAlign {
|
||||
t.columnsAlign[i] = t.align
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print Row Information
|
||||
// Adjust column alignment based on type
|
||||
|
||||
func (t *Table) printRow(columns [][]string, rowIdx int) {
|
||||
// Get Maximum Height
|
||||
max := t.rs[rowIdx]
|
||||
total := len(columns)
|
||||
|
||||
// TODO Fix uneven col size
|
||||
// if total < t.colSize {
|
||||
// for n := t.colSize - total; n < t.colSize ; n++ {
|
||||
// columns = append(columns, []string{SPACE})
|
||||
// t.cs[n] = t.mW
|
||||
// }
|
||||
//}
|
||||
|
||||
// Pad Each Height
|
||||
pads := []int{}
|
||||
|
||||
// Checking for ANSI escape sequences for columns
|
||||
is_esc_seq := false
|
||||
if len(t.columnsParams) > 0 {
|
||||
is_esc_seq = true
|
||||
}
|
||||
t.fillAlignment(total)
|
||||
|
||||
for i, line := range columns {
|
||||
length := len(line)
|
||||
pad := max - length
|
||||
pads = append(pads, pad)
|
||||
for n := 0; n < pad; n++ {
|
||||
columns[i] = append(columns[i], " ")
|
||||
}
|
||||
}
|
||||
//fmt.Println(max, "\n")
|
||||
for x := 0; x < max; x++ {
|
||||
for y := 0; y < total; y++ {
|
||||
|
||||
// Check if border is set
|
||||
if !t.noWhiteSpace {
|
||||
fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
|
||||
fmt.Fprintf(t.out, SPACE)
|
||||
}
|
||||
|
||||
str := columns[y][x]
|
||||
|
||||
// Embedding escape sequence with column value
|
||||
if is_esc_seq {
|
||||
str = format(str, t.columnsParams[y])
|
||||
}
|
||||
|
||||
// This would print alignment
|
||||
// Default alignment would use multiple configuration
|
||||
switch t.columnsAlign[y] {
|
||||
case ALIGN_CENTER: //
|
||||
fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
|
||||
case ALIGN_RIGHT:
|
||||
fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
|
||||
case ALIGN_LEFT:
|
||||
fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
|
||||
default:
|
||||
if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
|
||||
fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y]))
|
||||
} else {
|
||||
fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
|
||||
|
||||
// TODO Custom alignment per column
|
||||
//if max == 1 || pads[y] > 0 {
|
||||
// fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y]))
|
||||
//} else {
|
||||
// fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y]))
|
||||
//}
|
||||
|
||||
}
|
||||
}
|
||||
if !t.noWhiteSpace {
|
||||
fmt.Fprintf(t.out, SPACE)
|
||||
} else {
|
||||
fmt.Fprintf(t.out, t.tablePadding)
|
||||
}
|
||||
}
|
||||
// Check if border is set
|
||||
// Replace with space if not set
|
||||
if !t.noWhiteSpace {
|
||||
fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE))
|
||||
}
|
||||
fmt.Fprint(t.out, t.newLine)
|
||||
}
|
||||
|
||||
if t.rowLine {
|
||||
t.printLine(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Print the rows of the table and merge the cells that are identical
|
||||
func (t *Table) printRowsMergeCells() {
|
||||
var previousLine []string
|
||||
var displayCellBorder []bool
|
||||
var tmpWriter bytes.Buffer
|
||||
for i, lines := range t.lines {
|
||||
// We store the display of the current line in a tmp writer, as we need to know which border needs to be print above
|
||||
previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine)
|
||||
if i > 0 { //We don't need to print borders above first line
|
||||
if t.rowLine {
|
||||
t.printLineOptionalCellSeparators(true, displayCellBorder)
|
||||
}
|
||||
}
|
||||
tmpWriter.WriteTo(t.out)
|
||||
}
|
||||
//Print the end of the table
|
||||
if t.rowLine {
|
||||
t.printLine(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Print Row Information to a writer and merge identical cells.
|
||||
// Adjust column alignment based on type
|
||||
|
||||
func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) {
|
||||
// Get Maximum Height
|
||||
max := t.rs[rowIdx]
|
||||
total := len(columns)
|
||||
|
||||
// Pad Each Height
|
||||
pads := []int{}
|
||||
|
||||
// Checking for ANSI escape sequences for columns
|
||||
is_esc_seq := false
|
||||
if len(t.columnsParams) > 0 {
|
||||
is_esc_seq = true
|
||||
}
|
||||
for i, line := range columns {
|
||||
length := len(line)
|
||||
pad := max - length
|
||||
pads = append(pads, pad)
|
||||
for n := 0; n < pad; n++ {
|
||||
columns[i] = append(columns[i], " ")
|
||||
}
|
||||
}
|
||||
|
||||
var displayCellBorder []bool
|
||||
t.fillAlignment(total)
|
||||
for x := 0; x < max; x++ {
|
||||
for y := 0; y < total; y++ {
|
||||
|
||||
// Check if border is set
|
||||
fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn))
|
||||
|
||||
fmt.Fprintf(writer, SPACE)
|
||||
|
||||
str := columns[y][x]
|
||||
|
||||
// Embedding escape sequence with column value
|
||||
if is_esc_seq {
|
||||
str = format(str, t.columnsParams[y])
|
||||
}
|
||||
|
||||
if t.autoMergeCells {
|
||||
var mergeCell bool
|
||||
if t.columnsToAutoMergeCells != nil {
|
||||
// Check to see if the column index is in columnsToAutoMergeCells.
|
||||
if t.columnsToAutoMergeCells[y] {
|
||||
mergeCell = true
|
||||
}
|
||||
} else {
|
||||
// columnsToAutoMergeCells was not set.
|
||||
mergeCell = true
|
||||
}
|
||||
//Store the full line to merge mutli-lines cells
|
||||
fullLine := strings.TrimRight(strings.Join(columns[y], " "), " ")
|
||||
if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" && mergeCell {
|
||||
// If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty.
|
||||
displayCellBorder = append(displayCellBorder, false)
|
||||
str = ""
|
||||
} else {
|
||||
// First line or different content, keep the content and print the cell border
|
||||
displayCellBorder = append(displayCellBorder, true)
|
||||
}
|
||||
}
|
||||
|
||||
// This would print alignment
|
||||
// Default alignment would use multiple configuration
|
||||
switch t.columnsAlign[y] {
|
||||
case ALIGN_CENTER: //
|
||||
fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y]))
|
||||
case ALIGN_RIGHT:
|
||||
fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
|
||||
case ALIGN_LEFT:
|
||||
fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
|
||||
default:
|
||||
if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) {
|
||||
fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y]))
|
||||
} else {
|
||||
fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y]))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(writer, SPACE)
|
||||
}
|
||||
// Check if border is set
|
||||
// Replace with space if not set
|
||||
fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE))
|
||||
fmt.Fprint(writer, t.newLine)
|
||||
}
|
||||
|
||||
//The new previous line is the current one
|
||||
previousLine = make([]string, total)
|
||||
for y := 0; y < total; y++ {
|
||||
previousLine[y] = strings.TrimRight(strings.Join(columns[y], " "), " ") //Store the full line for multi-lines cells
|
||||
}
|
||||
//Returns the newly added line and wether or not a border should be displayed above.
|
||||
return previousLine, displayCellBorder
|
||||
}
|
||||
|
||||
func (t *Table) parseDimension(str string, colKey, rowKey int) []string {
|
||||
var (
|
||||
raw []string
|
||||
maxWidth int
|
||||
)
|
||||
|
||||
raw = getLines(str)
|
||||
maxWidth = 0
|
||||
for _, line := range raw {
|
||||
if w := DisplayWidth(line); w > maxWidth {
|
||||
maxWidth = w
|
||||
}
|
||||
}
|
||||
|
||||
// If wrapping, ensure that all paragraphs in the cell fit in the
|
||||
// specified width.
|
||||
if t.autoWrap {
|
||||
// If there's a maximum allowed width for wrapping, use that.
|
||||
if maxWidth > t.mW {
|
||||
maxWidth = t.mW
|
||||
}
|
||||
|
||||
// In the process of doing so, we need to recompute maxWidth. This
|
||||
// is because perhaps a word in the cell is longer than the
|
||||
// allowed maximum width in t.mW.
|
||||
newMaxWidth := maxWidth
|
||||
newRaw := make([]string, 0, len(raw))
|
||||
|
||||
if t.reflowText {
|
||||
// Make a single paragraph of everything.
|
||||
raw = []string{strings.Join(raw, " ")}
|
||||
}
|
||||
for i, para := range raw {
|
||||
paraLines, _ := WrapString(para, maxWidth)
|
||||
for _, line := range paraLines {
|
||||
if w := DisplayWidth(line); w > newMaxWidth {
|
||||
newMaxWidth = w
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
newRaw = append(newRaw, " ")
|
||||
}
|
||||
newRaw = append(newRaw, paraLines...)
|
||||
}
|
||||
raw = newRaw
|
||||
maxWidth = newMaxWidth
|
||||
}
|
||||
|
||||
// Store the new known maximum width.
|
||||
v, ok := t.cs[colKey]
|
||||
if !ok || v < maxWidth || v == 0 {
|
||||
t.cs[colKey] = maxWidth
|
||||
}
|
||||
|
||||
// Remember the number of lines for the row printer.
|
||||
h := len(raw)
|
||||
v, ok = t.rs[rowKey]
|
||||
|
||||
if !ok || v < h || v == 0 {
|
||||
t.rs[rowKey] = h
|
||||
}
|
||||
//fmt.Printf("Raw %+v %d\n", raw, len(raw))
|
||||
return raw
|
||||
}
|
||||
136
vendor/github.com/olekukonko/tablewriter/table_with_color.go
generated
vendored
136
vendor/github.com/olekukonko/tablewriter/table_with_color.go
generated
vendored
@@ -1,136 +0,0 @@
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ESC = "\033"
|
||||
const SEP = ";"
|
||||
|
||||
const (
|
||||
BgBlackColor int = iota + 40
|
||||
BgRedColor
|
||||
BgGreenColor
|
||||
BgYellowColor
|
||||
BgBlueColor
|
||||
BgMagentaColor
|
||||
BgCyanColor
|
||||
BgWhiteColor
|
||||
)
|
||||
|
||||
const (
|
||||
FgBlackColor int = iota + 30
|
||||
FgRedColor
|
||||
FgGreenColor
|
||||
FgYellowColor
|
||||
FgBlueColor
|
||||
FgMagentaColor
|
||||
FgCyanColor
|
||||
FgWhiteColor
|
||||
)
|
||||
|
||||
const (
|
||||
BgHiBlackColor int = iota + 100
|
||||
BgHiRedColor
|
||||
BgHiGreenColor
|
||||
BgHiYellowColor
|
||||
BgHiBlueColor
|
||||
BgHiMagentaColor
|
||||
BgHiCyanColor
|
||||
BgHiWhiteColor
|
||||
)
|
||||
|
||||
const (
|
||||
FgHiBlackColor int = iota + 90
|
||||
FgHiRedColor
|
||||
FgHiGreenColor
|
||||
FgHiYellowColor
|
||||
FgHiBlueColor
|
||||
FgHiMagentaColor
|
||||
FgHiCyanColor
|
||||
FgHiWhiteColor
|
||||
)
|
||||
|
||||
const (
|
||||
Normal = 0
|
||||
Bold = 1
|
||||
UnderlineSingle = 4
|
||||
Italic
|
||||
)
|
||||
|
||||
type Colors []int
|
||||
|
||||
func startFormat(seq string) string {
|
||||
return fmt.Sprintf("%s[%sm", ESC, seq)
|
||||
}
|
||||
|
||||
func stopFormat() string {
|
||||
return fmt.Sprintf("%s[%dm", ESC, Normal)
|
||||
}
|
||||
|
||||
// Making the SGR (Select Graphic Rendition) sequence.
|
||||
func makeSequence(codes []int) string {
|
||||
codesInString := []string{}
|
||||
for _, code := range codes {
|
||||
codesInString = append(codesInString, strconv.Itoa(code))
|
||||
}
|
||||
return strings.Join(codesInString, SEP)
|
||||
}
|
||||
|
||||
// Adding ANSI escape sequences before and after string
|
||||
func format(s string, codes interface{}) string {
|
||||
var seq string
|
||||
|
||||
switch v := codes.(type) {
|
||||
|
||||
case string:
|
||||
seq = v
|
||||
case []int:
|
||||
seq = makeSequence(v)
|
||||
case Colors:
|
||||
seq = makeSequence(v)
|
||||
default:
|
||||
return s
|
||||
}
|
||||
|
||||
if len(seq) == 0 {
|
||||
return s
|
||||
}
|
||||
return startFormat(seq) + s + stopFormat()
|
||||
}
|
||||
|
||||
// Adding header colors (ANSI codes)
|
||||
func (t *Table) SetHeaderColor(colors ...Colors) {
|
||||
if t.colSize != len(colors) {
|
||||
panic("Number of header colors must be equal to number of headers.")
|
||||
}
|
||||
for i := 0; i < len(colors); i++ {
|
||||
t.headerParams = append(t.headerParams, makeSequence(colors[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// Adding column colors (ANSI codes)
|
||||
func (t *Table) SetColumnColor(colors ...Colors) {
|
||||
if t.colSize != len(colors) {
|
||||
panic("Number of column colors must be equal to number of headers.")
|
||||
}
|
||||
for i := 0; i < len(colors); i++ {
|
||||
t.columnsParams = append(t.columnsParams, makeSequence(colors[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// Adding column colors (ANSI codes)
|
||||
func (t *Table) SetFooterColor(colors ...Colors) {
|
||||
if len(t.footers) != len(colors) {
|
||||
panic("Number of footer colors must be equal to number of footer.")
|
||||
}
|
||||
for i := 0; i < len(colors); i++ {
|
||||
t.footerParams = append(t.footerParams, makeSequence(colors[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func Color(colors ...int) []int {
|
||||
return colors
|
||||
}
|
||||
2234
vendor/github.com/olekukonko/tablewriter/tablewriter.go
generated
vendored
Normal file
2234
vendor/github.com/olekukonko/tablewriter/tablewriter.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
64
vendor/github.com/olekukonko/tablewriter/tw/cell.go
generated
vendored
Normal file
64
vendor/github.com/olekukonko/tablewriter/tw/cell.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package tw
|
||||
|
||||
// CellFormatting holds formatting options for table cells.
|
||||
type CellFormatting struct {
|
||||
AutoWrap int // Wrapping behavior (e.g., WrapTruncate, WrapNormal)
|
||||
MergeMode int // Bitmask for merge behavior (e.g., MergeHorizontal, MergeVertical)
|
||||
|
||||
// Changed form bool to State
|
||||
// See https://github.com/olekukonko/tablewriter/issues/261
|
||||
AutoFormat State // Enables automatic formatting (e.g., title case for headers)
|
||||
|
||||
// Deprecated: kept for compatibility
|
||||
// will be removed soon
|
||||
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
|
||||
|
||||
}
|
||||
|
||||
// CellPadding defines padding settings for table cells.
|
||||
type CellPadding struct {
|
||||
Global Padding // Default padding applied to all cells
|
||||
PerColumn []Padding // Column-specific padding overrides
|
||||
}
|
||||
|
||||
// CellFilter defines filtering functions for cell content.
|
||||
type CellFilter struct {
|
||||
Global func([]string) []string // Processes the entire row
|
||||
PerColumn []func(string) string // Processes individual cells by column
|
||||
}
|
||||
|
||||
// CellCallbacks holds callback functions for cell processing.
|
||||
// Note: These are currently placeholders and not fully implemented.
|
||||
type CellCallbacks struct {
|
||||
Global func() // Global callback applied to all cells
|
||||
PerColumn []func() // Column-specific callbacks
|
||||
}
|
||||
|
||||
// CellAlignment defines alignment settings for table cells.
|
||||
type CellAlignment struct {
|
||||
Global Align // Default alignment applied to all cells
|
||||
PerColumn []Align // Column-specific alignment overrides
|
||||
}
|
||||
|
||||
// CellConfig combines formatting, padding, and callback settings for a table section.
|
||||
type CellConfig struct {
|
||||
Formatting CellFormatting // Cell formatting options
|
||||
Padding CellPadding // Padding configuration
|
||||
Callbacks CellCallbacks // Callback functions (unused)
|
||||
Filter CellFilter // Function to filter cell content (renamed from Filter Filter)
|
||||
Alignment CellAlignment // Alignment configuration for cells
|
||||
ColMaxWidths CellWidth // Per-column maximum width overrides
|
||||
|
||||
// Deprecated: use Alignment.PerColumn instead. Will be removed in a future version.
|
||||
// will be removed soon
|
||||
ColumnAligns []Align // Per-column alignment overrides
|
||||
}
|
||||
|
||||
type CellWidth struct {
|
||||
Global int
|
||||
PerColumn Mapper[int, int]
|
||||
}
|
||||
|
||||
func (c CellWidth) Constrained() bool {
|
||||
return c.Global > 0 || c.PerColumn.Len() > 0
|
||||
}
|
||||
137
vendor/github.com/olekukonko/tablewriter/tw/deprecated.go
generated
vendored
Normal file
137
vendor/github.com/olekukonko/tablewriter/tw/deprecated.go
generated
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
package tw
|
||||
|
||||
// Deprecated: SymbolASCII is deprecated; use Glyphs with StyleASCII instead.
|
||||
// this will be removed soon
|
||||
type SymbolASCII struct{}
|
||||
|
||||
// SymbolASCII symbol methods
|
||||
func (s *SymbolASCII) Name() string { return StyleNameASCII.String() }
|
||||
func (s *SymbolASCII) Center() string { return "+" }
|
||||
func (s *SymbolASCII) Row() string { return "-" }
|
||||
func (s *SymbolASCII) Column() string { return "|" }
|
||||
func (s *SymbolASCII) TopLeft() string { return "+" }
|
||||
func (s *SymbolASCII) TopMid() string { return "+" }
|
||||
func (s *SymbolASCII) TopRight() string { return "+" }
|
||||
func (s *SymbolASCII) MidLeft() string { return "+" }
|
||||
func (s *SymbolASCII) MidRight() string { return "+" }
|
||||
func (s *SymbolASCII) BottomLeft() string { return "+" }
|
||||
func (s *SymbolASCII) BottomMid() string { return "+" }
|
||||
func (s *SymbolASCII) BottomRight() string { return "+" }
|
||||
func (s *SymbolASCII) HeaderLeft() string { return "+" }
|
||||
func (s *SymbolASCII) HeaderMid() string { return "+" }
|
||||
func (s *SymbolASCII) HeaderRight() string { return "+" }
|
||||
|
||||
// Deprecated: SymbolUnicode is deprecated; use Glyphs with appropriate styles (e.g., StyleLight, StyleHeavy) instead.
|
||||
// this will be removed soon
|
||||
type SymbolUnicode struct {
|
||||
row string
|
||||
column string
|
||||
center string
|
||||
corners [9]string // [topLeft, topMid, topRight, midLeft, center, midRight, bottomLeft, bottomMid, bottomRight]
|
||||
}
|
||||
|
||||
// SymbolUnicode symbol methods
|
||||
func (s *SymbolUnicode) Name() string { return "unicode" }
|
||||
func (s *SymbolUnicode) Center() string { return s.center }
|
||||
func (s *SymbolUnicode) Row() string { return s.row }
|
||||
func (s *SymbolUnicode) Column() string { return s.column }
|
||||
func (s *SymbolUnicode) TopLeft() string { return s.corners[0] }
|
||||
func (s *SymbolUnicode) TopMid() string { return s.corners[1] }
|
||||
func (s *SymbolUnicode) TopRight() string { return s.corners[2] }
|
||||
func (s *SymbolUnicode) MidLeft() string { return s.corners[3] }
|
||||
func (s *SymbolUnicode) MidRight() string { return s.corners[5] }
|
||||
func (s *SymbolUnicode) BottomLeft() string { return s.corners[6] }
|
||||
func (s *SymbolUnicode) BottomMid() string { return s.corners[7] }
|
||||
func (s *SymbolUnicode) BottomRight() string { return s.corners[8] }
|
||||
func (s *SymbolUnicode) HeaderLeft() string { return s.MidLeft() }
|
||||
func (s *SymbolUnicode) HeaderMid() string { return s.Center() }
|
||||
func (s *SymbolUnicode) HeaderRight() string { return s.MidRight() }
|
||||
|
||||
// Deprecated: SymbolMarkdown is deprecated; use Glyphs with StyleMarkdown instead.
|
||||
// this will be removed soon
|
||||
type SymbolMarkdown struct{}
|
||||
|
||||
// SymbolMarkdown symbol methods
|
||||
func (s *SymbolMarkdown) Name() string { return StyleNameMarkdown.String() }
|
||||
func (s *SymbolMarkdown) Center() string { return "|" }
|
||||
func (s *SymbolMarkdown) Row() string { return "-" }
|
||||
func (s *SymbolMarkdown) Column() string { return "|" }
|
||||
func (s *SymbolMarkdown) TopLeft() string { return "" }
|
||||
func (s *SymbolMarkdown) TopMid() string { return "" }
|
||||
func (s *SymbolMarkdown) TopRight() string { return "" }
|
||||
func (s *SymbolMarkdown) MidLeft() string { return "|" }
|
||||
func (s *SymbolMarkdown) MidRight() string { return "|" }
|
||||
func (s *SymbolMarkdown) BottomLeft() string { return "" }
|
||||
func (s *SymbolMarkdown) BottomMid() string { return "" }
|
||||
func (s *SymbolMarkdown) BottomRight() string { return "" }
|
||||
func (s *SymbolMarkdown) HeaderLeft() string { return "|" }
|
||||
func (s *SymbolMarkdown) HeaderMid() string { return "|" }
|
||||
func (s *SymbolMarkdown) HeaderRight() string { return "|" }
|
||||
|
||||
// Deprecated: SymbolNothing is deprecated; use Glyphs with StyleNone instead.
|
||||
// this will be removed soon
|
||||
type SymbolNothing struct{}
|
||||
|
||||
// SymbolNothing symbol methods
|
||||
func (s *SymbolNothing) Name() string { return StyleNameNothing.String() }
|
||||
func (s *SymbolNothing) Center() string { return "" }
|
||||
func (s *SymbolNothing) Row() string { return "" }
|
||||
func (s *SymbolNothing) Column() string { return "" }
|
||||
func (s *SymbolNothing) TopLeft() string { return "" }
|
||||
func (s *SymbolNothing) TopMid() string { return "" }
|
||||
func (s *SymbolNothing) TopRight() string { return "" }
|
||||
func (s *SymbolNothing) MidLeft() string { return "" }
|
||||
func (s *SymbolNothing) MidRight() string { return "" }
|
||||
func (s *SymbolNothing) BottomLeft() string { return "" }
|
||||
func (s *SymbolNothing) BottomMid() string { return "" }
|
||||
func (s *SymbolNothing) BottomRight() string { return "" }
|
||||
func (s *SymbolNothing) HeaderLeft() string { return "" }
|
||||
func (s *SymbolNothing) HeaderMid() string { return "" }
|
||||
func (s *SymbolNothing) HeaderRight() string { return "" }
|
||||
|
||||
// Deprecated: SymbolGraphical is deprecated; use Glyphs with StyleGraphical instead.
|
||||
// this will be removed soon
|
||||
type SymbolGraphical struct{}
|
||||
|
||||
// SymbolGraphical symbol methods
|
||||
func (s *SymbolGraphical) Name() string { return StyleNameGraphical.String() }
|
||||
func (s *SymbolGraphical) Center() string { return "🟧" } // Orange square (matches mid junctions)
|
||||
func (s *SymbolGraphical) Row() string { return "🟥" } // Red square (matches corners)
|
||||
func (s *SymbolGraphical) Column() string { return "🟦" } // Blue square (vertical line)
|
||||
func (s *SymbolGraphical) TopLeft() string { return "🟥" } // Top-left corner
|
||||
func (s *SymbolGraphical) TopMid() string { return "🔳" } // Top junction
|
||||
func (s *SymbolGraphical) TopRight() string { return "🟥" } // Top-right corner
|
||||
func (s *SymbolGraphical) MidLeft() string { return "🟧" } // Left junction
|
||||
func (s *SymbolGraphical) MidRight() string { return "🟧" } // Right junction
|
||||
func (s *SymbolGraphical) BottomLeft() string { return "🟥" } // Bottom-left corner
|
||||
func (s *SymbolGraphical) BottomMid() string { return "🔳" } // Bottom junction
|
||||
func (s *SymbolGraphical) BottomRight() string { return "🟥" } // Bottom-right corner
|
||||
func (s *SymbolGraphical) HeaderLeft() string { return "🟧" } // Header left (matches mid junctions)
|
||||
func (s *SymbolGraphical) HeaderMid() string { return "🟧" } // Header middle (matches mid junctions)
|
||||
func (s *SymbolGraphical) HeaderRight() string { return "🟧" } // Header right (matches mid junctions)
|
||||
|
||||
// Deprecated: SymbolMerger is deprecated; use Glyphs with StyleMerger instead.
|
||||
// this will be removed soon
|
||||
type SymbolMerger struct {
|
||||
row string
|
||||
column string
|
||||
center string
|
||||
corners [9]string // [TL, TM, TR, ML, CenterIdx(unused), MR, BL, BM, BR]
|
||||
}
|
||||
|
||||
// SymbolMerger symbol methods
|
||||
func (s *SymbolMerger) Name() string { return StyleNameMerger.String() }
|
||||
func (s *SymbolMerger) Center() string { return s.center } // Main crossing symbol
|
||||
func (s *SymbolMerger) Row() string { return s.row }
|
||||
func (s *SymbolMerger) Column() string { return s.column }
|
||||
func (s *SymbolMerger) TopLeft() string { return s.corners[0] }
|
||||
func (s *SymbolMerger) TopMid() string { return s.corners[1] } // LevelHeader junction
|
||||
func (s *SymbolMerger) TopRight() string { return s.corners[2] }
|
||||
func (s *SymbolMerger) MidLeft() string { return s.corners[3] } // Left junction
|
||||
func (s *SymbolMerger) MidRight() string { return s.corners[5] } // Right junction
|
||||
func (s *SymbolMerger) BottomLeft() string { return s.corners[6] }
|
||||
func (s *SymbolMerger) BottomMid() string { return s.corners[7] } // LevelFooter junction
|
||||
func (s *SymbolMerger) BottomRight() string { return s.corners[8] }
|
||||
func (s *SymbolMerger) HeaderLeft() string { return s.MidLeft() }
|
||||
func (s *SymbolMerger) HeaderMid() string { return s.Center() }
|
||||
func (s *SymbolMerger) HeaderRight() string { return s.MidRight() }
|
||||
231
vendor/github.com/olekukonko/tablewriter/tw/fn.go
generated
vendored
Normal file
231
vendor/github.com/olekukonko/tablewriter/tw/fn.go
generated
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
// Package tw provides utility functions for text formatting, width calculation, and string manipulation
|
||||
// specifically tailored for table rendering, including handling ANSI escape codes and Unicode text.
|
||||
package tw
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/tablewriter/pkg/twwidth"
|
||||
"math" // For mathematical operations like ceiling
|
||||
"strconv" // For string-to-number conversions
|
||||
"strings" // For string manipulation utilities
|
||||
"unicode" // For Unicode character classification
|
||||
"unicode/utf8" // For UTF-8 rune handling
|
||||
)
|
||||
|
||||
// Title normalizes and uppercases a label string for use in headers.
|
||||
// It replaces underscores and certain dots with spaces and trims whitespace.
|
||||
func Title(name string) string {
|
||||
origLen := len(name)
|
||||
rs := []rune(name)
|
||||
for i, r := range rs {
|
||||
switch r {
|
||||
case '_':
|
||||
rs[i] = ' ' // Replace underscores with spaces
|
||||
case '.':
|
||||
// Replace dots with spaces unless they are between numeric or space characters
|
||||
if (i != 0 && !IsIsNumericOrSpace(rs[i-1])) || (i != len(rs)-1 && !IsIsNumericOrSpace(rs[i+1])) {
|
||||
rs[i] = ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
name = string(rs)
|
||||
name = strings.TrimSpace(name)
|
||||
// If the input was non-empty but trimmed to empty, return a single space
|
||||
if len(name) == 0 && origLen > 0 {
|
||||
name = " "
|
||||
}
|
||||
// Convert to uppercase for header formatting
|
||||
return strings.ToUpper(name)
|
||||
}
|
||||
|
||||
// PadCenter centers a string within a specified width using a padding character.
|
||||
// Extra padding is split between left and right, with slight preference to left if uneven.
|
||||
func PadCenter(s, pad string, width int) string {
|
||||
gap := width - twwidth.Width(s)
|
||||
if gap > 0 {
|
||||
// Calculate left and right padding; ceil ensures left gets extra if gap is odd
|
||||
gapLeft := int(math.Ceil(float64(gap) / 2))
|
||||
gapRight := gap - gapLeft
|
||||
return strings.Repeat(pad, gapLeft) + s + strings.Repeat(pad, gapRight)
|
||||
}
|
||||
// If no padding needed or string is too wide, return as is
|
||||
return s
|
||||
}
|
||||
|
||||
// PadRight left-aligns a string within a specified width, filling remaining space on the right with padding.
|
||||
func PadRight(s, pad string, width int) string {
|
||||
gap := width - twwidth.Width(s)
|
||||
if gap > 0 {
|
||||
// Append padding to the right
|
||||
return s + strings.Repeat(pad, gap)
|
||||
}
|
||||
// If no padding needed or string is too wide, return as is
|
||||
return s
|
||||
}
|
||||
|
||||
// PadLeft right-aligns a string within a specified width, filling remaining space on the left with padding.
|
||||
func PadLeft(s, pad string, width int) string {
|
||||
gap := width - twwidth.Width(s)
|
||||
if gap > 0 {
|
||||
// Prepend padding to the left
|
||||
return strings.Repeat(pad, gap) + s
|
||||
}
|
||||
// If no padding needed or string is too wide, return as is
|
||||
return s
|
||||
}
|
||||
|
||||
// Pad aligns a string within a specified width using a padding character.
|
||||
// It truncates if the string is wider than the target width.
|
||||
func Pad(s string, padChar string, totalWidth int, alignment Align) string {
|
||||
sDisplayWidth := twwidth.Width(s)
|
||||
if sDisplayWidth > totalWidth {
|
||||
return twwidth.Truncate(s, totalWidth) // Only truncate if necessary
|
||||
}
|
||||
switch alignment {
|
||||
case AlignLeft:
|
||||
return PadRight(s, padChar, totalWidth)
|
||||
case AlignRight:
|
||||
return PadLeft(s, padChar, totalWidth)
|
||||
case AlignCenter:
|
||||
return PadCenter(s, padChar, totalWidth)
|
||||
default:
|
||||
return PadRight(s, padChar, totalWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// IsIsNumericOrSpace checks if a rune is a digit or space character.
|
||||
// Used in formatting logic to determine safe character replacements.
|
||||
func IsIsNumericOrSpace(r rune) bool {
|
||||
return ('0' <= r && r <= '9') || r == ' '
|
||||
}
|
||||
|
||||
// IsNumeric checks if a string represents a valid integer or floating-point number.
|
||||
func IsNumeric(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
// Try parsing as integer first
|
||||
if _, err := strconv.Atoi(s); err == nil {
|
||||
return true
|
||||
}
|
||||
// Then try parsing as float
|
||||
_, err := strconv.ParseFloat(s, 64)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SplitCamelCase splits a camelCase or PascalCase or snake_case string into separate words.
|
||||
// It detects transitions between uppercase, lowercase, digits, and other characters.
|
||||
func SplitCamelCase(src string) (entries []string) {
|
||||
// Validate UTF-8 input; return as single entry if invalid
|
||||
if !utf8.ValidString(src) {
|
||||
return []string{src}
|
||||
}
|
||||
entries = []string{}
|
||||
var runes [][]rune
|
||||
lastClass := 0
|
||||
class := 0
|
||||
// Classify each rune into categories: lowercase (1), uppercase (2), digit (3), other (4)
|
||||
for _, r := range src {
|
||||
switch {
|
||||
case unicode.IsLower(r):
|
||||
class = 1
|
||||
case unicode.IsUpper(r):
|
||||
class = 2
|
||||
case unicode.IsDigit(r):
|
||||
class = 3
|
||||
default:
|
||||
class = 4
|
||||
}
|
||||
// Group consecutive runes of the same class together
|
||||
if class == lastClass {
|
||||
runes[len(runes)-1] = append(runes[len(runes)-1], r)
|
||||
} else {
|
||||
runes = append(runes, []rune{r})
|
||||
}
|
||||
lastClass = class
|
||||
}
|
||||
// Adjust for cases where an uppercase letter is followed by lowercase (e.g., CamelCase)
|
||||
for i := 0; i < len(runes)-1; i++ {
|
||||
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
|
||||
// Move the last uppercase rune to the next group for proper word splitting
|
||||
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
|
||||
runes[i] = runes[i][:len(runes[i])-1]
|
||||
}
|
||||
}
|
||||
// Convert rune groups to strings, excluding empty, underscore or whitespace-only groups
|
||||
for _, s := range runes {
|
||||
str := string(s)
|
||||
if len(s) > 0 && strings.TrimSpace(str) != "" && str != "_" {
|
||||
entries = append(entries, str)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Or provides a ternary-like operation for strings, returning 'valid' if cond is true, else 'inValid'.
|
||||
func Or(cond bool, valid, inValid string) string {
|
||||
if cond {
|
||||
return valid
|
||||
}
|
||||
return inValid
|
||||
}
|
||||
|
||||
// Max returns the greater of two integers.
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Min returns the smaller of two integers.
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// BreakPoint finds the rune index where the display width of a string first exceeds the specified limit.
|
||||
// It returns the number of runes if the entire string fits, or 0 if nothing fits.
|
||||
func BreakPoint(s string, limit int) int {
|
||||
// If limit is 0 or negative, nothing can fit
|
||||
if limit <= 0 {
|
||||
return 0
|
||||
}
|
||||
// Empty string has a breakpoint of 0
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
currentWidth := 0
|
||||
runeCount := 0
|
||||
// Iterate over runes, accumulating display width
|
||||
for _, r := range s {
|
||||
runeWidth := twwidth.Width(string(r)) // Calculate width of individual rune
|
||||
if currentWidth+runeWidth > limit {
|
||||
// Adding this rune would exceed the limit; breakpoint is before this rune
|
||||
if currentWidth == 0 {
|
||||
// First rune is too wide; allow breaking after it if limit > 0
|
||||
if runeWidth > limit && limit > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return runeCount
|
||||
}
|
||||
currentWidth += runeWidth
|
||||
runeCount++
|
||||
}
|
||||
|
||||
// Entire string fits within the limit
|
||||
return runeCount
|
||||
}
|
||||
|
||||
func MakeAlign(l int, align Align) Alignment {
|
||||
aa := make(Alignment, l)
|
||||
for i := 0; i < l; i++ {
|
||||
aa[i] = align
|
||||
}
|
||||
return aa
|
||||
}
|
||||
220
vendor/github.com/olekukonko/tablewriter/tw/mapper.go
generated
vendored
Normal file
220
vendor/github.com/olekukonko/tablewriter/tw/mapper.go
generated
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
package tw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// KeyValuePair represents a single key-value pair from a Mapper.
|
||||
type KeyValuePair[K comparable, V any] struct {
|
||||
Key K
|
||||
Value V
|
||||
}
|
||||
|
||||
// Mapper is a generic map type with comparable keys and any value type.
|
||||
// It provides type-safe operations on maps with additional convenience methods.
|
||||
type Mapper[K comparable, V any] map[K]V
|
||||
|
||||
// NewMapper creates and returns a new initialized Mapper.
|
||||
func NewMapper[K comparable, V any]() Mapper[K, V] {
|
||||
return make(Mapper[K, V])
|
||||
}
|
||||
|
||||
// Get returns the value associated with the key.
|
||||
// If the key doesn't exist or the map is nil, it returns the zero value for the value type.
|
||||
func (m Mapper[K, V]) Get(key K) V {
|
||||
if m == nil {
|
||||
var zero V
|
||||
return zero
|
||||
}
|
||||
return m[key]
|
||||
}
|
||||
|
||||
// OK returns the value associated with the key and a boolean indicating whether the key exists.
|
||||
func (m Mapper[K, V]) OK(key K) (V, bool) {
|
||||
if m == nil {
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
val, ok := m[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Set sets the value for the specified key.
|
||||
// Does nothing if the map is nil.
|
||||
func (m Mapper[K, V]) Set(key K, value V) Mapper[K, V] {
|
||||
if m != nil {
|
||||
m[key] = value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Delete removes the specified key from the map.
|
||||
// Does nothing if the key doesn't exist or the map is nil.
|
||||
func (m Mapper[K, V]) Delete(key K) Mapper[K, V] {
|
||||
if m != nil {
|
||||
delete(m, key)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Has returns true if the key exists in the map, false otherwise.
|
||||
func (m Mapper[K, V]) Has(key K) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
_, exists := m[key]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the map.
|
||||
// Returns 0 if the map is nil.
|
||||
func (m Mapper[K, V]) Len() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(m)
|
||||
}
|
||||
|
||||
// Keys returns a slice containing all keys in the map.
|
||||
// Returns nil if the map is nil or empty.
|
||||
func (m Mapper[K, V]) Keys() []K {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
keys := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (m Mapper[K, V]) Clear() {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
for k := range m {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Values returns a slice containing all values in the map.
|
||||
// Returns nil if the map is nil or empty.
|
||||
func (m Mapper[K, V]) Values() []V {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
values := make([]V, 0, len(m))
|
||||
for _, v := range m {
|
||||
values = append(values, v)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// Each iterates over each key-value pair in the map and calls the provided function.
|
||||
// Does nothing if the map is nil.
|
||||
func (m Mapper[K, V]) Each(fn func(K, V)) {
|
||||
if m != nil {
|
||||
for k, v := range m {
|
||||
fn(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a new Mapper containing only the key-value pairs that satisfy the predicate.
|
||||
func (m Mapper[K, V]) Filter(fn func(K, V) bool) Mapper[K, V] {
|
||||
result := NewMapper[K, V]()
|
||||
if m != nil {
|
||||
for k, v := range m {
|
||||
if fn(k, v) {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MapValues returns a new Mapper with the same keys but values transformed by the provided function.
|
||||
func (m Mapper[K, V]) MapValues(fn func(V) V) Mapper[K, V] {
|
||||
result := NewMapper[K, V]()
|
||||
if m != nil {
|
||||
for k, v := range m {
|
||||
result[k] = fn(v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Clone returns a shallow copy of the Mapper.
|
||||
func (m Mapper[K, V]) Clone() Mapper[K, V] {
|
||||
result := NewMapper[K, V]()
|
||||
if m != nil {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Slicer converts the Mapper to a Slicer of key-value pairs.
|
||||
func (m Mapper[K, V]) Slicer() Slicer[KeyValuePair[K, V]] {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
result := make(Slicer[KeyValuePair[K, V]], 0, len(m))
|
||||
for k, v := range m {
|
||||
result = append(result, KeyValuePair[K, V]{Key: k, Value: v})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m Mapper[K, V]) SortedKeys() []K {
|
||||
keys := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
a, b := any(keys[i]), any(keys[j])
|
||||
|
||||
switch va := a.(type) {
|
||||
case int:
|
||||
if vb, ok := b.(int); ok {
|
||||
return va < vb
|
||||
}
|
||||
case int32:
|
||||
if vb, ok := b.(int32); ok {
|
||||
return va < vb
|
||||
}
|
||||
case int64:
|
||||
if vb, ok := b.(int64); ok {
|
||||
return va < vb
|
||||
}
|
||||
case uint:
|
||||
if vb, ok := b.(uint); ok {
|
||||
return va < vb
|
||||
}
|
||||
case uint64:
|
||||
if vb, ok := b.(uint64); ok {
|
||||
return va < vb
|
||||
}
|
||||
case float32:
|
||||
if vb, ok := b.(float32); ok {
|
||||
return va < vb
|
||||
}
|
||||
case float64:
|
||||
if vb, ok := b.(float64); ok {
|
||||
return va < vb
|
||||
}
|
||||
case string:
|
||||
if vb, ok := b.(string); ok {
|
||||
return va < vb
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to string comparison
|
||||
return fmt.Sprintf("%v", a) < fmt.Sprintf("%v", b)
|
||||
})
|
||||
|
||||
return keys
|
||||
}
|
||||
18
vendor/github.com/olekukonko/tablewriter/tw/preset.go
generated
vendored
Normal file
18
vendor/github.com/olekukonko/tablewriter/tw/preset.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package tw
|
||||
|
||||
// BorderNone defines a border configuration with all sides disabled.
|
||||
var (
|
||||
// PaddingNone represents explicitly empty padding (no spacing on any side)
|
||||
// Equivalent to Padding{Overwrite: true}
|
||||
PaddingNone = Padding{Left: Empty, Right: Empty, Top: Empty, Bottom: Empty, Overwrite: true}
|
||||
BorderNone = Border{Left: Off, Right: Off, Top: Off, Bottom: Off}
|
||||
LinesNone = Lines{ShowTop: Off, ShowBottom: Off, ShowHeaderLine: Off, ShowFooterLine: Off}
|
||||
SeparatorsNone = Separators{ShowHeader: Off, ShowFooter: Off, BetweenRows: Off, BetweenColumns: Off}
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// PaddingDefault represents standard single-space padding on left/right
|
||||
// Equivalent to Padding{Left: " ", Right: " ", Overwrite: true}
|
||||
PaddingDefault = Padding{Left: " ", Right: " ", Overwrite: true}
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user