mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-23 14:09:20 -06:00
Merge pull request #888 from opencloud-eu/dependabot/go_modules/github.com/olekukonko/tablewriter-1.0.6
build(deps): bump github.com/olekukonko/tablewriter from 0.0.5 to 1.0.6
This commit is contained in:
4
go.mod
4
go.mod
@@ -58,7 +58,7 @@ require (
|
||||
github.com/nats-io/nats-server/v2 v2.11.3
|
||||
github.com/nats-io/nats.go v1.42.0
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/olekukonko/tablewriter v1.0.6
|
||||
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/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
|
||||
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 // 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.9 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -842,8 +842,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 v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
|
||||
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985 h1:V2wKiwjwAfRJRtUP6pC7wt4opeF14enO0du2dRV6Llo=
|
||||
github.com/olekukonko/ll v0.0.8-0.20250516010636-22ea57d81985/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.6 h1:/T45mIHc5hcEvibgzBzvMy7ruT+RjgoQRvkHbnl6OWA=
|
||||
github.com/olekukonko/tablewriter v1.0.6/go.mod h1:SJ0MV1aHb/89fLcsBMXMp30Xg3g5eGoOUu0RptEk4AU=
|
||||
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,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/opencloud/pkg/register"
|
||||
"github.com/opencloud-eu/opencloud/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
@@ -403,11 +404,24 @@ 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})
|
||||
cfg := tablewriter.Config{
|
||||
Header: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoFormat: tw.Off,
|
||||
},
|
||||
},
|
||||
Row: tw.CellConfig{
|
||||
ColumnAligns: []tw.Align{
|
||||
tw.AlignLeft,
|
||||
tw.AlignRight,
|
||||
tw.AlignRight,
|
||||
tw.AlignRight,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
table := tablewriter.NewTable(os.Stdout, tablewriter.WithConfig(cfg))
|
||||
table.Header([]string{"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]()
|
||||
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/urfave/cli/v2"
|
||||
mreg "go-micro.dev/v4/registry"
|
||||
|
||||
@@ -62,9 +63,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -480,8 +480,8 @@ func (s *Service) generateRunSet(cfg *occfg.Config) {
|
||||
// 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([]string{"Service"})
|
||||
|
||||
names := []string{}
|
||||
for t := range s.serviceToken {
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/app-provider/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/app-registry/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-app/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-machine/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-service/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/frontend/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/gateway/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/renderer"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
@@ -54,12 +56,17 @@ 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
|
||||
r := tw.Rendition{
|
||||
Settings: tw.Settings{
|
||||
Separators: tw.Separators{
|
||||
BetweenRows: tw.On,
|
||||
},
|
||||
},
|
||||
}
|
||||
tbl := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(r)))
|
||||
|
||||
headers := []string{"Name", "UID", "Enabled", "Description", "Condition", "Allowed resource actions"}
|
||||
tbl.SetHeader(headers)
|
||||
tbl.Header(headers)
|
||||
|
||||
for _, definition := range unifiedrole.GetRoles(unifiedrole.RoleFilterAll()) {
|
||||
const enabled = "enabled"
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/groups/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/idp/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/invitations/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/ocdav/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/ocs/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/policies/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/search/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/settings/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/sharing/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/storage-publiclink/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/storage-shares/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/storage-system/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -14,7 +14,8 @@ 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/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
zlog "github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/config"
|
||||
@@ -469,11 +470,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"itemID", "path", "type", "delete at"})
|
||||
table.Footer([]string{"", "", "", "total count: " + strconv.Itoa(total)})
|
||||
return table
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
@@ -145,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Space", "Upload Id", "Name", "Offset", "Size", "Executant", "Owner", "Expires", "Processing", "Scan Date", "Scan Result"})
|
||||
}
|
||||
|
||||
for _, u := range uploads {
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/thumbnails/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/users/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/web/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/webdav/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/registry"
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
|
||||
tw "github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/opencloud-eu/opencloud/services/webfinger/pkg/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@@ -35,9 +36,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, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
|
||||
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.
|
||||
1358
vendor/github.com/olekukonko/errors/README.md
generated
vendored
Normal file
1358
vendor/github.com/olekukonko/errors/README.md
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
957
vendor/github.com/olekukonko/errors/errors.go
generated
vendored
Normal file
957
vendor/github.com/olekukonko/errors/errors.go
generated
vendored
Normal file
@@ -0,0 +1,957 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
ctxTimeout = "[error] timeout" // Context key for marking timeout errors
|
||||
ctxRetry = "[error] retry" // Context key for marking retryable errors
|
||||
|
||||
contextSize = 4 // Default size of fixed-size context array
|
||||
bufferSize = 256 // Initial buffer size for JSON marshaling
|
||||
warmUpSize = 100 // Number of errors to pre-warm the pool
|
||||
stackDepth = 32 // Default maximum stack trace depth
|
||||
)
|
||||
|
||||
type ErrorCategory string
|
||||
|
||||
// ErrorOpts provides options for customizing error creation.
|
||||
type ErrorOpts struct {
|
||||
SkipStack int // Number of stack frames to skip when capturing the stack trace
|
||||
}
|
||||
|
||||
// Config defines the configuration for the errors package.
|
||||
type Config struct {
|
||||
StackDepth int // Maximum depth of the stack trace; 0 uses default
|
||||
ContextSize int // Initial size of the context map; 0 uses default
|
||||
DisablePooling bool // Disables object pooling for errors if true
|
||||
FilterInternal bool // Filters internal package frames from stack traces if true
|
||||
AutoFree bool // Automatically frees errors to pool if true
|
||||
}
|
||||
|
||||
// cachedConfig holds the current configuration, updated only on Configure().
|
||||
type cachedConfig struct {
|
||||
stackDepth int
|
||||
contextSize int
|
||||
disablePooling bool
|
||||
filterInternal bool
|
||||
autoFree bool
|
||||
}
|
||||
|
||||
var (
|
||||
currentConfig cachedConfig
|
||||
configMu sync.RWMutex
|
||||
errorPool = NewErrorPool() // Custom pool for Error instances
|
||||
stackPool = sync.Pool{ // Pool for stack trace slices
|
||||
New: func() interface{} {
|
||||
return make([]uintptr, currentConfig.stackDepth)
|
||||
},
|
||||
}
|
||||
emptyError = &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
msg: "",
|
||||
name: "",
|
||||
template: "",
|
||||
cause: nil,
|
||||
}
|
||||
)
|
||||
|
||||
//var bufferPool = sync.Pool{
|
||||
// New: func() interface{} {
|
||||
// return bytes.NewBuffer(make([]byte, 0, bufferSize))
|
||||
// },
|
||||
//}
|
||||
|
||||
// contextItem represents a single key-value pair in the smallContext array.
|
||||
type contextItem struct {
|
||||
key string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
// Error represents a custom error with enhanced features like context, stack traces, and wrapping.
|
||||
type Error struct {
|
||||
// Primary error information (most frequently accessed)
|
||||
msg string // Error message
|
||||
name string // Error name/type
|
||||
stack []uintptr // Stack trace
|
||||
|
||||
// Secondary error metadata
|
||||
template string // Message template used if msg is empty
|
||||
category string // Error category (e.g., "network", "validation")
|
||||
count uint64 // Occurrence count for tracking frequency
|
||||
code int32 // HTTP-like error code
|
||||
smallCount int32 // Number of items in smallContext
|
||||
|
||||
// Context and chaining
|
||||
context map[string]interface{} // Additional context as key-value pairs
|
||||
cause error // Wrapped underlying error
|
||||
callback func() // Optional callback executed on Error()
|
||||
smallContext [contextSize]contextItem // Fixed-size context storage for efficiency
|
||||
|
||||
// Synchronization
|
||||
mu sync.RWMutex // Protects concurrent access to mutable fields
|
||||
}
|
||||
|
||||
// init initializes the package with default configuration and pre-warms the error pool.
|
||||
func init() {
|
||||
currentConfig = cachedConfig{
|
||||
stackDepth: stackDepth,
|
||||
contextSize: contextSize,
|
||||
disablePooling: false,
|
||||
filterInternal: true,
|
||||
autoFree: true,
|
||||
}
|
||||
WarmPool(warmUpSize) // Pre-warm pool with initial errors
|
||||
}
|
||||
|
||||
// Configure updates the global configuration for the errors package.
|
||||
// Thread-safe; should be called before heavy usage for optimal performance.
|
||||
// Changes apply immediately to all subsequent error operations.
|
||||
func Configure(cfg Config) {
|
||||
configMu.Lock()
|
||||
defer configMu.Unlock()
|
||||
|
||||
if cfg.StackDepth != 0 {
|
||||
currentConfig.stackDepth = cfg.StackDepth
|
||||
}
|
||||
if cfg.ContextSize != 0 {
|
||||
currentConfig.contextSize = cfg.ContextSize
|
||||
}
|
||||
currentConfig.disablePooling = cfg.DisablePooling
|
||||
currentConfig.filterInternal = cfg.FilterInternal
|
||||
currentConfig.autoFree = cfg.AutoFree
|
||||
}
|
||||
|
||||
// newError creates a new Error instance, using the pool if enabled.
|
||||
// Initializes smallContext and stack appropriately.
|
||||
func newError() *Error {
|
||||
if currentConfig.disablePooling {
|
||||
return &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
stack: nil,
|
||||
}
|
||||
}
|
||||
return errorPool.Get()
|
||||
}
|
||||
|
||||
// Empty creates a new empty error with no stack trace.
|
||||
// Useful as a base for building errors incrementally.
|
||||
func Empty() *Error {
|
||||
return emptyError
|
||||
}
|
||||
|
||||
// Named creates a new error with a specific name and stack trace.
|
||||
// The name is used as the error message if no other message is set.
|
||||
func Named(name string) *Error {
|
||||
e := newError()
|
||||
e.name = name
|
||||
return e.WithStack()
|
||||
}
|
||||
|
||||
// New creates a fast, lightweight error without stack tracing.
|
||||
// Use instead of Trace() when stack traces aren't needed for better performance.
|
||||
func New(text string) *Error {
|
||||
if text == "" {
|
||||
return emptyError.Copy() // Global pre-allocated empty error
|
||||
}
|
||||
err := newError()
|
||||
err.msg = text
|
||||
return err
|
||||
}
|
||||
|
||||
// Newf is an alias to Errorf for fmt.Errorf compatibility.
|
||||
// Creates a formatted error without stack traces.
|
||||
func Newf(format string, args ...interface{}) *Error {
|
||||
err := newError()
|
||||
err.msg = fmt.Sprintf(format, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Std creates a standard error using errors.New, provided for backward compatibility.
|
||||
// This function serves as a lightweight wrapper around the standard library's error creation,
|
||||
// allowing users to opt into basic error handling without adopting the full features of this package.
|
||||
func Std(text string) error {
|
||||
return errors.New(text)
|
||||
}
|
||||
|
||||
// Stdf creates a formatted standard error using fmt.Errorf, provided for backward compatibility.
|
||||
// This function wraps the standard library's formatted error creation, offering a simple alternative
|
||||
// to the package's enhanced error handling while maintaining compatibility with existing codebases.
|
||||
func Stdf(format string, a ...interface{}) error {
|
||||
return fmt.Errorf(format, a...)
|
||||
}
|
||||
|
||||
// Trace creates an error with stack trace capture enabled.
|
||||
// Use when call stacks are needed for debugging; has performance overhead.
|
||||
func Trace(text string) *Error {
|
||||
e := New(text)
|
||||
return e.WithStack()
|
||||
}
|
||||
|
||||
// Tracef creates a formatted error with stack trace.
|
||||
// Combines Errorf and WithStack for convenience.
|
||||
func Tracef(format string, args ...interface{}) *Error {
|
||||
e := Newf(format, args...)
|
||||
return e.WithStack()
|
||||
}
|
||||
|
||||
// As attempts to assign the error or one in its chain to the target interface.
|
||||
// It only assigns when the target is a **Error and the current error node has a non-empty name.
|
||||
// If the current node has an empty name, it delegates to its wrapped cause.
|
||||
func (e *Error) As(target interface{}) bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
// Handle *Error target (for stderrors.As compatibility)
|
||||
if targetPtr, ok := target.(*Error); ok {
|
||||
current := e
|
||||
for current != nil {
|
||||
if current.name != "" {
|
||||
*targetPtr = *current
|
||||
return true
|
||||
}
|
||||
if next, ok := current.cause.(*Error); ok {
|
||||
current = next
|
||||
} else if current.cause != nil {
|
||||
return errors.As(current.cause, target)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Handle *error target - unwrap to innermost error
|
||||
if targetErr, ok := target.(*error); ok {
|
||||
innermost := error(e)
|
||||
current := error(e)
|
||||
for current != nil {
|
||||
if err, ok := current.(*Error); ok && err.cause != nil {
|
||||
current = err.cause
|
||||
innermost = current
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
*targetErr = innermost
|
||||
return true
|
||||
}
|
||||
|
||||
// Delegate to cause for other types
|
||||
if e.cause != nil {
|
||||
return errors.As(e.cause, target)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Callback sets a function to be called when Error() is invoked.
|
||||
// Useful for logging or side effects; returns the error for chaining.
|
||||
func (e *Error) Callback(fn func()) *Error {
|
||||
e.callback = fn
|
||||
return e
|
||||
}
|
||||
|
||||
// Category returns the error's category, if set.
|
||||
// Returns an empty string if no category is defined.
|
||||
func (e *Error) Category() string {
|
||||
return e.category
|
||||
}
|
||||
|
||||
// Code returns the error's status code, if set.
|
||||
// Returns 0 if no code is defined.
|
||||
func (e *Error) Code() int {
|
||||
return int(e.code)
|
||||
}
|
||||
|
||||
// Context returns the error's context as a map.
|
||||
// Converts smallContext to a map if needed; returns nil if empty.
|
||||
func (e *Error) Context() map[string]interface{} {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
if e.smallCount > 0 && e.context == nil {
|
||||
e.context = make(map[string]interface{}, e.smallCount)
|
||||
for i := int32(0); i < e.smallCount; i++ {
|
||||
e.context[e.smallContext[i].key] = e.smallContext[i].value
|
||||
}
|
||||
}
|
||||
return e.context
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of the error, preserving all fields except stack.
|
||||
// The new error does not capture a new stack trace unless explicitly added.
|
||||
func (e *Error) Copy() *Error {
|
||||
if e == emptyError {
|
||||
return &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
}
|
||||
}
|
||||
|
||||
newErr := newError()
|
||||
|
||||
newErr.msg = e.msg
|
||||
newErr.name = e.name
|
||||
newErr.template = e.template
|
||||
newErr.cause = e.cause
|
||||
newErr.code = e.code
|
||||
newErr.category = e.category
|
||||
newErr.count = e.count
|
||||
|
||||
if e.smallCount > 0 {
|
||||
newErr.smallCount = e.smallCount
|
||||
for i := int32(0); i < e.smallCount; i++ {
|
||||
newErr.smallContext[i] = e.smallContext[i]
|
||||
}
|
||||
} else if e.context != nil {
|
||||
newErr.context = make(map[string]interface{}, len(e.context))
|
||||
for k, v := range e.context {
|
||||
newErr.context[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if e.stack != nil && len(e.stack) > 0 {
|
||||
if newErr.stack == nil {
|
||||
newErr.stack = stackPool.Get().([]uintptr)
|
||||
}
|
||||
newErr.stack = append(newErr.stack[:0], e.stack...)
|
||||
}
|
||||
|
||||
return newErr
|
||||
}
|
||||
|
||||
// Count returns the number of times the error has been incremented.
|
||||
// Useful for tracking occurrence frequency.
|
||||
func (e *Error) Count() uint64 {
|
||||
return e.count
|
||||
}
|
||||
|
||||
// Err returns the error as an error interface.
|
||||
// Provided for compatibility; simply returns the error itself.
|
||||
func (e *Error) Err() error {
|
||||
return e
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
// Prioritizes msg, then template, then name, falling back to "unknown error".
|
||||
// Executes callback if set before returning the message.
|
||||
func (e *Error) Error() string {
|
||||
if e.callback != nil {
|
||||
e.callback()
|
||||
}
|
||||
var msg string
|
||||
switch {
|
||||
case e.msg != "":
|
||||
msg = e.msg
|
||||
case e.template != "":
|
||||
msg = e.template
|
||||
case e.name != "":
|
||||
msg = e.name
|
||||
default:
|
||||
msg = "unknown error"
|
||||
}
|
||||
if e.cause != nil {
|
||||
causeMsg := e.cause.Error()
|
||||
if msg != "" && causeMsg != "" {
|
||||
msg = msg + ": " + causeMsg
|
||||
} else if causeMsg != "" {
|
||||
msg = causeMsg
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// Errorf creates a formatted error without stack traces.
|
||||
// Compatible with fmt.Errorf; does not capture stack trace for performance.
|
||||
func Errorf(format string, args ...interface{}) *Error {
|
||||
err := newError()
|
||||
err.msg = fmt.Sprintf(format, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// FastStack returns a lightweight stack trace without function names.
|
||||
// Filters internal frames if FilterInternal is enabled; returns nil if no stack.
|
||||
func (e *Error) FastStack() []string {
|
||||
if e.stack == nil {
|
||||
return nil
|
||||
}
|
||||
configMu.RLock()
|
||||
filter := currentConfig.filterInternal
|
||||
configMu.RUnlock()
|
||||
|
||||
pcs := e.stack
|
||||
frames := make([]string, 0, len(pcs))
|
||||
for _, pc := range pcs {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
frames = append(frames, "unknown")
|
||||
continue
|
||||
}
|
||||
file, line := fn.FileLine(pc)
|
||||
if filter && isInternalFrame(runtime.Frame{File: file, Function: fn.Name()}) {
|
||||
continue
|
||||
}
|
||||
frames = append(frames, fmt.Sprintf("%s:%d", file, line))
|
||||
}
|
||||
return frames
|
||||
}
|
||||
|
||||
// Find searches the error chain for the first error matching pred.
|
||||
// Starts with the current error and follows Unwrap() and Cause() chains.
|
||||
func (e *Error) Find(pred func(error) bool) error {
|
||||
if e == nil || pred == nil {
|
||||
return nil
|
||||
}
|
||||
return Find(e, pred)
|
||||
}
|
||||
|
||||
// Format returns a formatted string representation of the error.
|
||||
// Includes message, code, context, and stack trace if present.
|
||||
func (e *Error) Format() string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Error message
|
||||
sb.WriteString("Error: " + e.Error() + "\n")
|
||||
|
||||
// Metadata
|
||||
if e.code != 0 {
|
||||
sb.WriteString(fmt.Sprintf("Code: %d\n", e.code))
|
||||
}
|
||||
|
||||
// Context (only show context added at this level)
|
||||
if ctx := e.contextAtThisLevel(); len(ctx) > 0 {
|
||||
sb.WriteString("Context:\n")
|
||||
for k, v := range ctx {
|
||||
sb.WriteString(fmt.Sprintf(" %s: %v\n", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Stack trace
|
||||
if e.stack != nil {
|
||||
sb.WriteString("Stack:\n")
|
||||
for i, frame := range e.Stack() {
|
||||
sb.WriteString(fmt.Sprintf(" %d. %s\n", i+1, frame))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// contextAtThisLevel returns context specific to this error level, excluding inherited context.
|
||||
// Combines smallContext and context map into a single map; returns nil if empty.
|
||||
func (e *Error) contextAtThisLevel() map[string]interface{} {
|
||||
if e.context == nil && e.smallCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := make(map[string]interface{})
|
||||
// Add smallContext items
|
||||
for i := 0; i < int(e.smallCount); i++ {
|
||||
ctx[e.smallContext[i].key] = e.smallContext[i].value
|
||||
}
|
||||
// Add map context items
|
||||
if e.context != nil {
|
||||
for k, v := range e.context {
|
||||
ctx[k] = v
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Free resets the error and returns it to the pool if pooling is enabled.
|
||||
// Does nothing beyond reset if pooling is disabled.
|
||||
func (e *Error) Free() {
|
||||
if currentConfig.disablePooling {
|
||||
return
|
||||
}
|
||||
|
||||
e.Reset()
|
||||
|
||||
if e.stack != nil {
|
||||
stackPool.Put(e.stack[:cap(e.stack)])
|
||||
e.stack = nil
|
||||
}
|
||||
errorPool.Put(e)
|
||||
}
|
||||
|
||||
// Has checks if the error contains meaningful content.
|
||||
// Returns true if msg, template, name, or cause is non-empty/nil.
|
||||
func (e *Error) Has() bool {
|
||||
return e != nil && (e.msg != "" || e.template != "" || e.name != "" || e.cause != nil)
|
||||
}
|
||||
|
||||
// HasContextKey checks if the specified key exists in the error's context.
|
||||
// Searches both smallContext and context map; thread-safe.
|
||||
func (e *Error) HasContextKey(key string) bool {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
if e.smallCount > 0 {
|
||||
for i := int32(0); i < e.smallCount; i++ {
|
||||
if e.smallContext[i].key == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.context != nil {
|
||||
_, exists := e.context[key]
|
||||
return exists
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Increment increases the error's count by 1 and returns the error.
|
||||
// Uses atomic operation for thread safety.
|
||||
func (e *Error) Increment() *Error {
|
||||
atomic.AddUint64(&e.count, 1)
|
||||
return e
|
||||
}
|
||||
|
||||
// Is checks if the error matches a target error by pointer equality, name, or wrapped cause.
|
||||
// Ensures compatibility with stderrors.Is by prioritizing chain traversal.
|
||||
func (e *Error) Is(target error) bool {
|
||||
if e == nil || target == nil {
|
||||
return e == target
|
||||
}
|
||||
if e == target {
|
||||
return true
|
||||
}
|
||||
if e.name != "" {
|
||||
if te, ok := target.(*Error); ok && te.name != "" && e.name == te.name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Add string comparison for standard errors
|
||||
if stdErr, ok := target.(error); ok && e.Error() == stdErr.Error() {
|
||||
return true
|
||||
}
|
||||
if e.cause != nil {
|
||||
return errors.Is(e.cause, target)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEmpty checks if the error has no meaningful content (empty message, no name/template/cause).
|
||||
// Returns true for nil errors or errors with no data.
|
||||
func (e *Error) IsEmpty() bool {
|
||||
if e == nil {
|
||||
return true
|
||||
}
|
||||
return e.msg == "" && e.template == "" && e.name == "" && e.cause == nil
|
||||
}
|
||||
|
||||
// IsNull checks if an error is nil or represents a SQL NULL value.
|
||||
// Considers both the error itself and any context values; returns true if all context is null.
|
||||
func (e *Error) IsNull() bool {
|
||||
if e == nil || e == emptyError {
|
||||
return true
|
||||
}
|
||||
// If no context or cause, and no content, it's not null
|
||||
if e.smallCount == 0 && e.context == nil && e.cause == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check cause first - if it’s null, the whole error is null
|
||||
if e.cause != nil {
|
||||
var isNull bool
|
||||
if ce, ok := e.cause.(*Error); ok {
|
||||
isNull = ce.IsNull()
|
||||
} else {
|
||||
isNull = sqlNull(e.cause)
|
||||
}
|
||||
if isNull {
|
||||
return true
|
||||
}
|
||||
// If cause isn’t null, continue checking this error’s context
|
||||
}
|
||||
|
||||
// Check small context
|
||||
if e.smallCount > 0 {
|
||||
allNull := true
|
||||
for i := 0; i < int(e.smallCount); i++ {
|
||||
isNull := sqlNull(e.smallContext[i].value)
|
||||
if !isNull {
|
||||
allNull = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allNull {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check regular context
|
||||
if e.context != nil {
|
||||
allNull := true
|
||||
for _, v := range e.context {
|
||||
isNull := sqlNull(v)
|
||||
if !isNull {
|
||||
allNull = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allNull {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Null if we have context and it’s all null
|
||||
return e.smallCount > 0 || e.context != nil
|
||||
}
|
||||
|
||||
var (
|
||||
jsonBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, bufferSize))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// MarshalJSON serializes the error to JSON, including name, message, context, cause, and stack.
|
||||
// Handles nested *Error causes and custom marshalers efficiently.
|
||||
func (e *Error) MarshalJSON() ([]byte, error) {
|
||||
// Get buffer from pool
|
||||
buf := jsonBufferPool.Get().(*bytes.Buffer)
|
||||
defer jsonBufferPool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
// Create new encoder each time (no Reset available)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
||||
// Prepare error data
|
||||
je := struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Cause interface{} `json:"cause,omitempty"`
|
||||
Stack []string `json:"stack,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}{
|
||||
Name: e.name,
|
||||
Message: e.msg,
|
||||
Code: e.Code(),
|
||||
}
|
||||
|
||||
// Handle context
|
||||
if ctx := e.Context(); len(ctx) > 0 {
|
||||
je.Context = ctx
|
||||
}
|
||||
|
||||
// Handle stack
|
||||
if e.stack != nil {
|
||||
je.Stack = e.Stack()
|
||||
}
|
||||
|
||||
// Handle cause
|
||||
if e.cause != nil {
|
||||
switch c := e.cause.(type) {
|
||||
case *Error:
|
||||
je.Cause = c
|
||||
case json.Marshaler:
|
||||
je.Cause = c
|
||||
default:
|
||||
je.Cause = c.Error()
|
||||
}
|
||||
}
|
||||
|
||||
// Encode
|
||||
if err := enc.Encode(je); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return bytes without trailing newline
|
||||
result := buf.Bytes()
|
||||
if len(result) > 0 && result[len(result)-1] == '\n' {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Msgf sets the error message using a formatted string.
|
||||
// Overwrites any existing message; returns the error for chaining.
|
||||
func (e *Error) Msgf(format string, args ...interface{}) *Error {
|
||||
e.msg = fmt.Sprintf(format, args...)
|
||||
return e
|
||||
}
|
||||
|
||||
// Name returns the error's name, if set.
|
||||
// Returns an empty string if no name is defined.
|
||||
func (e *Error) Name() string {
|
||||
return e.name
|
||||
}
|
||||
|
||||
// Reset clears all fields of the error, preparing it for reuse.
|
||||
// Does not free the stack; use Free() to return to pool.
|
||||
func (e *Error) Reset() {
|
||||
e.msg = ""
|
||||
e.name = ""
|
||||
e.template = ""
|
||||
e.category = ""
|
||||
e.code = 0
|
||||
e.count = 0
|
||||
e.cause = nil
|
||||
e.callback = nil
|
||||
|
||||
if e.context != nil {
|
||||
for k := range e.context {
|
||||
delete(e.context, k)
|
||||
}
|
||||
}
|
||||
e.smallCount = 0
|
||||
|
||||
if e.stack != nil {
|
||||
e.stack = e.stack[:0]
|
||||
}
|
||||
}
|
||||
|
||||
// Stack returns a detailed stack trace as a slice of strings.
|
||||
// Filters internal frames if FilterInternal is enabled; returns nil if no stack.
|
||||
func (e *Error) Stack() []string {
|
||||
if e.stack == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
frames := runtime.CallersFrames(e.stack)
|
||||
var trace []string
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
if frame == (runtime.Frame{}) {
|
||||
break
|
||||
}
|
||||
|
||||
if currentConfig.filterInternal && isInternalFrame(frame) {
|
||||
continue
|
||||
}
|
||||
|
||||
trace = append(trace, fmt.Sprintf("%s %s:%d",
|
||||
frame.Function,
|
||||
frame.File,
|
||||
frame.Line))
|
||||
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
return trace
|
||||
}
|
||||
|
||||
// Trace ensures the error has a stack trace, capturing it if missing.
|
||||
// Skips capture if stack already exists; returns the error for chaining.
|
||||
func (e *Error) Trace() *Error {
|
||||
if e.stack == nil {
|
||||
e.stack = captureStack(2)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Transform applies transformations to a copy of the error.
|
||||
// Returns the transformed copy or the original if no changes are needed.
|
||||
func (e *Error) Transform(fn func(*Error)) *Error {
|
||||
if e == nil || fn == nil {
|
||||
return e
|
||||
}
|
||||
newErr := e.Copy()
|
||||
fn(newErr)
|
||||
return newErr
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause of the error, if any.
|
||||
// Implements the errors.Unwrap interface for unwrapping chains.
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// UnwrapAll returns a slice of all errors in the chain, starting with this error.
|
||||
// Traverses the cause chain, creating isolated copies of each *Error.
|
||||
func (e *Error) UnwrapAll() []error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
var chain []error
|
||||
current := error(e)
|
||||
for current != nil {
|
||||
if err, ok := current.(*Error); ok {
|
||||
isolated := newError()
|
||||
isolated.msg = err.msg
|
||||
isolated.name = err.name
|
||||
isolated.template = err.template
|
||||
isolated.code = err.code
|
||||
isolated.category = err.category
|
||||
if err.smallCount > 0 {
|
||||
isolated.smallCount = err.smallCount
|
||||
for i := int32(0); i < err.smallCount; i++ {
|
||||
isolated.smallContext[i] = err.smallContext[i]
|
||||
}
|
||||
}
|
||||
if err.context != nil {
|
||||
isolated.context = make(map[string]interface{}, len(err.context))
|
||||
for k, v := range err.context {
|
||||
isolated.context[k] = v
|
||||
}
|
||||
}
|
||||
if err.stack != nil {
|
||||
isolated.stack = append([]uintptr(nil), err.stack...)
|
||||
}
|
||||
chain = append(chain, isolated)
|
||||
} else {
|
||||
chain = append(chain, current)
|
||||
}
|
||||
if unwrapper, ok := current.(interface{ Unwrap() error }); ok {
|
||||
current = unwrapper.Unwrap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return chain
|
||||
}
|
||||
|
||||
// Walk traverses the error chain, applying fn to each error.
|
||||
// Starts with the current error and follows the cause chain.
|
||||
func (e *Error) Walk(fn func(error)) {
|
||||
if e == nil || fn == nil {
|
||||
return
|
||||
}
|
||||
current := error(e)
|
||||
for current != nil {
|
||||
fn(current)
|
||||
if unwrappable, ok := current.(interface{ Unwrap() error }); ok {
|
||||
current = unwrappable.Unwrap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With adds a key-value pair to the error's context.
|
||||
// Uses smallContext for efficiency until full, then switches to map; thread-safe.
|
||||
func (e *Error) With(key string, value interface{}) *Error {
|
||||
// Fast path for small context (no map needed)
|
||||
if e.smallCount < contextSize && e.context == nil {
|
||||
e.mu.Lock()
|
||||
// Double-check after acquiring lock
|
||||
if e.smallCount < contextSize && e.context == nil {
|
||||
e.smallContext[e.smallCount] = contextItem{key, value}
|
||||
e.smallCount++
|
||||
e.mu.Unlock()
|
||||
return e
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Slow path - requires map
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.context == nil {
|
||||
e.context = make(map[string]interface{}, currentConfig.contextSize)
|
||||
// Migrate existing items if any
|
||||
for i := int32(0); i < e.smallCount; i++ {
|
||||
e.context[e.smallContext[i].key] = e.smallContext[i].value
|
||||
}
|
||||
}
|
||||
|
||||
e.context[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// WithCategory sets a category for the error and returns the error.
|
||||
// Useful for classifying errors (e.g., "network", "validation").
|
||||
func (e *Error) WithCategory(category ErrorCategory) *Error {
|
||||
e.category = string(category)
|
||||
return e
|
||||
}
|
||||
|
||||
// WithCode sets an HTTP-like status code for the error and returns the error.
|
||||
// Overwrites any existing code.
|
||||
func (e *Error) WithCode(code int) *Error {
|
||||
e.code = int32(code)
|
||||
return e
|
||||
}
|
||||
|
||||
// WithName sets the error's name and returns the error.
|
||||
// Overwrites any existing name.
|
||||
func (e *Error) WithName(name string) *Error {
|
||||
e.name = name
|
||||
return e
|
||||
}
|
||||
|
||||
// WithRetryable marks the error as retryable in its context.
|
||||
// Adds a "retry" key with value true; returns the error.
|
||||
func (e *Error) WithRetryable() *Error {
|
||||
return e.With(ctxRetry, true)
|
||||
}
|
||||
|
||||
// WithStack captures the stack trace at call time and returns the error.
|
||||
// Skips capturing if stack already exists or depth is 0.
|
||||
func (e *Error) WithStack() *Error {
|
||||
if e.stack == nil {
|
||||
e.stack = captureStack(1) // Skip WithStack
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// WithTemplate sets a template string for the error and returns the error.
|
||||
// Used as the error message if no explicit message is set.
|
||||
func (e *Error) WithTemplate(template string) *Error {
|
||||
e.template = template
|
||||
return e
|
||||
}
|
||||
|
||||
// WithTimeout marks the error as a timeout error in its context.
|
||||
// Adds a "timeout" key with value true; returns the error.
|
||||
func (e *Error) WithTimeout() *Error {
|
||||
return e.With(ctxTimeout, true)
|
||||
}
|
||||
|
||||
// Wrap associates a cause error with this error, creating an error chain.
|
||||
// Returns the error for method chaining.
|
||||
func (e *Error) Wrap(cause error) *Error {
|
||||
if cause == nil {
|
||||
return e
|
||||
}
|
||||
e.cause = cause
|
||||
return e
|
||||
}
|
||||
|
||||
// WrapNotNil wraps a cause error only if it is non-nil.
|
||||
// Returns the error for method chaining; no-op if cause is nil.
|
||||
func (e *Error) WrapNotNil(cause error) *Error {
|
||||
if cause != nil {
|
||||
e.cause = cause
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// WarmPool pre-populates the error pool with a specified number of instances.
|
||||
// Reduces allocation overhead during initial usage; no effect if pooling is disabled.
|
||||
func WarmPool(count int) {
|
||||
if currentConfig.disablePooling {
|
||||
return
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
e := &Error{
|
||||
smallContext: [contextSize]contextItem{},
|
||||
stack: nil,
|
||||
}
|
||||
errorPool.Put(e)
|
||||
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
|
||||
}
|
||||
}
|
||||
|
||||
// WarmStackPool pre-populates the stack pool with a specified number of slices.
|
||||
// Reduces allocation overhead for stack traces; no effect if pooling is disabled.
|
||||
func WarmStackPool(count int) {
|
||||
if currentConfig.disablePooling {
|
||||
return
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
stackPool.Put(make([]uintptr, 0, currentConfig.stackDepth))
|
||||
}
|
||||
}
|
||||
423
vendor/github.com/olekukonko/errors/helper.go
generated
vendored
Normal file
423
vendor/github.com/olekukonko/errors/helper.go
generated
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
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 500
|
||||
}
|
||||
|
||||
// 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
|
||||
for unwrapped := err; unwrapped != nil; {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to context map
|
||||
if e.context != nil {
|
||||
if val, ok := e.context[ctxRetry].(bool); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
325
vendor/github.com/olekukonko/errors/multi_error.go
generated
vendored
Normal file
325
vendor/github.com/olekukonko/errors/multi_error.go
generated
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Check for duplicates by comparing error messages
|
||||
for _, e := range m.errors {
|
||||
if e.Error() == err.Error() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Respect limit if set
|
||||
if m.limit > 0 && len(m.errors) >= m.limit {
|
||||
return
|
||||
}
|
||||
|
||||
m.errors = append(m.errors, err)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func fastRand() uint32 {
|
||||
r := uint32(time.Now().UnixNano())
|
||||
r ^= r << 13
|
||||
r ^= r >> 17
|
||||
r ^= r << 5
|
||||
return r
|
||||
}
|
||||
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
|
||||
}
|
||||
286
vendor/github.com/olekukonko/errors/retry.go
generated
vendored
Normal file
286
vendor/github.com/olekukonko/errors/retry.go
generated
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
// 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++ {
|
||||
err := fn()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if retry is applicable; return immediately if not retryable
|
||||
if r.retryIf != nil && !r.retryIf(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if r.onRetry != nil {
|
||||
r.onRetry(attempt, err)
|
||||
}
|
||||
|
||||
// Exit if this was the last attempt
|
||||
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 r.ctx.Err()
|
||||
case <-time.After(currentDelay):
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
640
vendor/github.com/olekukonko/ll/global.go
generated
vendored
Normal file
640
vendor/github.com/olekukonko/ll/global.go
generated
vendored
Normal file
@@ -0,0 +1,640 @@
|
||||
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...)
|
||||
}
|
||||
|
||||
// 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(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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
439
vendor/github.com/olekukonko/ll/lh/colorized.go
generated
vendored
Normal file
439
vendor/github.com/olekukonko/ll/lh/colorized.go
generated
vendored
Normal file
@@ -0,0 +1,439 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c := &ColorizedHandler{w: w} // Initialize with writer
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
// 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
|
||||
})
|
||||
}
|
||||
93
vendor/github.com/olekukonko/ll/lh/memory.go
generated
vendored
Normal file
93
vendor/github.com/olekukonko/ll/lh/memory.go
generated
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
193
vendor/github.com/olekukonko/ll/lh/text.go
generated
vendored
Normal file
193
vendor/github.com/olekukonko/ll/lh/text.go
generated
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
package lh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/ll/lx"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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 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")
|
||||
}
|
||||
1368
vendor/github.com/olekukonko/ll/ll.go
generated
vendored
Normal file
1368
vendor/github.com/olekukonko/ll/ll.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
154
vendor/github.com/olekukonko/ll/lx/lx.go
generated
vendored
Normal file
154
vendor/github.com/olekukonko/ll/lx/lx.go
generated
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
1453
vendor/github.com/olekukonko/tablewriter/README.md
generated
vendored
1453
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
|
||||
1151
vendor/github.com/olekukonko/tablewriter/config.go
generated
vendored
Normal file
1151
vendor/github.com/olekukonko/tablewriter/config.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
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
|
||||
}
|
||||
|
||||
40
vendor/github.com/olekukonko/tablewriter/deprecated.go
generated
vendored
Normal file
40
vendor/github.com/olekukonko/tablewriter/deprecated.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package tablewriter
|
||||
|
||||
import "github.com/olekukonko/tablewriter/tw"
|
||||
|
||||
// Deprecated: WithBorders is no longer used.
|
||||
// Border control has been moved to the renderer, which now manages its own borders.
|
||||
// This Option has no effect on the Table and may be removed in future versions.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: WithBorders is no longer supported.
|
||||
// Use [tw.Behavior] directly to configure border settings.
|
||||
type Behavior tw.Behavior
|
||||
|
||||
// Deprecated: WithRendererSettings i sno longer supported.
|
||||
type Settings tw.Settings
|
||||
|
||||
// WithRendererSettings updates the renderer's settings (e.g., separators, lines).
|
||||
// Render setting has move to renders directly
|
||||
// you can also use WithRendition for renders that have rendition support
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
234
vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go
generated
vendored
Normal file
234
vendor/github.com/olekukonko/tablewriter/pkg/twwarp/wrap.go
generated
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
// 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 (
|
||||
"github.com/rivo/uniseg"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
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)
|
||||
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 {
|
||||
return []string{s}, runewidth.StringWidth(s)
|
||||
}
|
||||
// For very long all-space strings, "wrap" by truncating to the limit.
|
||||
if lim > 0 {
|
||||
// Use our new helper function to get a substring of the correct display width
|
||||
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) })
|
||||
// firstNonSpace will not be -1 due to TrimSpace check above.
|
||||
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)
|
||||
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) // Get width of the current grapheme cluster
|
||||
|
||||
if currentWidth+graphemeWidth > targetWidth {
|
||||
// Adding this grapheme would exceed the target width
|
||||
break
|
||||
}
|
||||
|
||||
currentWidth += graphemeWidth
|
||||
// Get the end byte position of the current grapheme cluster
|
||||
_, 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])
|
||||
}
|
||||
nbrk := make([]int, n)
|
||||
cost := make([]int, n)
|
||||
for i := range cost {
|
||||
cost[i] = math.MaxInt32
|
||||
}
|
||||
remainderLen := lengths[n-1]
|
||||
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)
|
||||
}
|
||||
578
vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go
generated
vendored
Normal file
578
vendor/github.com/olekukonko/tablewriter/renderer/blueprint.go
generated
vendored
Normal file
@@ -0,0 +1,578 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"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}
|
||||
}
|
||||
|
||||
// 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 = tw.DisplayWidth(prefix) + tw.DisplayWidth(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 += tw.DisplayWidth(f.config.Symbols.Column())
|
||||
}
|
||||
if f.config.Borders.Right.Enabled() {
|
||||
targetTotalWidth += tw.DisplayWidth(f.config.Symbols.Column())
|
||||
}
|
||||
if f.config.Settings.Separators.BetweenColumns.Enabled() && len(sortedKeys) > 1 {
|
||||
targetTotalWidth += tw.DisplayWidth(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 = tw.DisplayWidth(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 - tw.DisplayWidth(f.config.Symbols.Column())
|
||||
}
|
||||
if f.config.Borders.Right.Enabled() && keyIndex == len(visibleColIndices)-1 {
|
||||
rightBorderWidth := tw.DisplayWidth(jr.RenderRight(currentColIdx))
|
||||
adjustedColWidth -= rightBorderWidth - tw.DisplayWidth(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 := tw.DisplayWidth(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 := tw.DisplayWidth(repeatedSegment)
|
||||
if actualWidth > adjustedColWidth {
|
||||
// Truncate if too long
|
||||
repeatedSegment = tw.TruncateString(repeatedSegment, adjustedColWidth)
|
||||
actualWidth = tw.DisplayWidth(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 = tw.DisplayWidth(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 tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) {
|
||||
junction = f.config.Symbols.Center()
|
||||
if tw.DisplayWidth(junction) != tw.DisplayWidth(f.config.Symbols.Column()) {
|
||||
junction = f.config.Symbols.Column()
|
||||
}
|
||||
}
|
||||
junctionWidth := tw.DisplayWidth(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 = tw.DisplayWidth(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 := tw.DisplayWidth(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 := tw.DisplayWidth(leftPadChar)
|
||||
padRightWidth := tw.DisplayWidth(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 = tw.TruncateString(content, availableContentWidth)
|
||||
runeWidth = tw.DisplayWidth(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 := tw.DisplayWidth(output)
|
||||
// Adjust output to match target width
|
||||
if finalWidth > width {
|
||||
output = tw.TruncateString(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() && tw.DisplayWidth(output) != width {
|
||||
f.logger.Debugf("formatCell Warning: Final width %d does not match target %d for result '%s'",
|
||||
tw.DisplayWidth(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 += tw.DisplayWidth(prefix)
|
||||
f.logger.Debugf("renderLine: Prefix='%s' (f.width %d)", prefix, tw.DisplayWidth(prefix))
|
||||
}
|
||||
|
||||
colIndex := 0
|
||||
separatorDisplayWidth := 0
|
||||
if f.config.Settings.Separators.BetweenColumns.Enabled() {
|
||||
separatorDisplayWidth = tw.DisplayWidth(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
|
||||
|
||||
// Override alignment for footer merged cells
|
||||
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
|
||||
if align != tw.AlignRight {
|
||||
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 := tw.DisplayWidth(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 += tw.DisplayWidth(suffix)
|
||||
f.logger.Debugf("renderLine: Suffix='%s' (f.width %d)", suffix, tw.DisplayWidth(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)
|
||||
}
|
||||
|
||||
func (f *Blueprint) Rendition(config tw.Rendition) {
|
||||
f.config = mergeRendition(f.config, config)
|
||||
f.logger.Debugf("Blueprint.Rendition updated. New internal 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"
|
||||
"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 := tw.DisplayWidth(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 := tw.DisplayWidth(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 := tw.DisplayWidth(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 := tw.DisplayWidth(padLeftCharStr)
|
||||
definedPadRightWidth := tw.DisplayWidth(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 = tw.TruncateString(content, availableForContentAndAlign)
|
||||
contentVisualWidth = tw.DisplayWidth(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 := tw.DisplayWidth(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 = tw.TruncateString(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, tw.DisplayWidth(output), output)
|
||||
}
|
||||
|
||||
c.logger.Debugf("Formatted cell final result: '%s' (target width %d, display width %d)", output, width, tw.DisplayWidth(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 = tw.DisplayWidth(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
|
||||
}
|
||||
440
vendor/github.com/olekukonko/tablewriter/renderer/html.go
generated
vendored
Normal file
440
vendor/github.com/olekukonko/tablewriter/renderer/html.go
generated
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"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}
|
||||
}
|
||||
|
||||
// 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.AlignLeft)
|
||||
}
|
||||
|
||||
// add per colum alignment if it exits
|
||||
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 := tw.DisplayWidth(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 := tw.DisplayWidth(padLeftChar)
|
||||
padRightCharWidth := tw.DisplayWidth(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 := tw.DisplayWidth(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 = tw.TruncateString(result, targetWidth)
|
||||
}
|
||||
m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, tw.DisplayWidth(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(':')
|
||||
default:
|
||||
sb.WriteRune(':')
|
||||
sb.WriteString(strings.Repeat("-", targetWidth-1))
|
||||
}
|
||||
|
||||
result := sb.String()
|
||||
currentLen := tw.DisplayWidth(result)
|
||||
if currentLen < targetWidth {
|
||||
result += strings.Repeat("-", targetWidth-currentLen)
|
||||
} else if currentLen > targetWidth {
|
||||
result = tw.TruncateString(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 := tw.DisplayWidth(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 == (tw.Padding{}) {
|
||||
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
|
||||
|
||||
if align == tw.AlignNone || align == tw.Empty {
|
||||
if ctx.Row.Position == tw.Header && !isHeaderSep {
|
||||
align = tw.AlignCenter
|
||||
} else if ctx.Row.Position == tw.Footer {
|
||||
align = tw.AlignRight
|
||||
} else {
|
||||
align = tw.AlignLeft
|
||||
}
|
||||
m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, align)
|
||||
}
|
||||
|
||||
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 := tw.AlignCenter // Default for headers
|
||||
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK {
|
||||
headerAlign = headerCellCtx.Align
|
||||
if headerAlign == tw.AlignNone || headerAlign == tw.Empty {
|
||||
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
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
505
vendor/github.com/olekukonko/tablewriter/renderer/ocean.go
generated
vendored
Normal file
505
vendor/github.com/olekukonko/tablewriter/renderer/ocean.go
generated
vendored
Normal file
@@ -0,0 +1,505 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"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)
|
||||
}
|
||||
// The batch renderer (table.go/renderHeader) will call Line() for the top border
|
||||
// and for the header separator if its main config t.config says so.
|
||||
// So, Ocean.Header should *not* draw these itself when in batch mode.
|
||||
// For true streaming, table.go's streamRenderHeader would make these Line() calls.
|
||||
|
||||
// Decision: Ocean.Header *only* renders header content.
|
||||
// Lines (top border, header separator) are managed by the caller (batch or stream logic in table.go).
|
||||
|
||||
if !o.widthsFinalized {
|
||||
o.logger.Error("Ocean.Header: Cannot render content, widths are not finalized.")
|
||||
// o.headerContentRendered = true; // No, content wasn't rendered.
|
||||
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.")
|
||||
// If header is empty, table.go's renderHeader might still call Line() for the separator.
|
||||
// o.headerContentRendered remains false if no content.
|
||||
}
|
||||
// DO NOT draw the header separator line here. Let table.go's renderHeader or streamRenderHeader call o.Line().
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Top border / header separator logic:
|
||||
// If this is the very first output, table.go's batch renderHeader (or streamRenderHeader)
|
||||
// should have already called Line() for top border and header separator.
|
||||
// If Header() was called but rendered no content, table.go's renderHeader would still call Line() for the separator.
|
||||
// If Header() was never called by table.go (e.g. streaming rows directly after Start()),
|
||||
// then table.go's streamAppendRow needs to handle initial lines.
|
||||
|
||||
// Decision: Ocean.Row *only* renders row content.
|
||||
|
||||
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).")
|
||||
}
|
||||
// Separator line before footer:
|
||||
// This should be handled by table.go's renderFooter or streamRenderFooter calling o.Line().
|
||||
|
||||
// Decision: Ocean.Footer *only* renders footer content.
|
||||
|
||||
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.")
|
||||
}
|
||||
// DO NOT draw the bottom border here. Let table.go's main Close or batch renderFooter call o.Line().
|
||||
}
|
||||
|
||||
func (o *Ocean) Line(ctx tw.Formatting) {
|
||||
// This method is now called EXTERNALLY by table.go's batch or stream logic
|
||||
// to draw all horizontal lines (top border, header sep, footer sep, bottom border).
|
||||
if !o.widthsFinalized {
|
||||
// If Line is called before widths are known (e.g. table.go's batch renderHeader trying to draw top border)
|
||||
// we must try to finalize widths from this context.
|
||||
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 := tw.DisplayWidth(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.")
|
||||
// The actual bottom border drawing is expected to be handled by table.go's
|
||||
// batch render logic (renderFooter) or stream logic (streamRenderBottomBorder)
|
||||
// by making an explicit call to o.Line() with the correct context.
|
||||
// Ocean.Close() itself does not draw the bottom border to avoid duplication.
|
||||
|
||||
// Only reset state.
|
||||
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 != (tw.Padding{}) {
|
||||
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 += tw.DisplayWidth(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 := tw.DisplayWidth(content)
|
||||
|
||||
padLeftChar := padding.Left
|
||||
if padLeftChar == tw.Empty {
|
||||
padLeftChar = tw.Space
|
||||
}
|
||||
padRightChar := padding.Right
|
||||
if padRightChar == tw.Empty {
|
||||
padRightChar = tw.Space
|
||||
}
|
||||
|
||||
padLeftDisplayWidth := tw.DisplayWidth(padLeftChar)
|
||||
padRightDisplayWidth := tw.DisplayWidth(padRightChar)
|
||||
|
||||
spaceForContentAndAlignment := cellVisualWidth - padLeftDisplayWidth - padRightDisplayWidth
|
||||
if spaceForContentAndAlignment < 0 {
|
||||
spaceForContentAndAlignment = 0
|
||||
}
|
||||
|
||||
if contentDisplayWidth > spaceForContentAndAlignment {
|
||||
content = tw.TruncateString(content, spaceForContentAndAlignment)
|
||||
contentDisplayWidth = tw.DisplayWidth(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 := tw.DisplayWidth(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(tw.TruncateString(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)
|
||||
702
vendor/github.com/olekukonko/tablewriter/renderer/svg.go
generated
vendored
Normal file
702
vendor/github.com/olekukonko/tablewriter/renderer/svg.go
generated
vendored
Normal file
@@ -0,0 +1,702 @@
|
||||
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),
|
||||
}
|
||||
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)
|
||||
}
|
||||
1150
vendor/github.com/olekukonko/tablewriter/stream.go
generated
vendored
Normal file
1150
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
|
||||
}
|
||||
2140
vendor/github.com/olekukonko/tablewriter/tablewriter.go
generated
vendored
Normal file
2140
vendor/github.com/olekukonko/tablewriter/tablewriter.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
46
vendor/github.com/olekukonko/tablewriter/tw/cell.go
generated
vendored
Normal file
46
vendor/github.com/olekukonko/tablewriter/tw/cell.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
package tw
|
||||
|
||||
// CellFormatting holds formatting options for table cells.
|
||||
type CellFormatting struct {
|
||||
Alignment Align // Text alignment within the cell (e.g., Left, Right, Center)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
ColumnAligns []Align // Per-column alignment overrides
|
||||
ColMaxWidths CellWidth // Per-column maximum width overrides
|
||||
}
|
||||
|
||||
type CellWidth struct {
|
||||
Global int
|
||||
PerColumn Mapper[int, int]
|
||||
}
|
||||
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() }
|
||||
362
vendor/github.com/olekukonko/tablewriter/tw/fn.go
generated
vendored
Normal file
362
vendor/github.com/olekukonko/tablewriter/tw/fn.go
generated
vendored
Normal file
@@ -0,0 +1,362 @@
|
||||
// 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 (
|
||||
"bytes" // For buffering string output
|
||||
"github.com/mattn/go-runewidth" // For calculating display width of Unicode characters
|
||||
"math" // For mathematical operations like ceiling
|
||||
"regexp" // For regular expression handling of ANSI codes
|
||||
"strconv" // For string-to-number conversions
|
||||
"strings" // For string manipulation utilities
|
||||
"unicode" // For Unicode character classification
|
||||
"unicode/utf8" // For UTF-8 rune handling
|
||||
)
|
||||
|
||||
// ansi is a compiled regex pattern used to strip ANSI escape codes.
|
||||
// These codes are used in terminal output for styling and are invisible in rendered text.
|
||||
var ansi = CompileANSIFilter()
|
||||
|
||||
// CompileANSIFilter constructs and compiles a regex for matching ANSI sequences.
|
||||
// It supports both control sequences (CSI) and operating system commands (OSC) like hyperlinks.
|
||||
func CompileANSIFilter() *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 + ")")
|
||||
}
|
||||
|
||||
// DisplayWidth calculates the visual width of a string, excluding ANSI escape sequences.
|
||||
// It uses go-runewidth to handle Unicode characters correctly.
|
||||
func DisplayWidth(str string) int {
|
||||
// Strip ANSI codes before calculating width to avoid counting invisible characters
|
||||
return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
|
||||
}
|
||||
|
||||
// TruncateString shortens a string to a specified maximum display width while preserving ANSI color codes.
|
||||
// An optional suffix (e.g., "...") is appended if truncation occurs.
|
||||
func TruncateString(s string, maxWidth int, suffix ...string) string {
|
||||
// If maxWidth is 0 or negative, return an empty string
|
||||
if maxWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Join suffix slices into a single string and calculate its display width
|
||||
suffixStr := strings.Join(suffix, " ")
|
||||
suffixDisplayWidth := 0
|
||||
if len(suffixStr) > 0 {
|
||||
// Strip ANSI from suffix for accurate width calculation
|
||||
suffixDisplayWidth = runewidth.StringWidth(ansi.ReplaceAllLiteralString(suffixStr, ""))
|
||||
}
|
||||
|
||||
// Check if the string (without ANSI) plus suffix fits within maxWidth
|
||||
strippedS := ansi.ReplaceAllLiteralString(s, "")
|
||||
if runewidth.StringWidth(strippedS)+suffixDisplayWidth <= maxWidth {
|
||||
// If it fits, return the original string (with ANSI) plus suffix
|
||||
return s + suffixStr
|
||||
}
|
||||
|
||||
// Handle edge case: maxWidth is too small for even the suffix
|
||||
if maxWidth < suffixDisplayWidth {
|
||||
// Try truncating the string without suffix
|
||||
return TruncateString(s, maxWidth) // Recursive call without suffix
|
||||
}
|
||||
// Handle edge case: maxWidth exactly equals suffix width
|
||||
if maxWidth == suffixDisplayWidth {
|
||||
if runewidth.StringWidth(strippedS) > 0 {
|
||||
// If there's content, it's fully truncated; return suffix
|
||||
return suffixStr
|
||||
}
|
||||
return "" // No content and no space for content; return empty string
|
||||
}
|
||||
|
||||
// Calculate the maximum width available for the content (excluding suffix)
|
||||
targetContentDisplayWidth := maxWidth - suffixDisplayWidth
|
||||
|
||||
var contentBuf bytes.Buffer // Buffer for building truncated content
|
||||
var currentContentDisplayWidth int // Tracks display width of content
|
||||
var ansiSeqBuf bytes.Buffer // Buffer for collecting ANSI sequences
|
||||
inAnsiSequence := false // Tracks if we're inside an ANSI sequence
|
||||
|
||||
// Iterate over runes to build content while respecting maxWidth
|
||||
for _, r := range s {
|
||||
if r == '\x1b' { // Start of ANSI escape sequence
|
||||
if inAnsiSequence {
|
||||
// Unexpected new ESC; flush existing sequence
|
||||
contentBuf.Write(ansiSeqBuf.Bytes())
|
||||
ansiSeqBuf.Reset()
|
||||
}
|
||||
inAnsiSequence = true
|
||||
ansiSeqBuf.WriteRune(r)
|
||||
} else if inAnsiSequence {
|
||||
ansiSeqBuf.WriteRune(r)
|
||||
// Detect end of common ANSI sequences (e.g., SGR 'm' or CSI terminators)
|
||||
if r == 'm' || (ansiSeqBuf.Len() > 2 && ansiSeqBuf.Bytes()[1] == '[' && r >= '@' && r <= '~') {
|
||||
inAnsiSequence = false
|
||||
contentBuf.Write(ansiSeqBuf.Bytes()) // Append completed sequence
|
||||
ansiSeqBuf.Reset()
|
||||
} else if ansiSeqBuf.Len() > 128 { // Prevent buffer overflow for malformed sequences
|
||||
inAnsiSequence = false
|
||||
contentBuf.Write(ansiSeqBuf.Bytes())
|
||||
ansiSeqBuf.Reset()
|
||||
}
|
||||
} else {
|
||||
// Handle displayable characters
|
||||
runeDisplayWidth := runewidth.RuneWidth(r)
|
||||
if currentContentDisplayWidth+runeDisplayWidth > targetContentDisplayWidth {
|
||||
// Adding this rune would exceed the content width; stop here
|
||||
break
|
||||
}
|
||||
contentBuf.WriteRune(r)
|
||||
currentContentDisplayWidth += runeDisplayWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Append any unterminated ANSI sequence
|
||||
if ansiSeqBuf.Len() > 0 {
|
||||
contentBuf.Write(ansiSeqBuf.Bytes())
|
||||
}
|
||||
|
||||
finalContent := contentBuf.String()
|
||||
|
||||
// Append suffix if content was truncated or if suffix is provided and content exists
|
||||
if runewidth.StringWidth(ansi.ReplaceAllLiteralString(finalContent, "")) < runewidth.StringWidth(strippedS) {
|
||||
// Content was truncated; append suffix
|
||||
return finalContent + suffixStr
|
||||
} else if len(suffixStr) > 0 && len(finalContent) > 0 {
|
||||
// No truncation but suffix exists; append it
|
||||
return finalContent + suffixStr
|
||||
} else if len(suffixStr) > 0 && len(strippedS) == 0 {
|
||||
// Original string was empty; return suffix
|
||||
return suffixStr
|
||||
}
|
||||
|
||||
// Return content as is (with preserved ANSI codes)
|
||||
return finalContent
|
||||
}
|
||||
|
||||
// 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 - DisplayWidth(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 - DisplayWidth(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 - DisplayWidth(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 := DisplayWidth(s)
|
||||
if sDisplayWidth > totalWidth {
|
||||
return TruncateString(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 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 or whitespace-only groups
|
||||
for _, s := range runes {
|
||||
if len(s) > 0 && strings.TrimSpace(string(s)) != "" {
|
||||
entries = append(entries, string(s))
|
||||
}
|
||||
}
|
||||
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 := DisplayWidth(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
|
||||
}
|
||||
128
vendor/github.com/olekukonko/tablewriter/tw/renderer.go
generated
vendored
Normal file
128
vendor/github.com/olekukonko/tablewriter/tw/renderer.go
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
package tw
|
||||
|
||||
import (
|
||||
"github.com/olekukonko/ll"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Renderer defines the interface for rendering tables to an io.Writer.
|
||||
// Implementations must handle headers, rows, footers, and separator lines.
|
||||
type Renderer interface {
|
||||
Start(w io.Writer) error
|
||||
Header(headers [][]string, ctx Formatting) // Renders table header
|
||||
Row(row []string, ctx Formatting) // Renders a single row
|
||||
Footer(footers [][]string, ctx Formatting) // Renders table footer
|
||||
Line(ctx Formatting) // Renders separator line
|
||||
Config() Rendition // Returns renderer config
|
||||
Close() error // Gets Rendition form Blueprint
|
||||
Logger(logger *ll.Logger) // send logger to renderers
|
||||
}
|
||||
|
||||
// Rendition holds the configuration for the default renderer.
|
||||
type Rendition struct {
|
||||
Borders Border // Border visibility settings
|
||||
Symbols Symbols // Symbols used for table drawing
|
||||
Settings Settings // Rendering behavior settings
|
||||
Streaming bool
|
||||
}
|
||||
|
||||
// Renditioning has a method to update its rendition.
|
||||
// Let's define an optional interface for this.
|
||||
type Renditioning interface {
|
||||
Rendition(r Rendition)
|
||||
}
|
||||
|
||||
// Formatting encapsulates the complete formatting context for a table row.
|
||||
// It provides all necessary information to render a row correctly within the table structure.
|
||||
type Formatting struct {
|
||||
Row RowContext // Detailed configuration for the row and its cells
|
||||
Level Level // Hierarchical level (Header, Body, Footer) affecting line drawing
|
||||
HasFooter bool // Indicates if the table includes a footer section
|
||||
IsSubRow bool // Marks this as a continuation or padding line in multi-line rows
|
||||
NormalizedWidths Mapper[int, int]
|
||||
}
|
||||
|
||||
// CellContext defines the properties and formatting state of an individual table cell.
|
||||
type CellContext struct {
|
||||
Data string // Content to be displayed in the cell, provided by the caller
|
||||
Align Align // Text alignment within the cell (Left, Right, Center, Skip)
|
||||
Padding Padding // Padding characters surrounding the cell content
|
||||
Width int // Suggested width (often overridden by Row.Widths)
|
||||
Merge MergeState // Details about cell spanning across rows or columns
|
||||
}
|
||||
|
||||
// MergeState captures how a cell merges across different directions.
|
||||
type MergeState struct {
|
||||
Vertical MergeStateOption // Properties for vertical merging (across rows)
|
||||
Horizontal MergeStateOption // Properties for horizontal merging (across columns)
|
||||
Hierarchical MergeStateOption // Properties for nested/hierarchical merging
|
||||
}
|
||||
|
||||
// MergeStateOption represents common attributes for merging in a specific direction.
|
||||
type MergeStateOption struct {
|
||||
Present bool // True if this merge direction is active
|
||||
Span int // Number of cells this merge spans
|
||||
Start bool // True if this cell is the starting point of the merge
|
||||
End bool // True if this cell is the ending point of the merge
|
||||
}
|
||||
|
||||
// RowContext manages layout properties and relationships for a row and its columns.
|
||||
// It maintains state about the current row and its neighbors for proper rendering.
|
||||
type RowContext struct {
|
||||
Position Position // Section of the table (Header, Row, Footer)
|
||||
Location Location // Boundary position (First, Middle, End)
|
||||
Current map[int]CellContext // Cells in this row, indexed by column
|
||||
Previous map[int]CellContext // Cells from the row above; nil if none
|
||||
Next map[int]CellContext // Cells from the row below; nil if none
|
||||
Widths Mapper[int, int] // Computed widths for each column
|
||||
ColMaxWidths CellWidth // Maximum allowed width per column
|
||||
}
|
||||
|
||||
func (r RowContext) GetCell(col int) CellContext {
|
||||
return r.Current[col]
|
||||
}
|
||||
|
||||
// Separators controls the visibility of separators in the table.
|
||||
type Separators struct {
|
||||
ShowHeader State // Controls header separator visibility
|
||||
ShowFooter State // Controls footer separator visibility
|
||||
BetweenRows State // Determines if lines appear between rows
|
||||
BetweenColumns State // Determines if separators appear between columns
|
||||
}
|
||||
|
||||
// Lines manages the visibility of table boundary lines.
|
||||
type Lines struct {
|
||||
ShowTop State // Top border visibility
|
||||
ShowBottom State // Bottom border visibility
|
||||
ShowHeaderLine State // Header separator line visibility
|
||||
ShowFooterLine State // Footer separator line visibility
|
||||
}
|
||||
|
||||
// Settings holds configuration preferences for rendering behavior.
|
||||
type Settings struct {
|
||||
Separators Separators // Separator visibility settings
|
||||
Lines Lines // Line visibility settings
|
||||
CompactMode State // Reserved for future compact rendering (unused)
|
||||
// Cushion State
|
||||
}
|
||||
|
||||
// Border defines the visibility states of table borders.
|
||||
type Border struct {
|
||||
Left State // Left border visibility
|
||||
Right State // Right border visibility
|
||||
Top State // Top border visibility
|
||||
Bottom State // Bottom border visibility
|
||||
}
|
||||
|
||||
// BorderNone defines a border configuration with all sides disabled.
|
||||
var (
|
||||
PaddingNone = Padding{Left: Empty, Right: Empty, Top: Empty, Bottom: Empty}
|
||||
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}
|
||||
)
|
||||
|
||||
type StreamConfig struct {
|
||||
Enable bool
|
||||
Widths CellWidth // Cell/column widths
|
||||
}
|
||||
147
vendor/github.com/olekukonko/tablewriter/tw/slicer.go
generated
vendored
Normal file
147
vendor/github.com/olekukonko/tablewriter/tw/slicer.go
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
package tw
|
||||
|
||||
// Slicer is a generic slice type that provides additional methods for slice manipulation.
|
||||
type Slicer[T any] []T
|
||||
|
||||
// NewSlicer creates and returns a new initialized Slicer.
|
||||
func NewSlicer[T any]() Slicer[T] {
|
||||
return make(Slicer[T], 0)
|
||||
}
|
||||
|
||||
// Get returns the element at the specified index.
|
||||
// Returns the zero value if the index is out of bounds or the slice is nil.
|
||||
func (s Slicer[T]) Get(index int) T {
|
||||
if s == nil || index < 0 || index >= len(s) {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return s[index]
|
||||
}
|
||||
|
||||
// GetOK returns the element at the specified index and a boolean indicating whether the index was valid.
|
||||
func (s Slicer[T]) GetOK(index int) (T, bool) {
|
||||
if s == nil || index < 0 || index >= len(s) {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
return s[index], true
|
||||
}
|
||||
|
||||
// Append appends elements to the slice and returns the new slice.
|
||||
func (s Slicer[T]) Append(elements ...T) Slicer[T] {
|
||||
return append(s, elements...)
|
||||
}
|
||||
|
||||
// Prepend adds elements to the beginning of the slice and returns the new slice.
|
||||
func (s Slicer[T]) Prepend(elements ...T) Slicer[T] {
|
||||
return append(elements, s...)
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the slice.
|
||||
// Returns 0 if the slice is nil.
|
||||
func (s Slicer[T]) Len() int {
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the slice is nil or has zero elements.
|
||||
func (s Slicer[T]) IsEmpty() bool {
|
||||
return s.Len() == 0
|
||||
}
|
||||
|
||||
// Has returns true if the index exists in the slice.
|
||||
func (s Slicer[T]) Has(index int) bool {
|
||||
return index >= 0 && index < s.Len()
|
||||
}
|
||||
|
||||
// First returns the first element of the slice, or the zero value if empty.
|
||||
func (s Slicer[T]) First() T {
|
||||
return s.Get(0)
|
||||
}
|
||||
|
||||
// Last returns the last element of the slice, or the zero value if empty.
|
||||
func (s Slicer[T]) Last() T {
|
||||
return s.Get(s.Len() - 1)
|
||||
}
|
||||
|
||||
// Each iterates over each element in the slice and calls the provided function.
|
||||
// Does nothing if the slice is nil.
|
||||
func (s Slicer[T]) Each(fn func(T)) {
|
||||
if s != nil {
|
||||
for _, v := range s {
|
||||
fn(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a new Slicer containing only elements that satisfy the predicate.
|
||||
func (s Slicer[T]) Filter(fn func(T) bool) Slicer[T] {
|
||||
result := NewSlicer[T]()
|
||||
if s != nil {
|
||||
for _, v := range s {
|
||||
if fn(v) {
|
||||
result = result.Append(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Map returns a new Slicer with each element transformed by the provided function.
|
||||
func (s Slicer[T]) Map(fn func(T) T) Slicer[T] {
|
||||
result := NewSlicer[T]()
|
||||
if s != nil {
|
||||
for _, v := range s {
|
||||
result = result.Append(fn(v))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Contains returns true if the slice contains an element that satisfies the predicate.
|
||||
func (s Slicer[T]) Contains(fn func(T) bool) bool {
|
||||
if s != nil {
|
||||
for _, v := range s {
|
||||
if fn(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Find returns the first element that satisfies the predicate, along with a boolean indicating if it was found.
|
||||
func (s Slicer[T]) Find(fn func(T) bool) (T, bool) {
|
||||
if s != nil {
|
||||
for _, v := range s {
|
||||
if fn(v) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Clone returns a shallow copy of the Slicer.
|
||||
func (s Slicer[T]) Clone() Slicer[T] {
|
||||
result := NewSlicer[T]()
|
||||
if s != nil {
|
||||
result = append(result, s...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SlicerToMapper converts a Slicer of KeyValuePair to a Mapper.
|
||||
func SlicerToMapper[K comparable, V any](s Slicer[KeyValuePair[K, V]]) Mapper[K, V] {
|
||||
result := make(Mapper[K, V])
|
||||
if s == nil {
|
||||
return result
|
||||
}
|
||||
for _, pair := range s {
|
||||
result[pair.Key] = pair.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
51
vendor/github.com/olekukonko/tablewriter/tw/state.go
generated
vendored
Normal file
51
vendor/github.com/olekukonko/tablewriter/tw/state.go
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package tw
|
||||
|
||||
// State represents an on/off state
|
||||
type State int
|
||||
|
||||
// Public: Methods for State type
|
||||
|
||||
// Enabled checks if the state is on
|
||||
func (o State) Enabled() bool { return o == Success }
|
||||
|
||||
// Default checks if the state is unknown
|
||||
func (o State) Default() bool { return o == Unknown }
|
||||
|
||||
// Disabled checks if the state is off
|
||||
func (o State) Disabled() bool { return o == Fail }
|
||||
|
||||
// Toggle switches the state between on and off
|
||||
func (o State) Toggle() State {
|
||||
if o == Fail {
|
||||
return Success
|
||||
}
|
||||
return Fail
|
||||
}
|
||||
|
||||
// Cond executes a condition if the state is enabled
|
||||
func (o State) Cond(c func() bool) bool {
|
||||
if o.Enabled() {
|
||||
return c()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Or returns this state if enabled, else the provided state
|
||||
func (o State) Or(c State) State {
|
||||
if o.Enabled() {
|
||||
return o
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// String returns the string representation of the state
|
||||
func (o State) String() string {
|
||||
if o.Enabled() {
|
||||
return "on"
|
||||
}
|
||||
|
||||
if o.Disabled() {
|
||||
return "off"
|
||||
}
|
||||
return "undefined"
|
||||
}
|
||||
1012
vendor/github.com/olekukonko/tablewriter/tw/symbols.go
generated
vendored
Normal file
1012
vendor/github.com/olekukonko/tablewriter/tw/symbols.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
105
vendor/github.com/olekukonko/tablewriter/tw/tw.go
generated
vendored
Normal file
105
vendor/github.com/olekukonko/tablewriter/tw/tw.go
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
package tw
|
||||
|
||||
// Operation Status Constants
|
||||
// Used to indicate the success or failure of operations
|
||||
const (
|
||||
Pending = 0 // Operation failed
|
||||
Fail = -1 // Operation failed
|
||||
Success = 1 // Operation succeeded
|
||||
|
||||
MinimumColumnWidth = 8
|
||||
)
|
||||
|
||||
const (
|
||||
Empty = ""
|
||||
Skip = ""
|
||||
Space = " "
|
||||
NewLine = "\n"
|
||||
)
|
||||
|
||||
// Feature State Constants
|
||||
// Represents enabled/disabled states for features
|
||||
const (
|
||||
Unknown State = Pending // Feature is enabled
|
||||
On State = Success // Feature is enabled
|
||||
Off State = Fail // Feature is disabled
|
||||
)
|
||||
|
||||
// Table Alignment Constants
|
||||
// Defines text alignment options for table content
|
||||
const (
|
||||
AlignNone Align = "none" // Center-aligned text
|
||||
AlignCenter Align = "center" // Center-aligned text
|
||||
AlignRight Align = "right" // Right-aligned text
|
||||
AlignLeft Align = "left" // Left-aligned text
|
||||
AlignDefault = AlignLeft // Left-aligned text
|
||||
)
|
||||
|
||||
const (
|
||||
Header Position = "header" // Table header section
|
||||
Row Position = "row" // Table row section
|
||||
Footer Position = "footer" // Table footer section
|
||||
)
|
||||
|
||||
const (
|
||||
LevelHeader Level = iota // Topmost line position
|
||||
LevelBody // LevelBody line position
|
||||
LevelFooter // LevelFooter line position
|
||||
)
|
||||
|
||||
const (
|
||||
LocationFirst Location = "first" // Topmost line position
|
||||
LocationMiddle Location = "middle" // LevelBody line position
|
||||
LocationEnd Location = "end" // LevelFooter line position
|
||||
)
|
||||
|
||||
const (
|
||||
SectionHeader = "heder"
|
||||
SectionRow = "row"
|
||||
SectionFooter = "footer"
|
||||
)
|
||||
|
||||
// Text Wrapping Constants
|
||||
// Defines text wrapping behavior in table cells
|
||||
const (
|
||||
WrapNone = iota // No wrapping
|
||||
WrapNormal // Standard word wrapping
|
||||
WrapTruncate // Truncate text with ellipsis
|
||||
WrapBreak // Break words to fit
|
||||
)
|
||||
|
||||
// Cell Merge Constants
|
||||
// Specifies cell merging behavior in tables
|
||||
|
||||
const (
|
||||
MergeNone = iota // No merging
|
||||
MergeVertical // Merge cells vertically
|
||||
MergeHorizontal // Merge cells horizontally
|
||||
MergeBoth // Merge both vertically and horizontally
|
||||
MergeHierarchical // Hierarchical merging
|
||||
)
|
||||
|
||||
// Special Character Constants
|
||||
// Defines special characters used in formatting
|
||||
const (
|
||||
CharEllipsis = "…" // Ellipsis character for truncation
|
||||
CharBreak = "↩" // Break character for wrapping
|
||||
)
|
||||
|
||||
type Spot int
|
||||
|
||||
const (
|
||||
SpotNone Spot = iota
|
||||
SpotTopLeft
|
||||
SpotTopCenter
|
||||
SpotTopRight
|
||||
SpotBottomLeft
|
||||
SpotBottomCenter // Default for legacy SetCaption
|
||||
SpotBottomRight
|
||||
SpotLeftTop
|
||||
SpotLeftCenter
|
||||
SpotLeftBottom
|
||||
SpotRightTop
|
||||
SpotRightCenter
|
||||
SpotRIghtBottom
|
||||
)
|
||||
153
vendor/github.com/olekukonko/tablewriter/tw/types.go
generated
vendored
Normal file
153
vendor/github.com/olekukonko/tablewriter/tw/types.go
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
// Package tw defines types and constants for table formatting and configuration,
|
||||
// including validation logic for various table properties.
|
||||
package tw
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/errors"
|
||||
"strings"
|
||||
) // Custom error handling library
|
||||
|
||||
// Position defines where formatting applies in the table (e.g., header, footer, or rows).
|
||||
type Position string
|
||||
|
||||
// Validate checks if the Position is one of the allowed values: Header, Footer, or Row.
|
||||
func (pos Position) Validate() error {
|
||||
switch pos {
|
||||
case Header, Footer, Row:
|
||||
return nil // Valid position
|
||||
}
|
||||
// Return an error for any unrecognized position
|
||||
return errors.New("invalid position")
|
||||
}
|
||||
|
||||
// Filter defines a function type for processing cell content.
|
||||
// It takes a slice of strings (representing cell data) and returns a processed slice.
|
||||
type Filter func([]string) []string
|
||||
|
||||
// Formatter defines an interface for types that can format themselves into a string.
|
||||
// Used for custom formatting of table cell content.
|
||||
type Formatter interface {
|
||||
Format() string // Returns the formatted string representation
|
||||
}
|
||||
|
||||
// Align specifies the text alignment within a table cell.
|
||||
type Align string
|
||||
|
||||
// Validate checks if the Align is one of the allowed values: None, Center, Left, or Right.
|
||||
func (a Align) Validate() error {
|
||||
switch a {
|
||||
case AlignNone, AlignCenter, AlignLeft, AlignRight:
|
||||
return nil // Valid alignment
|
||||
}
|
||||
// Return an error for any unrecognized alignment
|
||||
return errors.New("invalid align")
|
||||
}
|
||||
|
||||
type Alignment []Align
|
||||
|
||||
func (a Alignment) String() string {
|
||||
var str strings.Builder
|
||||
for i, a := range a {
|
||||
if i > 0 {
|
||||
str.WriteString("; ")
|
||||
}
|
||||
str.WriteString(fmt.Sprint(i))
|
||||
str.WriteString("=")
|
||||
str.WriteString(string(a))
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
func (a Alignment) Add(aligns ...Align) Alignment {
|
||||
aa := make(Alignment, len(aligns))
|
||||
copy(aa, aligns)
|
||||
return aa
|
||||
}
|
||||
|
||||
func (a Alignment) Set(col int, align Align) Alignment {
|
||||
if col >= 0 && col < len(a) {
|
||||
a[col] = align
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Copy creates a new independent copy of the Alignment
|
||||
func (a Alignment) Copy() Alignment {
|
||||
aa := make(Alignment, len(a))
|
||||
copy(aa, a)
|
||||
return aa
|
||||
}
|
||||
|
||||
// Level indicates the vertical position of a line in the table (e.g., header, body, or footer).
|
||||
type Level int
|
||||
|
||||
// Validate checks if the Level is one of the allowed values: Header, Body, or Footer.
|
||||
func (l Level) Validate() error {
|
||||
switch l {
|
||||
case LevelHeader, LevelBody, LevelFooter:
|
||||
return nil // Valid level
|
||||
}
|
||||
// Return an error for any unrecognized level
|
||||
return errors.New("invalid level")
|
||||
}
|
||||
|
||||
// Location specifies the horizontal position of a cell or column within a table row.
|
||||
type Location string
|
||||
|
||||
// Validate checks if the Location is one of the allowed values: First, Middle, or End.
|
||||
func (l Location) Validate() error {
|
||||
switch l {
|
||||
case LocationFirst, LocationMiddle, LocationEnd:
|
||||
return nil // Valid location
|
||||
}
|
||||
// Return an error for any unrecognized location
|
||||
return errors.New("invalid location")
|
||||
}
|
||||
|
||||
type Caption struct {
|
||||
Text string
|
||||
Spot Spot
|
||||
Align Align
|
||||
Width int
|
||||
}
|
||||
|
||||
func (c Caption) WithText(text string) Caption {
|
||||
c.Text = text
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Caption) WithSpot(spot Spot) Caption {
|
||||
c.Spot = spot
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Caption) WithAlign(align Align) Caption {
|
||||
c.Align = align
|
||||
return c
|
||||
}
|
||||
|
||||
func (c Caption) WithWidth(width int) Caption {
|
||||
c.Width = width
|
||||
return c
|
||||
}
|
||||
|
||||
// Padding defines custom padding characters for a cell
|
||||
type Padding struct {
|
||||
Left string
|
||||
Right string
|
||||
Top string
|
||||
Bottom string
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
Hide State
|
||||
}
|
||||
|
||||
// Behavior defines table behavior settings that control features like auto-hiding columns and trimming spaces.
|
||||
type Behavior struct {
|
||||
AutoHide State // Controls whether empty columns are automatically hidden (ignored in streaming mode)
|
||||
TrimSpace State // Controls whether leading/trailing spaces are trimmed from cell content
|
||||
Header Control
|
||||
Footer Control
|
||||
}
|
||||
93
vendor/github.com/olekukonko/tablewriter/util.go
generated
vendored
93
vendor/github.com/olekukonko/tablewriter/util.go
generated
vendored
@@ -1,93 +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
|
||||
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
|
||||
|
||||
func DisplayWidth(str string) int {
|
||||
return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
|
||||
}
|
||||
|
||||
// Simple Condition for string
|
||||
// Returns value based on condition
|
||||
func ConditionString(cond bool, valid, inValid string) string {
|
||||
if cond {
|
||||
return valid
|
||||
}
|
||||
return inValid
|
||||
}
|
||||
|
||||
func isNumOrSpace(r rune) bool {
|
||||
return ('0' <= r && r <= '9') || r == ' '
|
||||
}
|
||||
|
||||
// Format Table Header
|
||||
// Replace _ , . and spaces
|
||||
func Title(name string) string {
|
||||
origLen := len(name)
|
||||
rs := []rune(name)
|
||||
for i, r := range rs {
|
||||
switch r {
|
||||
case '_':
|
||||
rs[i] = ' '
|
||||
case '.':
|
||||
// ignore floating number 0.0
|
||||
if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) {
|
||||
rs[i] = ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
name = string(rs)
|
||||
name = strings.TrimSpace(name)
|
||||
if len(name) == 0 && origLen > 0 {
|
||||
// Keep at least one character. This is important to preserve
|
||||
// empty lines in multi-line headers/footers.
|
||||
name = " "
|
||||
}
|
||||
return strings.ToUpper(name)
|
||||
}
|
||||
|
||||
// Pad String
|
||||
// Attempts to place string in the center
|
||||
func Pad(s, pad string, width int) string {
|
||||
gap := width - DisplayWidth(s)
|
||||
if gap > 0 {
|
||||
gapLeft := int(math.Ceil(float64(gap / 2)))
|
||||
gapRight := gap - gapLeft
|
||||
return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Pad String Right position
|
||||
// This would place string at the left side of the screen
|
||||
func PadRight(s, pad string, width int) string {
|
||||
gap := width - DisplayWidth(s)
|
||||
if gap > 0 {
|
||||
return s + strings.Repeat(string(pad), gap)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Pad String Left position
|
||||
// This would place string at the right side of the screen
|
||||
func PadLeft(s, pad string, width int) string {
|
||||
gap := width - DisplayWidth(s)
|
||||
if gap > 0 {
|
||||
return strings.Repeat(string(pad), gap) + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
99
vendor/github.com/olekukonko/tablewriter/wrap.go
generated
vendored
99
vendor/github.com/olekukonko/tablewriter/wrap.go
generated
vendored
@@ -1,99 +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
|
||||
|
||||
package tablewriter
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
var (
|
||||
nl = "\n"
|
||||
sp = " "
|
||||
)
|
||||
|
||||
const defaultPenalty = 1e5
|
||||
|
||||
// Wrap wraps s into a paragraph of lines of length lim, with minimal
|
||||
// raggedness.
|
||||
func WrapString(s string, lim int) ([]string, int) {
|
||||
words := strings.Split(strings.Replace(s, nl, sp, -1), sp)
|
||||
var lines []string
|
||||
max := 0
|
||||
for _, v := range words {
|
||||
max = runewidth.StringWidth(v)
|
||||
if max > lim {
|
||||
lim = max
|
||||
}
|
||||
}
|
||||
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
|
||||
lines = append(lines, strings.Join(line, sp))
|
||||
}
|
||||
return lines, lim
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
length := make([][]int, n)
|
||||
for i := 0; i < n; i++ {
|
||||
length[i] = make([]int, n)
|
||||
length[i][i] = runewidth.StringWidth(words[i])
|
||||
for j := i + 1; j < n; j++ {
|
||||
length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j])
|
||||
}
|
||||
}
|
||||
nbrk := make([]int, n)
|
||||
cost := make([]int, n)
|
||||
for i := range cost {
|
||||
cost[i] = math.MaxInt32
|
||||
}
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if length[i][n-1] <= lim {
|
||||
cost[i] = 0
|
||||
nbrk[i] = n
|
||||
} else {
|
||||
for j := i + 1; j < n; j++ {
|
||||
d := lim - length[i][j-1]
|
||||
c := d*d + cost[j]
|
||||
if length[i][j-1] > 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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user