Compare commits
No commits in common. "0cc0b5f3102fce559631b6ab9f705ab82cd0d857" and "9c2748de1579f5e5783fcaf3243eb0040a12e475" have entirely different histories.
0cc0b5f310
...
9c2748de15
3
gdb/orm/.gitignore
vendored
3
gdb/orm/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
**/.idea/*
|
||||
cover.out
|
||||
**db
|
@ -1,157 +0,0 @@
|
||||
//
|
||||
// binder.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// makeNewPointersOf creates a map of [field name] -> pointer to fill it
|
||||
// recursively. it will go down until reaches a driver.Valuer implementation, it will stop there.
|
||||
func (b *binder) makeNewPointersOf(v reflect.Value) interface{} {
|
||||
m := map[string]interface{}{}
|
||||
actualV := v
|
||||
for actualV.Type().Kind() == reflect.Ptr {
|
||||
actualV = actualV.Elem()
|
||||
}
|
||||
if actualV.Type().Kind() == reflect.Struct {
|
||||
for i := 0; i < actualV.NumField(); i++ {
|
||||
f := actualV.Field(i)
|
||||
if (f.Type().Kind() == reflect.Struct || f.Type().Kind() == reflect.Ptr) && !f.Type().Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) {
|
||||
f = reflect.NewAt(actualV.Type().Field(i).Type, unsafe.Pointer(actualV.Field(i).UnsafeAddr()))
|
||||
fm := b.makeNewPointersOf(f).(map[string]interface{})
|
||||
for k, p := range fm {
|
||||
m[k] = p
|
||||
}
|
||||
} else {
|
||||
var fm *field
|
||||
fm = b.s.getField(actualV.Type().Field(i))
|
||||
if fm == nil {
|
||||
fm = fieldMetadata(actualV.Type().Field(i), b.s.columnConstraints)[0]
|
||||
}
|
||||
m[fm.Name] = reflect.NewAt(actualV.Field(i).Type(), unsafe.Pointer(actualV.Field(i).UnsafeAddr())).Interface()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return v.Addr().Interface()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// ptrsFor first allocates for all struct fields recursively until reaches a driver.Value impl
|
||||
// then it will put them in a map with their correct field name as key, then loops over cts
|
||||
// and for each one gets appropriate one from the map and adds it to pointer list.
|
||||
func (b *binder) ptrsFor(v reflect.Value, cts []*sql.ColumnType) []interface{} {
|
||||
ptrs := b.makeNewPointersOf(v)
|
||||
var scanInto []interface{}
|
||||
if reflect.TypeOf(ptrs).Kind() == reflect.Map {
|
||||
nameToPtr := ptrs.(map[string]interface{})
|
||||
for _, ct := range cts {
|
||||
if nameToPtr[ct.Name()] != nil {
|
||||
scanInto = append(scanInto, nameToPtr[ct.Name()])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scanInto = append(scanInto, ptrs)
|
||||
}
|
||||
|
||||
return scanInto
|
||||
}
|
||||
|
||||
type binder struct {
|
||||
s *schema
|
||||
}
|
||||
|
||||
func newBinder(s *schema) *binder {
|
||||
return &binder{s: s}
|
||||
}
|
||||
|
||||
// bind binds given rows to the given object at obj. obj should be a pointer
|
||||
func (b *binder) bind(rows *sql.Rows, obj interface{}) error {
|
||||
cts, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(obj)
|
||||
v := reflect.ValueOf(obj)
|
||||
if t.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("obj should be a ptr")
|
||||
}
|
||||
// since passed input is always a pointer one deref is necessary
|
||||
t = t.Elem()
|
||||
v = v.Elem()
|
||||
if t.Kind() == reflect.Slice {
|
||||
// getting slice elemnt type -> slice[t]
|
||||
t = t.Elem()
|
||||
for rows.Next() {
|
||||
var rowValue reflect.Value
|
||||
// Since reflect.SetupConnections returns a pointer to the type, we need to unwrap it to get actual
|
||||
rowValue = reflect.New(t).Elem()
|
||||
// till we reach a not pointer type continue newing the underlying type.
|
||||
for rowValue.IsZero() && rowValue.Type().Kind() == reflect.Ptr {
|
||||
rowValue = reflect.New(rowValue.Type().Elem()).Elem()
|
||||
}
|
||||
newCts := make([]*sql.ColumnType, len(cts))
|
||||
copy(newCts, cts)
|
||||
ptrs := b.ptrsFor(rowValue, newCts)
|
||||
err = rows.Scan(ptrs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rowValue.Type() != t {
|
||||
tmp := reflect.New(rowValue.Type())
|
||||
tmp.Elem().Set(rowValue)
|
||||
rowValue = tmp
|
||||
}
|
||||
v = reflect.Append(v, rowValue)
|
||||
}
|
||||
} else {
|
||||
for rows.Next() {
|
||||
ptrs := b.ptrsFor(v, cts)
|
||||
err = rows.Scan(ptrs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// v is either struct or slice
|
||||
reflect.ValueOf(obj).Elem().Set(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindToMap(rows *sql.Rows) ([]map[string]interface{}, error) {
|
||||
cts, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ms []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var ptrs []interface{}
|
||||
for _, ct := range cts {
|
||||
ptrs = append(ptrs, reflect.New(ct.ScanType()).Interface())
|
||||
}
|
||||
|
||||
err = rows.Scan(ptrs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
for i, ptr := range ptrs {
|
||||
m[cts[i].Name()] = reflect.ValueOf(ptr).Elem().Interface()
|
||||
}
|
||||
|
||||
ms = append(ms, m)
|
||||
}
|
||||
return ms, nil
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
//
|
||||
// binder_test.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
Name string
|
||||
Timestamps
|
||||
}
|
||||
|
||||
func (u User) ConfigureEntity(e *EntityConfigurator) {
|
||||
e.Table("users")
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
ID int
|
||||
Path string
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
t.Run("single result", func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
mock.
|
||||
ExpectQuery("SELECT .* FROM users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at", "deleted_at"}).
|
||||
AddRow(1, "amirreza", sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{}))
|
||||
rows, err := db.Query(`SELECT * FROM users`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
u := &User{}
|
||||
md := schemaOfHeavyReflectionStuff(u)
|
||||
err = newBinder(md).bind(rows, u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "amirreza", u.Name)
|
||||
})
|
||||
|
||||
t.Run("multi result", func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
mock.
|
||||
ExpectQuery("SELECT .* FROM users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "amirreza").AddRow(2, "milad"))
|
||||
|
||||
rows, err := db.Query(`SELECT * FROM users`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
md := schemaOfHeavyReflectionStuff(&User{})
|
||||
var users []*User
|
||||
err = newBinder(md).bind(rows, &users)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "amirreza", users[0].Name)
|
||||
assert.Equal(t, "milad", users[1].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindMap(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
mock.
|
||||
ExpectQuery("SELECT .* FROM users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at", "deleted_at"}).
|
||||
AddRow(1, "amirreza", sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{Time: time.Now(), Valid: true}, sql.NullTime{}))
|
||||
rows, err := db.Query(`SELECT * FROM users`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ms, err := bindToMap(rows)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, ms)
|
||||
|
||||
assert.Len(t, ms, 1)
|
||||
}
|
@ -1,192 +0,0 @@
|
||||
//
|
||||
// configurators.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper"
|
||||
)
|
||||
|
||||
type EntityConfigurator struct {
|
||||
connection string
|
||||
table string
|
||||
this Entity
|
||||
relations map[string]interface{}
|
||||
resolveRelations []func()
|
||||
columnConstraints []*FieldConfigurator
|
||||
}
|
||||
|
||||
func newEntityConfigurator() *EntityConfigurator {
|
||||
return &EntityConfigurator{}
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) Table(name string) *EntityConfigurator {
|
||||
ec.table = name
|
||||
return ec
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) Connection(name string) *EntityConfigurator {
|
||||
ec.connection = name
|
||||
return ec
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) HasMany(property Entity, config HasManyConfig) *EntityConfigurator {
|
||||
if ec.relations == nil {
|
||||
ec.relations = map[string]interface{}{}
|
||||
}
|
||||
ec.resolveRelations = append(ec.resolveRelations, func() {
|
||||
if config.PropertyForeignKey != "" && config.PropertyTable != "" {
|
||||
ec.relations[config.PropertyTable] = config
|
||||
return
|
||||
}
|
||||
configurator := newEntityConfigurator()
|
||||
property.ConfigureEntity(configurator)
|
||||
|
||||
if config.PropertyTable == "" {
|
||||
config.PropertyTable = configurator.table
|
||||
}
|
||||
|
||||
if config.PropertyForeignKey == "" {
|
||||
config.PropertyForeignKey = helper.NewPluralizeClient().Singular(ec.table) + "_id"
|
||||
}
|
||||
|
||||
ec.relations[configurator.table] = config
|
||||
|
||||
return
|
||||
})
|
||||
return ec
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) HasOne(property Entity, config HasOneConfig) *EntityConfigurator {
|
||||
if ec.relations == nil {
|
||||
ec.relations = map[string]interface{}{}
|
||||
}
|
||||
ec.resolveRelations = append(ec.resolveRelations, func() {
|
||||
if config.PropertyForeignKey != "" && config.PropertyTable != "" {
|
||||
ec.relations[config.PropertyTable] = config
|
||||
return
|
||||
}
|
||||
|
||||
configurator := newEntityConfigurator()
|
||||
property.ConfigureEntity(configurator)
|
||||
|
||||
if config.PropertyTable == "" {
|
||||
config.PropertyTable = configurator.table
|
||||
}
|
||||
if config.PropertyForeignKey == "" {
|
||||
config.PropertyForeignKey = helper.NewPluralizeClient().Singular(ec.table) + "_id"
|
||||
}
|
||||
|
||||
ec.relations[configurator.table] = config
|
||||
return
|
||||
})
|
||||
return ec
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) BelongsTo(owner Entity, config BelongsToConfig) *EntityConfigurator {
|
||||
if ec.relations == nil {
|
||||
ec.relations = map[string]interface{}{}
|
||||
}
|
||||
ec.resolveRelations = append(ec.resolveRelations, func() {
|
||||
if config.ForeignColumnName != "" && config.LocalForeignKey != "" && config.OwnerTable != "" {
|
||||
ec.relations[config.OwnerTable] = config
|
||||
return
|
||||
}
|
||||
ownerConfigurator := newEntityConfigurator()
|
||||
owner.ConfigureEntity(ownerConfigurator)
|
||||
if config.OwnerTable == "" {
|
||||
config.OwnerTable = ownerConfigurator.table
|
||||
}
|
||||
if config.LocalForeignKey == "" {
|
||||
config.LocalForeignKey = helper.NewPluralizeClient().Singular(ownerConfigurator.table) + "_id"
|
||||
}
|
||||
if config.ForeignColumnName == "" {
|
||||
config.ForeignColumnName = "id"
|
||||
}
|
||||
ec.relations[ownerConfigurator.table] = config
|
||||
})
|
||||
return ec
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) BelongsToMany(owner Entity, config BelongsToManyConfig) *EntityConfigurator {
|
||||
if ec.relations == nil {
|
||||
ec.relations = map[string]interface{}{}
|
||||
}
|
||||
ec.resolveRelations = append(ec.resolveRelations, func() {
|
||||
ownerConfigurator := newEntityConfigurator()
|
||||
owner.ConfigureEntity(ownerConfigurator)
|
||||
|
||||
if config.OwnerLookupColumn == "" {
|
||||
var pkName string
|
||||
for _, field := range genericFieldsOf(owner) {
|
||||
if field.IsPK {
|
||||
pkName = field.Name
|
||||
}
|
||||
}
|
||||
config.OwnerLookupColumn = pkName
|
||||
|
||||
}
|
||||
if config.OwnerTable == "" {
|
||||
config.OwnerTable = ownerConfigurator.table
|
||||
}
|
||||
if config.IntermediateTable == "" {
|
||||
panic("cannot infer intermediate table yet")
|
||||
}
|
||||
if config.IntermediatePropertyID == "" {
|
||||
config.IntermediatePropertyID = helper.NewPluralizeClient().Singular(ownerConfigurator.table) + "_id"
|
||||
}
|
||||
if config.IntermediateOwnerID == "" {
|
||||
config.IntermediateOwnerID = helper.NewPluralizeClient().Singular(ec.table) + "_id"
|
||||
}
|
||||
|
||||
ec.relations[ownerConfigurator.table] = config
|
||||
})
|
||||
return ec
|
||||
}
|
||||
|
||||
type FieldConfigurator struct {
|
||||
fieldName string
|
||||
nullable sql.NullBool
|
||||
primaryKey bool
|
||||
column string
|
||||
isCreatedAt bool
|
||||
isUpdatedAt bool
|
||||
isDeletedAt bool
|
||||
}
|
||||
|
||||
func (ec *EntityConfigurator) Field(name string) *FieldConfigurator {
|
||||
cc := &FieldConfigurator{fieldName: name}
|
||||
ec.columnConstraints = append(ec.columnConstraints, cc)
|
||||
return cc
|
||||
}
|
||||
|
||||
func (fc *FieldConfigurator) IsPrimaryKey() *FieldConfigurator {
|
||||
fc.primaryKey = true
|
||||
return fc
|
||||
}
|
||||
|
||||
func (fc *FieldConfigurator) IsCreatedAt() *FieldConfigurator {
|
||||
fc.isCreatedAt = true
|
||||
return fc
|
||||
}
|
||||
|
||||
func (fc *FieldConfigurator) IsUpdatedAt() *FieldConfigurator {
|
||||
fc.isUpdatedAt = true
|
||||
return fc
|
||||
}
|
||||
|
||||
func (fc *FieldConfigurator) IsDeletedAt() *FieldConfigurator {
|
||||
fc.isDeletedAt = true
|
||||
return fc
|
||||
}
|
||||
|
||||
func (fc *FieldConfigurator) ColumnName(name string) *FieldConfigurator {
|
||||
fc.column = name
|
||||
return fc
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
//
|
||||
// connection.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/table"
|
||||
)
|
||||
|
||||
type connection struct {
|
||||
Name string
|
||||
Dialect *Dialect
|
||||
DB *sql.DB
|
||||
Schemas map[string]*schema
|
||||
DBSchema map[string][]columnSpec
|
||||
DatabaseValidations bool
|
||||
}
|
||||
|
||||
func (c *connection) inferedTables() []string {
|
||||
var tables []string
|
||||
for t, s := range c.Schemas {
|
||||
tables = append(tables, t)
|
||||
for _, relC := range s.relations {
|
||||
if belongsToManyConfig, is := relC.(BelongsToManyConfig); is {
|
||||
tables = append(tables, belongsToManyConfig.IntermediateTable)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tables
|
||||
}
|
||||
|
||||
func (c *connection) validateAllTablesArePresent() error {
|
||||
for _, inferedTable := range c.inferedTables() {
|
||||
if _, exists := c.DBSchema[inferedTable]; !exists {
|
||||
return fmt.Errorf("orm infered %s but it's not found in your database, your database is out of sync", inferedTable)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *connection) validateTablesSchemas() error {
|
||||
// check for entity tables: there should not be any struct field that does not have a coresponding column
|
||||
for table, sc := range c.Schemas {
|
||||
if columns, exists := c.DBSchema[table]; exists {
|
||||
for _, f := range sc.fields {
|
||||
found := false
|
||||
for _, c := range columns {
|
||||
if c.Name == f.Name {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("column %s not found while it was inferred", f.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("tables are out of sync, %s was inferred but not present in database", table)
|
||||
}
|
||||
}
|
||||
|
||||
// check for relation tables: for HasMany,HasOne relations check if OWNER pk column is in PROPERTY,
|
||||
// for BelongsToMany check intermediate table has 2 pk for two entities
|
||||
|
||||
for table, sc := range c.Schemas {
|
||||
for _, rel := range sc.relations {
|
||||
switch rel.(type) {
|
||||
case BelongsToConfig:
|
||||
columns := c.DBSchema[table]
|
||||
var found bool
|
||||
for _, col := range columns {
|
||||
if col.Name == rel.(BelongsToConfig).LocalForeignKey {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("cannot find local foreign key %s for relation", rel.(BelongsToConfig).LocalForeignKey)
|
||||
}
|
||||
case BelongsToManyConfig:
|
||||
columns := c.DBSchema[rel.(BelongsToManyConfig).IntermediateTable]
|
||||
var foundOwner bool
|
||||
var foundProperty bool
|
||||
|
||||
for _, col := range columns {
|
||||
if col.Name == rel.(BelongsToManyConfig).IntermediateOwnerID {
|
||||
foundOwner = true
|
||||
}
|
||||
if col.Name == rel.(BelongsToManyConfig).IntermediatePropertyID {
|
||||
foundProperty = true
|
||||
}
|
||||
}
|
||||
if !foundOwner || !foundProperty {
|
||||
return fmt.Errorf("table schema for %s is not correct one of foreign keys is not present", rel.(BelongsToManyConfig).IntermediateTable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *connection) Schematic() {
|
||||
fmt.Printf("SQL Dialect: %s\n", c.Dialect.DriverName)
|
||||
for t, schema := range c.Schemas {
|
||||
fmt.Printf("t: %s\n", t)
|
||||
w := table.NewWriter()
|
||||
w.AppendHeader(table.Row{"SQL Name", "Type", "Is Primary Key", "Is Virtual"})
|
||||
for _, field := range schema.fields {
|
||||
w.AppendRow(table.Row{field.Name, field.Type, field.IsPK, field.Virtual})
|
||||
}
|
||||
fmt.Println(w.Render())
|
||||
for _, rel := range schema.relations {
|
||||
switch rel.(type) {
|
||||
case HasOneConfig:
|
||||
fmt.Printf("%s 1-1 %s => %+v\n", t, rel.(HasOneConfig).PropertyTable, rel)
|
||||
case HasManyConfig:
|
||||
fmt.Printf("%s 1-N %s => %+v\n", t, rel.(HasManyConfig).PropertyTable, rel)
|
||||
|
||||
case BelongsToConfig:
|
||||
fmt.Printf("%s N-1 %s => %+v\n", t, rel.(BelongsToConfig).OwnerTable, rel)
|
||||
|
||||
case BelongsToManyConfig:
|
||||
fmt.Printf("%s N-N %s => %+v\n", t, rel.(BelongsToManyConfig).IntermediateTable, rel)
|
||||
}
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) getSchema(t string) *schema {
|
||||
return c.Schemas[t]
|
||||
}
|
||||
|
||||
func (c *connection) setSchema(e Entity, s *schema) {
|
||||
var configurator EntityConfigurator
|
||||
e.ConfigureEntity(&configurator)
|
||||
c.Schemas[configurator.table] = s
|
||||
}
|
||||
|
||||
func GetConnection(name string) *connection {
|
||||
return globalConnections[name]
|
||||
}
|
||||
|
||||
func (c *connection) exec(q string, args ...any) (sql.Result, error) {
|
||||
return c.DB.Exec(q, args...)
|
||||
}
|
||||
|
||||
func (c *connection) query(q string, args ...any) (*sql.Rows, error) {
|
||||
return c.DB.Query(q, args...)
|
||||
}
|
||||
|
||||
func (c *connection) queryRow(q string, args ...any) *sql.Row {
|
||||
return c.DB.QueryRow(q, args...)
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
//
|
||||
// dialect.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Dialect struct {
|
||||
DriverName string
|
||||
PlaceholderChar string
|
||||
IncludeIndexInPlaceholder bool
|
||||
AddTableNameInSelectColumns bool
|
||||
PlaceHolderGenerator func(n int) []string
|
||||
QueryListTables string
|
||||
QueryTableSchema string
|
||||
}
|
||||
|
||||
func getListOfTables(query string) func(db *sql.DB) ([]string, error) {
|
||||
return func(db *sql.DB) ([]string, error) {
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tables []string
|
||||
for rows.Next() {
|
||||
var table string
|
||||
err = rows.Scan(&table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, table)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
}
|
||||
|
||||
type columnSpec struct {
|
||||
//0|id|INTEGER|0||1
|
||||
Name string
|
||||
Type string
|
||||
Nullable bool
|
||||
DefaultValue sql.NullString
|
||||
IsPrimaryKey bool
|
||||
}
|
||||
|
||||
func getTableSchema(query string) func(db *sql.DB, query string) ([]columnSpec, error) {
|
||||
return func(db *sql.DB, table string) ([]columnSpec, error) {
|
||||
rows, err := db.Query(fmt.Sprintf(query, table))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var output []columnSpec
|
||||
for rows.Next() {
|
||||
var cs columnSpec
|
||||
var nullable string
|
||||
var pk int
|
||||
err = rows.Scan(&cs.Name, &cs.Type, &nullable, &cs.DefaultValue, &pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cs.Nullable = nullable == "notnull"
|
||||
cs.IsPrimaryKey = pk == 1
|
||||
output = append(output, cs)
|
||||
}
|
||||
return output, nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var Dialects = &struct {
|
||||
MySQL *Dialect
|
||||
PostgreSQL *Dialect
|
||||
SQLite3 *Dialect
|
||||
}{
|
||||
MySQL: &Dialect{
|
||||
DriverName: "mysql",
|
||||
PlaceholderChar: "?",
|
||||
IncludeIndexInPlaceholder: false,
|
||||
AddTableNameInSelectColumns: true,
|
||||
PlaceHolderGenerator: questionMarks,
|
||||
QueryListTables: "SHOW TABLES",
|
||||
QueryTableSchema: "DESCRIBE %s",
|
||||
},
|
||||
PostgreSQL: &Dialect{
|
||||
DriverName: "postgres",
|
||||
PlaceholderChar: "$",
|
||||
IncludeIndexInPlaceholder: true,
|
||||
AddTableNameInSelectColumns: true,
|
||||
PlaceHolderGenerator: postgresPlaceholder,
|
||||
QueryListTables: `\dt`,
|
||||
QueryTableSchema: `\d %s`,
|
||||
},
|
||||
SQLite3: &Dialect{
|
||||
DriverName: "sqlite3",
|
||||
PlaceholderChar: "?",
|
||||
IncludeIndexInPlaceholder: false,
|
||||
AddTableNameInSelectColumns: false,
|
||||
PlaceHolderGenerator: questionMarks,
|
||||
QueryListTables: "SELECT name FROM sqlite_schema WHERE type='table'",
|
||||
QueryTableSchema: `SELECT name,type,"notnull","dflt_value","pk" FROM PRAGMA_TABLE_INFO('%s')`,
|
||||
},
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
//
|
||||
// field.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper"
|
||||
)
|
||||
|
||||
type field struct {
|
||||
Name string
|
||||
IsPK bool
|
||||
Virtual bool
|
||||
IsCreatedAt bool
|
||||
IsUpdatedAt bool
|
||||
IsDeletedAt bool
|
||||
Nullable bool
|
||||
Default any
|
||||
Type reflect.Type
|
||||
}
|
||||
|
||||
func getFieldConfiguratorFor(fieldConfigurators []*FieldConfigurator, name string) *FieldConfigurator {
|
||||
for _, fc := range fieldConfigurators {
|
||||
if fc.fieldName == name {
|
||||
return fc
|
||||
}
|
||||
}
|
||||
return &FieldConfigurator{}
|
||||
}
|
||||
|
||||
func fieldMetadata(ft reflect.StructField, fieldConfigurators []*FieldConfigurator) []*field {
|
||||
var fms []*field
|
||||
fc := getFieldConfiguratorFor(fieldConfigurators, ft.Name)
|
||||
baseFm := &field{}
|
||||
baseFm.Type = ft.Type
|
||||
fms = append(fms, baseFm)
|
||||
if fc.column != "" {
|
||||
baseFm.Name = fc.column
|
||||
} else {
|
||||
baseFm.Name = helper.SnakeString(ft.Name)
|
||||
}
|
||||
if strings.ToLower(ft.Name) == "id" || fc.primaryKey {
|
||||
baseFm.IsPK = true
|
||||
}
|
||||
if strings.ToLower(ft.Name) == "createdat" || fc.isCreatedAt {
|
||||
baseFm.IsCreatedAt = true
|
||||
}
|
||||
if strings.ToLower(ft.Name) == "updatedat" || fc.isUpdatedAt {
|
||||
baseFm.IsUpdatedAt = true
|
||||
}
|
||||
if strings.ToLower(ft.Name) == "deletedat" || fc.isDeletedAt {
|
||||
baseFm.IsDeletedAt = true
|
||||
}
|
||||
if ft.Type.Kind() == reflect.Struct || ft.Type.Kind() == reflect.Ptr {
|
||||
t := ft.Type
|
||||
if ft.Type.Kind() == reflect.Ptr {
|
||||
t = ft.Type.Elem()
|
||||
}
|
||||
if !t.Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) {
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fms = append(fms, fieldMetadata(t.Field(i), fieldConfigurators)...)
|
||||
}
|
||||
fms = fms[1:]
|
||||
}
|
||||
}
|
||||
return fms
|
||||
}
|
654
gdb/orm/orm.go
654
gdb/orm/orm.go
@ -1,654 +0,0 @@
|
||||
//
|
||||
// orm.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
// Drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var globalConnections = map[string]*connection{}
|
||||
|
||||
// Schematic prints all information ORM inferred from your entities in startup, remember to pass
|
||||
// your entities in Entities when you call SetupConnections if you want their data inferred
|
||||
// otherwise Schematic does not print correct data since GoLobby ORM also
|
||||
// incrementally cache your entities metadata and schema.
|
||||
func Schematic() {
|
||||
for conn, connObj := range globalConnections {
|
||||
fmt.Printf("----------------%s---------------\n", conn)
|
||||
connObj.Schematic()
|
||||
fmt.Println("-----------------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectionConfig struct {
|
||||
// Name of your database connection, it's up to you to name them anything
|
||||
// just remember that having a connection name is mandatory if
|
||||
// you have multiple connections
|
||||
Name string
|
||||
// If you already have an active database connection configured pass it in this value and
|
||||
// do not pass Driver and DSN fields.
|
||||
DB *sql.DB
|
||||
// Which dialect of sql to generate queries for, you don't need it most of the times when you are using
|
||||
// traditional databases such as mysql, sqlite3, postgres.
|
||||
Dialect *Dialect
|
||||
// List of entities that you want to use for this connection, remember that you can ignore this field
|
||||
// and GoLobby ORM will build our metadata cache incrementally but you will lose schematic
|
||||
// information that we can provide you and also potentialy validations that we
|
||||
// can do with the database
|
||||
Entities []Entity
|
||||
// Database validations, check if all tables exists and also table schemas contains all necessary columns.
|
||||
// Check if all infered tables exist in your database
|
||||
DatabaseValidations bool
|
||||
}
|
||||
|
||||
// SetupConnections declares a new connections for ORM.
|
||||
func SetupConnections(configs ...ConnectionConfig) error {
|
||||
|
||||
for _, c := range configs {
|
||||
if err := setupConnection(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, conn := range globalConnections {
|
||||
if !conn.DatabaseValidations {
|
||||
continue
|
||||
}
|
||||
|
||||
tables, err := getListOfTables(conn.Dialect.QueryListTables)(conn.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
if conn.DatabaseValidations {
|
||||
spec, err := getTableSchema(conn.Dialect.QueryTableSchema)(conn.DB, table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.DBSchema[table] = spec
|
||||
} else {
|
||||
conn.DBSchema[table] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// check tables existence
|
||||
if conn.DatabaseValidations {
|
||||
err := conn.validateAllTablesArePresent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if conn.DatabaseValidations {
|
||||
err = conn.validateTablesSchemas()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupConnection(config ConnectionConfig) error {
|
||||
schemas := map[string]*schema{}
|
||||
if config.Name == "" {
|
||||
config.Name = "default"
|
||||
}
|
||||
|
||||
for _, entity := range config.Entities {
|
||||
s := schemaOfHeavyReflectionStuff(entity)
|
||||
var configurator EntityConfigurator
|
||||
entity.ConfigureEntity(&configurator)
|
||||
schemas[configurator.table] = s
|
||||
}
|
||||
|
||||
s := &connection{
|
||||
Name: config.Name,
|
||||
DB: config.DB,
|
||||
Dialect: config.Dialect,
|
||||
Schemas: schemas,
|
||||
DBSchema: make(map[string][]columnSpec),
|
||||
DatabaseValidations: config.DatabaseValidations,
|
||||
}
|
||||
|
||||
globalConnections[fmt.Sprintf("%s", config.Name)] = s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Entity defines the interface that each of your structs that
|
||||
// you want to use as database entities should have,
|
||||
// it's a simple one and its ConfigureEntity.
|
||||
type Entity interface {
|
||||
// ConfigureEntity should be defined for all of your database entities
|
||||
// and it can define Table, DB and also relations of your Entity.
|
||||
ConfigureEntity(e *EntityConfigurator)
|
||||
}
|
||||
|
||||
// InsertAll given entities into database based on their ConfigureEntity
|
||||
// we can find table and also DB name.
|
||||
func InsertAll(objs ...Entity) error {
|
||||
if len(objs) == 0 {
|
||||
return nil
|
||||
}
|
||||
s := getSchemaFor(objs[0])
|
||||
cols := s.Columns(false)
|
||||
var values [][]interface{}
|
||||
for _, obj := range objs {
|
||||
createdAtF := s.createdAt()
|
||||
if createdAtF != nil {
|
||||
genericSet(obj, createdAtF.Name, sql.NullTime{Time: time.Now(), Valid: true})
|
||||
}
|
||||
updatedAtF := s.updatedAt()
|
||||
if updatedAtF != nil {
|
||||
genericSet(obj, updatedAtF.Name, sql.NullTime{Time: time.Now(), Valid: true})
|
||||
}
|
||||
values = append(values, genericValuesOf(obj, false))
|
||||
}
|
||||
|
||||
is := insertStmt{
|
||||
PlaceHolderGenerator: s.getDialect().PlaceHolderGenerator,
|
||||
Table: s.getTable(),
|
||||
Columns: cols,
|
||||
Values: values,
|
||||
}
|
||||
|
||||
q, args := is.ToSql()
|
||||
|
||||
_, err := s.getConnection().exec(q, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert given entity into database based on their ConfigureEntity
|
||||
// we can find table and also DB name.
|
||||
func Insert(o Entity) error {
|
||||
s := getSchemaFor(o)
|
||||
cols := s.Columns(false)
|
||||
var values [][]interface{}
|
||||
createdAtF := s.createdAt()
|
||||
if createdAtF != nil {
|
||||
genericSet(o, createdAtF.Name, sql.NullTime{Time: time.Now(), Valid: true})
|
||||
}
|
||||
updatedAtF := s.updatedAt()
|
||||
if updatedAtF != nil {
|
||||
genericSet(o, updatedAtF.Name, sql.NullTime{Time: time.Now(), Valid: true})
|
||||
}
|
||||
values = append(values, genericValuesOf(o, false))
|
||||
|
||||
is := insertStmt{
|
||||
PlaceHolderGenerator: s.getDialect().PlaceHolderGenerator,
|
||||
Table: s.getTable(),
|
||||
Columns: cols,
|
||||
Values: values,
|
||||
}
|
||||
|
||||
if s.getDialect().DriverName == "postgres" {
|
||||
is.Returning = s.pkName()
|
||||
}
|
||||
q, args := is.ToSql()
|
||||
|
||||
res, err := s.getConnection().exec(q, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.pkName() != "" {
|
||||
// intermediate tables usually have no single pk column.
|
||||
s.setPK(o, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isZero(val interface{}) bool {
|
||||
switch val.(type) {
|
||||
case int64:
|
||||
return val.(int64) == 0
|
||||
case int:
|
||||
return val.(int) == 0
|
||||
case string:
|
||||
return val.(string) == ""
|
||||
default:
|
||||
return reflect.ValueOf(val).Elem().IsZero()
|
||||
}
|
||||
}
|
||||
|
||||
// Save saves given entity, if primary key is set
|
||||
// we will make an update query and if
|
||||
// primary key is zero value we will
|
||||
// insert it.
|
||||
func Save(obj Entity) error {
|
||||
if isZero(getSchemaFor(obj).getPK(obj)) {
|
||||
return Insert(obj)
|
||||
} else {
|
||||
return Update(obj)
|
||||
}
|
||||
}
|
||||
|
||||
// Find finds the Entity you want based on generic type and primary key you passed.
|
||||
func Find[T Entity](id interface{}) (T, error) {
|
||||
var q string
|
||||
out := new(T)
|
||||
md := getSchemaFor(*out)
|
||||
q, args, err := NewQueryBuilder[T](md).
|
||||
SetDialect(md.getDialect()).
|
||||
Table(md.Table).
|
||||
Select(md.Columns(true)...).
|
||||
Where(md.pkName(), id).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return *out, err
|
||||
}
|
||||
err = bind[T](out, q, args)
|
||||
|
||||
if err != nil {
|
||||
return *out, err
|
||||
}
|
||||
|
||||
return *out, nil
|
||||
}
|
||||
|
||||
func toKeyValues(obj Entity, withPK bool) []any {
|
||||
var tuples []any
|
||||
vs := genericValuesOf(obj, withPK)
|
||||
cols := getSchemaFor(obj).Columns(withPK)
|
||||
for i, col := range cols {
|
||||
tuples = append(tuples, col, vs[i])
|
||||
}
|
||||
return tuples
|
||||
}
|
||||
|
||||
// Update given Entity in database.
|
||||
func Update(obj Entity) error {
|
||||
s := getSchemaFor(obj)
|
||||
q, args, err := NewQueryBuilder[Entity](s).
|
||||
SetDialect(s.getDialect()).
|
||||
Set(toKeyValues(obj, false)...).
|
||||
Where(s.pkName(), genericGetPKValue(obj)).Table(s.Table).ToSql()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.getConnection().exec(q, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete given Entity from database
|
||||
func Delete(obj Entity) error {
|
||||
s := getSchemaFor(obj)
|
||||
genericSet(obj, "deleted_at", sql.NullTime{Time: time.Now(), Valid: true})
|
||||
query, args, err := NewQueryBuilder[Entity](s).SetDialect(s.getDialect()).Table(s.Table).Where(s.pkName(), genericGetPKValue(obj)).SetDelete().ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.getConnection().exec(query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func bind[T Entity](output interface{}, q string, args []interface{}) error {
|
||||
outputMD := getSchemaFor(*new(T))
|
||||
rows, err := outputMD.getConnection().query(q, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return newBinder(outputMD).bind(rows, output)
|
||||
}
|
||||
|
||||
// HasManyConfig contains all information we need for querying HasMany relationships.
|
||||
// We can infer both fields if you have them in standard way but you
|
||||
// can specify them if you want custom ones.
|
||||
type HasManyConfig struct {
|
||||
// PropertyTable is table of the property of HasMany relationship,
|
||||
// consider `Comment` in Post and Comment relationship,
|
||||
// each Post HasMany Comment, so PropertyTable is
|
||||
// `comments`.
|
||||
PropertyTable string
|
||||
// PropertyForeignKey is the foreign key field name in the property table,
|
||||
// for example in Post HasMany Comment, if comment has `post_id` field,
|
||||
// it's the PropertyForeignKey field.
|
||||
PropertyForeignKey string
|
||||
}
|
||||
|
||||
// HasMany configures a QueryBuilder for a HasMany relationship
|
||||
// this relationship will be defined for owner argument
|
||||
// that has many of PROPERTY generic type for example
|
||||
// HasMany[Comment](&Post{})
|
||||
// is for Post HasMany Comment relationship.
|
||||
func HasMany[PROPERTY Entity](owner Entity) *QueryBuilder[PROPERTY] {
|
||||
outSchema := getSchemaFor(*new(PROPERTY))
|
||||
|
||||
q := NewQueryBuilder[PROPERTY](outSchema)
|
||||
// getting config from our cache
|
||||
c, ok := getSchemaFor(owner).relations[outSchema.Table].(HasManyConfig)
|
||||
if !ok {
|
||||
q.err = fmt.Errorf("wrong config passed for HasMany")
|
||||
}
|
||||
|
||||
s := getSchemaFor(owner)
|
||||
return q.
|
||||
SetDialect(s.getDialect()).
|
||||
Table(c.PropertyTable).
|
||||
Select(outSchema.Columns(true)...).
|
||||
Where(c.PropertyForeignKey, genericGetPKValue(owner))
|
||||
}
|
||||
|
||||
// HasOneConfig contains all information we need for a HasOne relationship,
|
||||
// it's similar to HasManyConfig.
|
||||
type HasOneConfig struct {
|
||||
// PropertyTable is table of the property of HasOne relationship,
|
||||
// consider `HeaderPicture` in Post and HeaderPicture relationship,
|
||||
// each Post HasOne HeaderPicture, so PropertyTable is
|
||||
// `header_pictures`.
|
||||
PropertyTable string
|
||||
// PropertyForeignKey is the foreign key field name in the property table,
|
||||
// forexample in Post HasOne HeaderPicture, if header_picture has `post_id` field,
|
||||
// it's the PropertyForeignKey field.
|
||||
PropertyForeignKey string
|
||||
}
|
||||
|
||||
// HasOne configures a QueryBuilder for a HasOne relationship
|
||||
// this relationship will be defined for owner argument
|
||||
// that has one of PROPERTY generic type for example
|
||||
// HasOne[HeaderPicture](&Post{})
|
||||
// is for Post HasOne HeaderPicture relationship.
|
||||
func HasOne[PROPERTY Entity](owner Entity) *QueryBuilder[PROPERTY] {
|
||||
property := getSchemaFor(*new(PROPERTY))
|
||||
q := NewQueryBuilder[PROPERTY](property)
|
||||
c, ok := getSchemaFor(owner).relations[property.Table].(HasOneConfig)
|
||||
if !ok {
|
||||
q.err = fmt.Errorf("wrong config passed for HasOne")
|
||||
}
|
||||
|
||||
// settings default config Values
|
||||
return q.
|
||||
SetDialect(property.getDialect()).
|
||||
Table(c.PropertyTable).
|
||||
Select(property.Columns(true)...).
|
||||
Where(c.PropertyForeignKey, genericGetPKValue(owner))
|
||||
}
|
||||
|
||||
// BelongsToConfig contains all information we need for a BelongsTo relationship
|
||||
// BelongsTo is a relationship between a Comment and it's Post,
|
||||
// A Comment BelongsTo Post.
|
||||
type BelongsToConfig struct {
|
||||
// OwnerTable is the table that contains owner of a BelongsTo
|
||||
// relationship.
|
||||
OwnerTable string
|
||||
// LocalForeignKey is name of the field that links property
|
||||
// to its owner in BelongsTo relation. for example when
|
||||
// a Comment BelongsTo Post, LocalForeignKey is
|
||||
// post_id of Comment.
|
||||
LocalForeignKey string
|
||||
// ForeignColumnName is name of the field that LocalForeignKey
|
||||
// field value will point to it, for example when
|
||||
// a Comment BelongsTo Post, ForeignColumnName is
|
||||
// id of Post.
|
||||
ForeignColumnName string
|
||||
}
|
||||
|
||||
// BelongsTo configures a QueryBuilder for a BelongsTo relationship between
|
||||
// OWNER type parameter and property argument, so
|
||||
// property BelongsTo OWNER.
|
||||
func BelongsTo[OWNER Entity](property Entity) *QueryBuilder[OWNER] {
|
||||
owner := getSchemaFor(*new(OWNER))
|
||||
q := NewQueryBuilder[OWNER](owner)
|
||||
c, ok := getSchemaFor(property).relations[owner.Table].(BelongsToConfig)
|
||||
if !ok {
|
||||
q.err = fmt.Errorf("wrong config passed for BelongsTo")
|
||||
}
|
||||
|
||||
ownerIDidx := 0
|
||||
for idx, field := range owner.fields {
|
||||
if field.Name == c.LocalForeignKey {
|
||||
ownerIDidx = idx
|
||||
}
|
||||
}
|
||||
|
||||
ownerID := genericValuesOf(property, true)[ownerIDidx]
|
||||
|
||||
return q.
|
||||
SetDialect(owner.getDialect()).
|
||||
Table(c.OwnerTable).Select(owner.Columns(true)...).
|
||||
Where(c.ForeignColumnName, ownerID)
|
||||
|
||||
}
|
||||
|
||||
// BelongsToManyConfig contains information that we
|
||||
// need for creating many to many queries.
|
||||
type BelongsToManyConfig struct {
|
||||
// IntermediateTable is the name of the middle table
|
||||
// in a BelongsToMany (Many to Many) relationship.
|
||||
// for example when we have Post BelongsToMany
|
||||
// Category, this table will be post_categories
|
||||
// table, remember that this field cannot be
|
||||
// inferred.
|
||||
IntermediateTable string
|
||||
// IntermediatePropertyID is the name of the field name
|
||||
// of property foreign key in intermediate table,
|
||||
// for example when we have Post BelongsToMany
|
||||
// Category, in post_categories table, it would
|
||||
// be post_id.
|
||||
IntermediatePropertyID string
|
||||
// IntermediateOwnerID is the name of the field name
|
||||
// of property foreign key in intermediate table,
|
||||
// for example when we have Post BelongsToMany
|
||||
// Category, in post_categories table, it would
|
||||
// be category_id.
|
||||
IntermediateOwnerID string
|
||||
// Table name of the owner in BelongsToMany relation,
|
||||
// for example in Post BelongsToMany Category
|
||||
// Owner table is name of Category table
|
||||
// for example `categories`.
|
||||
OwnerTable string
|
||||
// OwnerLookupColumn is name of the field in the owner
|
||||
// table that is used in query, for example in Post BelongsToMany Category
|
||||
// Owner lookup field would be Category primary key which is id.
|
||||
OwnerLookupColumn string
|
||||
}
|
||||
|
||||
// BelongsToMany configures a QueryBuilder for a BelongsToMany relationship
|
||||
func BelongsToMany[OWNER Entity](property Entity) *QueryBuilder[OWNER] {
|
||||
out := *new(OWNER)
|
||||
outSchema := getSchemaFor(out)
|
||||
q := NewQueryBuilder[OWNER](outSchema)
|
||||
c, ok := getSchemaFor(property).relations[outSchema.Table].(BelongsToManyConfig)
|
||||
if !ok {
|
||||
q.err = fmt.Errorf("wrong config passed for HasMany")
|
||||
}
|
||||
return q.
|
||||
Select(outSchema.Columns(true)...).
|
||||
Table(outSchema.Table).
|
||||
WhereIn(c.OwnerLookupColumn, Raw(fmt.Sprintf(`SELECT %s FROM %s WHERE %s = ?`,
|
||||
c.IntermediatePropertyID,
|
||||
c.IntermediateTable, c.IntermediateOwnerID), genericGetPKValue(property)))
|
||||
}
|
||||
|
||||
// Add adds `items` to `to` using relations defined between items and to in ConfigureEntity method of `to`.
|
||||
func Add(to Entity, items ...Entity) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
rels := getSchemaFor(to).relations
|
||||
tname := getSchemaFor(items[0]).Table
|
||||
c, ok := rels[tname]
|
||||
if !ok {
|
||||
return fmt.Errorf("no config found for given to and item...")
|
||||
}
|
||||
switch c.(type) {
|
||||
case HasManyConfig:
|
||||
return addProperty(to, items...)
|
||||
case HasOneConfig:
|
||||
return addProperty(to, items[0])
|
||||
case BelongsToManyConfig:
|
||||
return addM2M(to, items...)
|
||||
default:
|
||||
return fmt.Errorf("cannot add for relation: %T", rels[getSchemaFor(items[0]).Table])
|
||||
}
|
||||
}
|
||||
|
||||
func addM2M(to Entity, items ...Entity) error {
|
||||
//TODO: Optimize this
|
||||
rels := getSchemaFor(to).relations
|
||||
tname := getSchemaFor(items[0]).Table
|
||||
c := rels[tname].(BelongsToManyConfig)
|
||||
var values [][]interface{}
|
||||
ownerPk := genericGetPKValue(to)
|
||||
for _, item := range items {
|
||||
pk := genericGetPKValue(item)
|
||||
if isZero(pk) {
|
||||
err := Insert(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pk = genericGetPKValue(item)
|
||||
}
|
||||
values = append(values, []interface{}{ownerPk, pk})
|
||||
}
|
||||
i := insertStmt{
|
||||
PlaceHolderGenerator: getSchemaFor(to).getDialect().PlaceHolderGenerator,
|
||||
Table: c.IntermediateTable,
|
||||
Columns: []string{c.IntermediateOwnerID, c.IntermediatePropertyID},
|
||||
Values: values,
|
||||
}
|
||||
|
||||
q, args := i.ToSql()
|
||||
|
||||
_, err := getConnectionFor(items[0]).DB.Exec(q, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// addHasMany(Post, comments)
|
||||
func addProperty(to Entity, items ...Entity) error {
|
||||
var lastTable string
|
||||
for _, obj := range items {
|
||||
s := getSchemaFor(obj)
|
||||
if lastTable == "" {
|
||||
lastTable = s.Table
|
||||
} else {
|
||||
if lastTable != s.Table {
|
||||
return fmt.Errorf("cannot batch insert for two different tables: %s and %s", s.Table, lastTable)
|
||||
}
|
||||
}
|
||||
}
|
||||
i := insertStmt{
|
||||
PlaceHolderGenerator: getSchemaFor(to).getDialect().PlaceHolderGenerator,
|
||||
Table: getSchemaFor(items[0]).getTable(),
|
||||
}
|
||||
ownerPKIdx := -1
|
||||
ownerPKName := getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey
|
||||
for idx, col := range getSchemaFor(items[0]).Columns(false) {
|
||||
if col == ownerPKName {
|
||||
ownerPKIdx = idx
|
||||
}
|
||||
}
|
||||
|
||||
ownerPK := genericGetPKValue(to)
|
||||
if ownerPKIdx != -1 {
|
||||
cols := getSchemaFor(items[0]).Columns(false)
|
||||
i.Columns = append(i.Columns, cols...)
|
||||
// Owner PK is present in the items struct
|
||||
for _, item := range items {
|
||||
vals := genericValuesOf(item, false)
|
||||
if cols[ownerPKIdx] != getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey {
|
||||
return fmt.Errorf("owner pk idx is not correct")
|
||||
}
|
||||
vals[ownerPKIdx] = ownerPK
|
||||
i.Values = append(i.Values, vals)
|
||||
}
|
||||
} else {
|
||||
ownerPKIdx = 0
|
||||
cols := getSchemaFor(items[0]).Columns(false)
|
||||
cols = append(cols[:ownerPKIdx+1], cols[ownerPKIdx:]...)
|
||||
cols[ownerPKIdx] = getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey
|
||||
i.Columns = append(i.Columns, cols...)
|
||||
for _, item := range items {
|
||||
vals := genericValuesOf(item, false)
|
||||
if cols[ownerPKIdx] != getSchemaFor(items[0]).relations[getSchemaFor(to).Table].(BelongsToConfig).LocalForeignKey {
|
||||
return fmt.Errorf("owner pk idx is not correct")
|
||||
}
|
||||
vals = append(vals[:ownerPKIdx+1], vals[ownerPKIdx:]...)
|
||||
vals[ownerPKIdx] = ownerPK
|
||||
i.Values = append(i.Values, vals)
|
||||
}
|
||||
}
|
||||
|
||||
q, args := i.ToSql()
|
||||
|
||||
_, err := getConnectionFor(items[0]).DB.Exec(q, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// Query creates a new QueryBuilder for given type parameter, sets dialect and table as well.
|
||||
func Query[E Entity]() *QueryBuilder[E] {
|
||||
s := getSchemaFor(*new(E))
|
||||
q := NewQueryBuilder[E](s)
|
||||
q.SetDialect(s.getDialect()).Table(s.Table)
|
||||
return q
|
||||
}
|
||||
|
||||
// ExecRaw executes given query string and arguments on given type parameter database connection.
|
||||
func ExecRaw[E Entity](q string, args ...interface{}) (int64, int64, error) {
|
||||
e := new(E)
|
||||
|
||||
res, err := getSchemaFor(*e).getSQLDB().Exec(q, args...)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return id, affected, nil
|
||||
}
|
||||
|
||||
// QueryRaw queries given query string and arguments on given type parameter database connection.
|
||||
func QueryRaw[OUTPUT Entity](q string, args ...interface{}) ([]OUTPUT, error) {
|
||||
o := new(OUTPUT)
|
||||
rows, err := getSchemaFor(*o).getSQLDB().Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var output []OUTPUT
|
||||
err = newBinder(getSchemaFor(*o)).bind(rows, &output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
@ -1,588 +0,0 @@
|
||||
//
|
||||
// orm_test.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/gdb/orm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type AuthorEmail struct {
|
||||
ID int64
|
||||
Email string
|
||||
}
|
||||
|
||||
func (a AuthorEmail) ConfigureEntity(e *orm.EntityConfigurator) {
|
||||
e.
|
||||
Table("emails").
|
||||
Connection("default").
|
||||
BelongsTo(&Post{}, orm.BelongsToConfig{})
|
||||
}
|
||||
|
||||
type HeaderPicture struct {
|
||||
ID int64
|
||||
PostID int64
|
||||
Link string
|
||||
}
|
||||
|
||||
func (h HeaderPicture) ConfigureEntity(e *orm.EntityConfigurator) {
|
||||
e.Table("header_pictures").BelongsTo(&Post{}, orm.BelongsToConfig{})
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID int64
|
||||
BodyText string
|
||||
CreatedAt sql.NullTime
|
||||
UpdatedAt sql.NullTime
|
||||
DeletedAt sql.NullTime
|
||||
}
|
||||
|
||||
func (p Post) ConfigureEntity(e *orm.EntityConfigurator) {
|
||||
e.Field("BodyText").ColumnName("body")
|
||||
e.Field("ID").ColumnName("id")
|
||||
e.
|
||||
Table("posts").
|
||||
HasMany(Comment{}, orm.HasManyConfig{}).
|
||||
HasOne(HeaderPicture{}, orm.HasOneConfig{}).
|
||||
HasOne(AuthorEmail{}, orm.HasOneConfig{}).
|
||||
BelongsToMany(Category{}, orm.BelongsToManyConfig{IntermediateTable: "post_categories"})
|
||||
|
||||
}
|
||||
|
||||
func (p *Post) Categories() ([]Category, error) {
|
||||
return orm.BelongsToMany[Category](p).All()
|
||||
}
|
||||
|
||||
func (p *Post) Comments() *orm.QueryBuilder[Comment] {
|
||||
return orm.HasMany[Comment](p)
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID int64
|
||||
PostID int64
|
||||
Body string
|
||||
}
|
||||
|
||||
func (c Comment) ConfigureEntity(e *orm.EntityConfigurator) {
|
||||
e.Table("comments").BelongsTo(&Post{}, orm.BelongsToConfig{})
|
||||
}
|
||||
|
||||
func (c *Comment) Post() (Post, error) {
|
||||
return orm.BelongsTo[Post](c).Get()
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID int64
|
||||
Title string
|
||||
}
|
||||
|
||||
func (c Category) ConfigureEntity(e *orm.EntityConfigurator) {
|
||||
e.Table("categories").BelongsToMany(Post{}, orm.BelongsToManyConfig{IntermediateTable: "post_categories"})
|
||||
}
|
||||
|
||||
func (c Category) Posts() ([]Post, error) {
|
||||
return orm.BelongsToMany[Post](c).All()
|
||||
}
|
||||
|
||||
// enough models let's test
|
||||
// Entities is mandatory
|
||||
// Errors should be carried
|
||||
|
||||
func setup() error {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, post_id INTEGER, body text)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return orm.SetupConnections(orm.ConnectionConfig{
|
||||
Name: "default",
|
||||
DB: db,
|
||||
Dialect: orm.Dialects.SQLite3,
|
||||
Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}},
|
||||
DatabaseValidations: true,
|
||||
})
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
err = orm.InsertAll(&Post{
|
||||
BodyText: "my body for insert",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
post, err := orm.Find[Post](1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "my body for insert", post.BodyText)
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post := &Post{
|
||||
BodyText: "my body for insert",
|
||||
}
|
||||
err = orm.Insert(post)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
var p Post
|
||||
assert.NoError(t,
|
||||
orm.GetConnection("default").DB.QueryRow(`SELECT id, body FROM posts where id = ?`, 1).Scan(&p.ID, &p.BodyText))
|
||||
|
||||
assert.Equal(t, "my body for insert", p.BodyText)
|
||||
}
|
||||
func TestInsertAll(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post1 := &Post{
|
||||
BodyText: "Body1",
|
||||
}
|
||||
post2 := &Post{
|
||||
BodyText: "Body2",
|
||||
}
|
||||
|
||||
post3 := &Post{
|
||||
BodyText: "Body3",
|
||||
}
|
||||
|
||||
err = orm.InsertAll(post1, post2, post3)
|
||||
assert.NoError(t, err)
|
||||
var counter int
|
||||
assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT count(id) FROM posts`).Scan(&counter))
|
||||
assert.Equal(t, 3, counter)
|
||||
|
||||
}
|
||||
func TestUpdateORM(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post := &Post{
|
||||
BodyText: "my body for insert",
|
||||
}
|
||||
err = orm.Insert(post)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
post.BodyText += " update text"
|
||||
assert.NoError(t, orm.Update(post))
|
||||
|
||||
var body string
|
||||
assert.NoError(t,
|
||||
orm.GetConnection("default").DB.QueryRow(`SELECT body FROM posts where id = ?`, post.ID).Scan(&body))
|
||||
|
||||
assert.Equal(t, "my body for insert update text", body)
|
||||
}
|
||||
|
||||
func TestDeleteORM(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post := &Post{
|
||||
BodyText: "my body for insert",
|
||||
}
|
||||
err = orm.Insert(post)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
assert.NoError(t, orm.Delete(post))
|
||||
|
||||
var count int
|
||||
assert.NoError(t,
|
||||
orm.GetConnection("default").DB.QueryRow(`SELECT count(id) FROM posts where id = ?`, post.ID).Scan(&count))
|
||||
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
func TestAdd_HasMany(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post := &Post{
|
||||
BodyText: "my body for insert",
|
||||
}
|
||||
err = orm.Insert(post)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
err = orm.Add(post, []orm.Entity{
|
||||
Comment{
|
||||
Body: "comment 1",
|
||||
},
|
||||
Comment{
|
||||
Body: "comment 2",
|
||||
},
|
||||
}...)
|
||||
// orm.Query(qm.WhereBetween())
|
||||
assert.NoError(t, err)
|
||||
var count int
|
||||
assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT COUNT(id) FROM comments`).Scan(&count))
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
comment, err := orm.Find[Comment](1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(1), comment.PostID)
|
||||
|
||||
}
|
||||
|
||||
func TestAdd_ManyToMany(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post := &Post{
|
||||
BodyText: "my body for insert",
|
||||
}
|
||||
err = orm.Insert(post)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
err = orm.Add(post, []orm.Entity{
|
||||
&Category{
|
||||
Title: "cat 1",
|
||||
},
|
||||
&Category{
|
||||
Title: "cat 2",
|
||||
},
|
||||
}...)
|
||||
assert.NoError(t, err)
|
||||
var count int
|
||||
assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT COUNT(post_id) FROM post_categories`).Scan(&count))
|
||||
assert.Equal(t, 2, count)
|
||||
assert.NoError(t, orm.GetConnection("default").DB.QueryRow(`SELECT COUNT(id) FROM categories`).Scan(&count))
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
categories, err := post.Categories()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 2, len(categories))
|
||||
assert.Equal(t, int64(1), categories[0].ID)
|
||||
assert.Equal(t, int64(2), categories[1].ID)
|
||||
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
t.Run("save should insert", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
post := &Post{
|
||||
BodyText: "1",
|
||||
}
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
})
|
||||
|
||||
t.Run("save should update", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "1",
|
||||
}
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
post.BodyText += "2"
|
||||
assert.NoError(t, orm.Save(post))
|
||||
|
||||
myPost, err := orm.Find[Post](1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, post.BodyText, myPost.BodyText)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestHasMany(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "first post",
|
||||
}
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
assert.NoError(t, orm.Save(&Comment{
|
||||
PostID: post.ID,
|
||||
Body: "comment 1",
|
||||
}))
|
||||
assert.NoError(t, orm.Save(&Comment{
|
||||
PostID: post.ID,
|
||||
Body: "comment 2",
|
||||
}))
|
||||
|
||||
comments, err := orm.HasMany[Comment](post).All()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, comments, 2)
|
||||
|
||||
assert.Equal(t, post.ID, comments[0].PostID)
|
||||
assert.Equal(t, post.ID, comments[1].PostID)
|
||||
}
|
||||
|
||||
func TestBelongsTo(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "first post",
|
||||
}
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
comment := &Comment{
|
||||
PostID: post.ID,
|
||||
Body: "comment 1",
|
||||
}
|
||||
assert.NoError(t, orm.Save(comment))
|
||||
|
||||
post2, err := orm.BelongsTo[Post](comment).Get()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, post.BodyText, post2.BodyText)
|
||||
}
|
||||
|
||||
func TestHasOne(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "first post",
|
||||
}
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
headerPicture := &HeaderPicture{
|
||||
PostID: post.ID,
|
||||
Link: "google",
|
||||
}
|
||||
assert.NoError(t, orm.Save(headerPicture))
|
||||
|
||||
c1, err := orm.HasOne[HeaderPicture](post).Get()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, headerPicture.PostID, c1.PostID)
|
||||
}
|
||||
|
||||
func TestBelongsToMany(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "first Post",
|
||||
}
|
||||
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.Equal(t, int64(1), post.ID)
|
||||
|
||||
category := &Category{
|
||||
Title: "first category",
|
||||
}
|
||||
assert.NoError(t, orm.Save(category))
|
||||
assert.Equal(t, int64(1), category.ID)
|
||||
|
||||
_, _, err = orm.ExecRaw[Category](`INSERT INTO post_categories (post_id, category_id) VALUES (?,?)`, post.ID, category.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
categories, err := orm.BelongsToMany[Category](post).All()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, categories, 1)
|
||||
}
|
||||
|
||||
func TestSchematic(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
orm.Schematic()
|
||||
}
|
||||
|
||||
func TestAddProperty(t *testing.T) {
|
||||
t.Run("having pk value", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "first post",
|
||||
}
|
||||
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.EqualValues(t, 1, post.ID)
|
||||
|
||||
err = orm.Add(post, &Comment{PostID: post.ID, Body: "firstComment"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
var comment Comment
|
||||
assert.NoError(t, orm.GetConnection("default").
|
||||
DB.
|
||||
QueryRow(`SELECT id, post_id, body FROM comments WHERE post_id=?`, post.ID).
|
||||
Scan(&comment.ID, &comment.PostID, &comment.Body))
|
||||
|
||||
assert.EqualValues(t, post.ID, comment.PostID)
|
||||
})
|
||||
t.Run("not having PK value", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
post := &Post{
|
||||
BodyText: "first post",
|
||||
}
|
||||
assert.NoError(t, orm.Save(post))
|
||||
assert.EqualValues(t, 1, post.ID)
|
||||
|
||||
err = orm.Add(post, &AuthorEmail{Email: "myemail"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
emails, err := orm.QueryRaw[AuthorEmail](`SELECT id, email FROM emails WHERE post_id=?`, post.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []AuthorEmail{{ID: 1, Email: "myemail"}}, emails)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQuery(t *testing.T) {
|
||||
t.Run("querying single row", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 1"}))
|
||||
// post, err := orm.Query[Post]().Where("id", 1).First()
|
||||
post, err := orm.Query[Post]().WherePK(1).First().Get()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "body 1", post.BodyText)
|
||||
assert.EqualValues(t, 1, post.ID)
|
||||
|
||||
})
|
||||
t.Run("querying multiple rows", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 1"}))
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 2"}))
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 3"}))
|
||||
posts, err := orm.Query[Post]().All()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, posts, 3)
|
||||
assert.Equal(t, "body 1", posts[0].BodyText)
|
||||
})
|
||||
|
||||
t.Run("updating a row using query interface", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 1"}))
|
||||
|
||||
affected, err := orm.Query[Post]().Where("id", 1).Set("body", "body jadid").Update()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, affected)
|
||||
|
||||
post, err := orm.Find[Post](1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "body jadid", post.BodyText)
|
||||
})
|
||||
|
||||
t.Run("deleting a row using query interface", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 1"}))
|
||||
|
||||
affected, err := orm.Query[Post]().WherePK(1).Delete()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, affected)
|
||||
count, err := orm.Query[Post]().WherePK(1).Count().Get()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
})
|
||||
|
||||
t.Run("count", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
count, err := orm.Query[Post]().WherePK(1).Count().Get()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
})
|
||||
|
||||
t.Run("latest", func(t *testing.T) {
|
||||
err := setup()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 1"}))
|
||||
assert.NoError(t, orm.Save(&Post{BodyText: "body 2"}))
|
||||
|
||||
post, err := orm.Query[Post]().Latest().Get()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, "body 2", post.BodyText)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
t.Run("tables are out of sync", func(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
// _, err = db.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, post_id INTEGER, body text)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`)
|
||||
// _, err = db.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`)
|
||||
|
||||
err = orm.SetupConnections(orm.ConnectionConfig{
|
||||
Name: "default",
|
||||
DB: db,
|
||||
Dialect: orm.Dialects.SQLite3,
|
||||
Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}},
|
||||
DatabaseValidations: true,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
})
|
||||
t.Run("schemas are wrong", func(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, body text)`) // missing post_id
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`)
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`)
|
||||
|
||||
err = orm.SetupConnections(orm.ConnectionConfig{
|
||||
Name: "default",
|
||||
DB: db,
|
||||
Dialect: orm.Dialects.SQLite3,
|
||||
Entities: []orm.Entity{&Post{}, &Comment{}, &Category{}, &HeaderPicture{}},
|
||||
DatabaseValidations: true,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
})
|
||||
}
|
811
gdb/orm/query.go
811
gdb/orm/query.go
@ -1,811 +0,0 @@
|
||||
//
|
||||
// query.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
queryTypeSELECT = iota + 1
|
||||
queryTypeUPDATE
|
||||
queryTypeDelete
|
||||
)
|
||||
|
||||
// QueryBuilder is our query builder, almost all methods and functions in GoLobby ORM
|
||||
// create or configure instance of QueryBuilder.
|
||||
type QueryBuilder[OUTPUT any] struct {
|
||||
typ int
|
||||
schema *schema
|
||||
// general parts
|
||||
where *whereClause
|
||||
table string
|
||||
placeholderGenerator func(n int) []string
|
||||
|
||||
// select parts
|
||||
orderBy *orderByClause
|
||||
groupBy *GroupBy
|
||||
selected *selected
|
||||
subQuery *struct {
|
||||
q string
|
||||
args []interface{}
|
||||
placeholderGenerator func(n int) []string
|
||||
}
|
||||
joins []*Join
|
||||
limit *Limit
|
||||
offset *Offset
|
||||
|
||||
// update parts
|
||||
sets [][2]interface{}
|
||||
|
||||
// execution parts
|
||||
db *sql.DB
|
||||
err error
|
||||
}
|
||||
|
||||
// Finisher APIs
|
||||
|
||||
// execute is a finisher executes QueryBuilder query, remember to use this when you have an Update
|
||||
// or Delete Query.
|
||||
func (q *QueryBuilder[OUTPUT]) execute() (sql.Result, error) {
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
if q.typ == queryTypeSELECT {
|
||||
return nil, fmt.Errorf("query type is SELECT")
|
||||
}
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.schema.getConnection().exec(query, args...)
|
||||
}
|
||||
|
||||
// Get limit results to 1, runs query generated by query builder, scans result into OUTPUT.
|
||||
func (q *QueryBuilder[OUTPUT]) Get() (OUTPUT, error) {
|
||||
if q.err != nil {
|
||||
return *new(OUTPUT), q.err
|
||||
}
|
||||
queryString, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return *new(OUTPUT), err
|
||||
}
|
||||
rows, err := q.schema.getConnection().query(queryString, args...)
|
||||
if err != nil {
|
||||
return *new(OUTPUT), err
|
||||
}
|
||||
var output OUTPUT
|
||||
err = newBinder(q.schema).bind(rows, &output)
|
||||
if err != nil {
|
||||
return *new(OUTPUT), err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// All is a finisher, create the Select query based on QueryBuilder and scan results into
|
||||
// slice of type parameter E.
|
||||
func (q *QueryBuilder[OUTPUT]) All() ([]OUTPUT, error) {
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
q.SetSelect()
|
||||
queryString, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := q.schema.getConnection().query(queryString, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var output []OUTPUT
|
||||
err = newBinder(q.schema).bind(rows, &output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Delete is a finisher, creates a delete query from query builder and executes it.
|
||||
func (q *QueryBuilder[OUTPUT]) Delete() (rowsAffected int64, err error) {
|
||||
if q.err != nil {
|
||||
return 0, q.err
|
||||
}
|
||||
q.SetDelete()
|
||||
res, err := q.execute()
|
||||
if err != nil {
|
||||
return 0, q.err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// Update is a finisher, creates an Update query from QueryBuilder and executes in into database, returns
|
||||
func (q *QueryBuilder[OUTPUT]) Update() (rowsAffected int64, err error) {
|
||||
if q.err != nil {
|
||||
return 0, q.err
|
||||
}
|
||||
q.SetUpdate()
|
||||
res, err := q.execute()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
func copyQueryBuilder[T1 any, T2 any](q *QueryBuilder[T1], q2 *QueryBuilder[T2]) {
|
||||
q2.db = q.db
|
||||
q2.err = q.err
|
||||
q2.groupBy = q.groupBy
|
||||
q2.joins = q.joins
|
||||
q2.limit = q.limit
|
||||
q2.offset = q.offset
|
||||
q2.orderBy = q.orderBy
|
||||
q2.placeholderGenerator = q.placeholderGenerator
|
||||
q2.schema = q.schema
|
||||
q2.selected = q.selected
|
||||
q2.sets = q.sets
|
||||
|
||||
q2.subQuery = q.subQuery
|
||||
q2.table = q.table
|
||||
q2.typ = q.typ
|
||||
q2.where = q.where
|
||||
}
|
||||
|
||||
// Count creates and execute a select query from QueryBuilder and set it's field list of selection
|
||||
// to COUNT(id).
|
||||
func (q *QueryBuilder[OUTPUT]) Count() *QueryBuilder[int] {
|
||||
q.selected = &selected{Columns: []string{"COUNT(id)"}}
|
||||
q.SetSelect()
|
||||
qCount := NewQueryBuilder[int](q.schema)
|
||||
|
||||
copyQueryBuilder(q, qCount)
|
||||
|
||||
return qCount
|
||||
}
|
||||
|
||||
// First returns first record of database using OrderBy primary key
|
||||
// ascending order.
|
||||
func (q *QueryBuilder[OUTPUT]) First() *QueryBuilder[OUTPUT] {
|
||||
q.OrderBy(q.schema.pkName(), ASC).Limit(1)
|
||||
return q
|
||||
}
|
||||
|
||||
// Latest is like Get but it also do a OrderBy(primary key, DESC)
|
||||
func (q *QueryBuilder[OUTPUT]) Latest() *QueryBuilder[OUTPUT] {
|
||||
q.OrderBy(q.schema.pkName(), DESC).Limit(1)
|
||||
return q
|
||||
}
|
||||
|
||||
// WherePK adds a where clause to QueryBuilder and also gets primary key name
|
||||
// from type parameter schema.
|
||||
func (q *QueryBuilder[OUTPUT]) WherePK(value interface{}) *QueryBuilder[OUTPUT] {
|
||||
return q.Where(q.schema.pkName(), value)
|
||||
}
|
||||
|
||||
func (d *QueryBuilder[OUTPUT]) toSqlDelete() (string, []interface{}, error) {
|
||||
base := fmt.Sprintf("DELETE FROM %s", d.table)
|
||||
var args []interface{}
|
||||
if d.where != nil {
|
||||
d.where.PlaceHolderGenerator = d.placeholderGenerator
|
||||
where, whereArgs, err := d.where.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
base += " WHERE " + where
|
||||
args = append(args, whereArgs...)
|
||||
}
|
||||
return base, args, nil
|
||||
}
|
||||
func pop(phs *[]string) string {
|
||||
top := (*phs)[len(*phs)-1]
|
||||
*phs = (*phs)[:len(*phs)-1]
|
||||
return top
|
||||
}
|
||||
|
||||
func (u *QueryBuilder[OUTPUT]) kvString() string {
|
||||
phs := u.placeholderGenerator(len(u.sets))
|
||||
var sets []string
|
||||
for _, pair := range u.sets {
|
||||
sets = append(sets, fmt.Sprintf("%s=%s", pair[0], pop(&phs)))
|
||||
}
|
||||
return strings.Join(sets, ",")
|
||||
}
|
||||
|
||||
func (u *QueryBuilder[OUTPUT]) args() []interface{} {
|
||||
var values []interface{}
|
||||
for _, pair := range u.sets {
|
||||
values = append(values, pair[1])
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func (u *QueryBuilder[OUTPUT]) toSqlUpdate() (string, []interface{}, error) {
|
||||
if u.table == "" {
|
||||
return "", nil, fmt.Errorf("table cannot be empty")
|
||||
}
|
||||
base := fmt.Sprintf("UPDATE %s SET %s", u.table, u.kvString())
|
||||
args := u.args()
|
||||
if u.where != nil {
|
||||
u.where.PlaceHolderGenerator = u.placeholderGenerator
|
||||
where, whereArgs, err := u.where.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
args = append(args, whereArgs...)
|
||||
base += " WHERE " + where
|
||||
}
|
||||
return base, args, nil
|
||||
}
|
||||
func (s *QueryBuilder[OUTPUT]) toSqlSelect() (string, []interface{}, error) {
|
||||
if s.err != nil {
|
||||
return "", nil, s.err
|
||||
}
|
||||
base := "SELECT"
|
||||
var args []interface{}
|
||||
// select
|
||||
if s.selected == nil {
|
||||
s.selected = &selected{
|
||||
Columns: []string{"*"},
|
||||
}
|
||||
}
|
||||
base += " " + s.selected.String()
|
||||
// from
|
||||
if s.table == "" && s.subQuery == nil {
|
||||
return "", nil, fmt.Errorf("Table name cannot be empty")
|
||||
} else if s.table != "" && s.subQuery != nil {
|
||||
return "", nil, fmt.Errorf("cannot have both Table and subquery")
|
||||
}
|
||||
if s.table != "" {
|
||||
base += " " + "FROM " + s.table
|
||||
}
|
||||
if s.subQuery != nil {
|
||||
s.subQuery.placeholderGenerator = s.placeholderGenerator
|
||||
base += " " + "FROM (" + s.subQuery.q + " )"
|
||||
args = append(args, s.subQuery.args...)
|
||||
}
|
||||
// Joins
|
||||
if s.joins != nil {
|
||||
for _, join := range s.joins {
|
||||
base += " " + join.String()
|
||||
}
|
||||
}
|
||||
// whereClause
|
||||
if s.where != nil {
|
||||
s.where.PlaceHolderGenerator = s.placeholderGenerator
|
||||
where, whereArgs, err := s.where.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
base += " WHERE " + where
|
||||
args = append(args, whereArgs...)
|
||||
}
|
||||
|
||||
// orderByClause
|
||||
if s.orderBy != nil {
|
||||
base += " " + s.orderBy.String()
|
||||
}
|
||||
|
||||
// GroupBy
|
||||
if s.groupBy != nil {
|
||||
base += " " + s.groupBy.String()
|
||||
}
|
||||
|
||||
// Limit
|
||||
if s.limit != nil {
|
||||
base += " " + s.limit.String()
|
||||
}
|
||||
|
||||
// Offset
|
||||
if s.offset != nil {
|
||||
base += " " + s.offset.String()
|
||||
}
|
||||
|
||||
return base, args, nil
|
||||
}
|
||||
|
||||
// ToSql creates sql query from QueryBuilder based on internal fields it would decide what kind
|
||||
// of query to build.
|
||||
func (q *QueryBuilder[OUTPUT]) ToSql() (string, []interface{}, error) {
|
||||
if q.err != nil {
|
||||
return "", nil, q.err
|
||||
}
|
||||
if q.typ == queryTypeSELECT {
|
||||
return q.toSqlSelect()
|
||||
} else if q.typ == queryTypeDelete {
|
||||
return q.toSqlDelete()
|
||||
} else if q.typ == queryTypeUPDATE {
|
||||
return q.toSqlUpdate()
|
||||
} else {
|
||||
return "", nil, fmt.Errorf("no sql type matched")
|
||||
}
|
||||
}
|
||||
|
||||
type orderByOrder string
|
||||
|
||||
const (
|
||||
ASC = "ASC"
|
||||
DESC = "DESC"
|
||||
)
|
||||
|
||||
type orderByClause struct {
|
||||
Columns [][2]string
|
||||
}
|
||||
|
||||
func (o orderByClause) String() string {
|
||||
var tuples []string
|
||||
for _, pair := range o.Columns {
|
||||
tuples = append(tuples, fmt.Sprintf("%s %s", pair[0], pair[1]))
|
||||
}
|
||||
return fmt.Sprintf("ORDER BY %s", strings.Join(tuples, ","))
|
||||
}
|
||||
|
||||
type GroupBy struct {
|
||||
Columns []string
|
||||
}
|
||||
|
||||
func (g GroupBy) String() string {
|
||||
return fmt.Sprintf("GROUP BY %s", strings.Join(g.Columns, ","))
|
||||
}
|
||||
|
||||
type joinType string
|
||||
|
||||
const (
|
||||
JoinTypeInner = "INNER"
|
||||
JoinTypeLeft = "LEFT"
|
||||
JoinTypeRight = "RIGHT"
|
||||
JoinTypeFull = "FULL OUTER"
|
||||
JoinTypeSelf = "SELF"
|
||||
)
|
||||
|
||||
type JoinOn struct {
|
||||
Lhs string
|
||||
Rhs string
|
||||
}
|
||||
|
||||
func (j JoinOn) String() string {
|
||||
return fmt.Sprintf("%s = %s", j.Lhs, j.Rhs)
|
||||
}
|
||||
|
||||
type Join struct {
|
||||
Type joinType
|
||||
Table string
|
||||
On JoinOn
|
||||
}
|
||||
|
||||
func (j Join) String() string {
|
||||
return fmt.Sprintf("%s JOIN %s ON %s", j.Type, j.Table, j.On.String())
|
||||
}
|
||||
|
||||
type Limit struct {
|
||||
N int
|
||||
}
|
||||
|
||||
func (l Limit) String() string {
|
||||
return fmt.Sprintf("LIMIT %d", l.N)
|
||||
}
|
||||
|
||||
type Offset struct {
|
||||
N int
|
||||
}
|
||||
|
||||
func (o Offset) String() string {
|
||||
return fmt.Sprintf("OFFSET %d", o.N)
|
||||
}
|
||||
|
||||
type selected struct {
|
||||
Columns []string
|
||||
}
|
||||
|
||||
func (s selected) String() string {
|
||||
return fmt.Sprintf("%s", strings.Join(s.Columns, ","))
|
||||
}
|
||||
|
||||
// OrderBy adds an OrderBy section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) OrderBy(column string, how string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
if q.orderBy == nil {
|
||||
q.orderBy = &orderByClause{}
|
||||
}
|
||||
q.orderBy.Columns = append(q.orderBy.Columns, [2]string{column, how})
|
||||
return q
|
||||
}
|
||||
|
||||
// LeftJoin adds a left join section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) LeftJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
q.joins = append(q.joins, &Join{
|
||||
Type: JoinTypeLeft,
|
||||
Table: table,
|
||||
On: JoinOn{
|
||||
Lhs: onLhs,
|
||||
Rhs: onRhs,
|
||||
},
|
||||
})
|
||||
return q
|
||||
}
|
||||
|
||||
// RightJoin adds a right join section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) RightJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
q.joins = append(q.joins, &Join{
|
||||
Type: JoinTypeRight,
|
||||
Table: table,
|
||||
On: JoinOn{
|
||||
Lhs: onLhs,
|
||||
Rhs: onRhs,
|
||||
},
|
||||
})
|
||||
return q
|
||||
}
|
||||
|
||||
// InnerJoin adds a inner join section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) InnerJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
q.joins = append(q.joins, &Join{
|
||||
Type: JoinTypeInner,
|
||||
Table: table,
|
||||
On: JoinOn{
|
||||
Lhs: onLhs,
|
||||
Rhs: onRhs,
|
||||
},
|
||||
})
|
||||
return q
|
||||
}
|
||||
|
||||
// Join adds a inner join section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) Join(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] {
|
||||
return q.InnerJoin(table, onLhs, onRhs)
|
||||
}
|
||||
|
||||
// FullOuterJoin adds a full outer join section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) FullOuterJoin(table string, onLhs string, onRhs string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
q.joins = append(q.joins, &Join{
|
||||
Type: JoinTypeFull,
|
||||
Table: table,
|
||||
On: JoinOn{
|
||||
Lhs: onLhs,
|
||||
Rhs: onRhs,
|
||||
},
|
||||
})
|
||||
return q
|
||||
}
|
||||
|
||||
// Where Adds a where clause to query, if already have where clause append to it
|
||||
// as AndWhere.
|
||||
func (q *QueryBuilder[OUTPUT]) Where(parts ...interface{}) *QueryBuilder[OUTPUT] {
|
||||
if q.where != nil {
|
||||
return q.addWhere("AND", parts...)
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
if r, isRaw := parts[0].(*raw); isRaw {
|
||||
q.where = &whereClause{raw: r.sql, args: r.args, PlaceHolderGenerator: q.placeholderGenerator}
|
||||
return q
|
||||
} else {
|
||||
q.err = fmt.Errorf("when you have one argument passed to where, it should be *raw")
|
||||
return q
|
||||
}
|
||||
|
||||
} else if len(parts) == 2 {
|
||||
if strings.Index(parts[0].(string), " ") == -1 {
|
||||
// Equal mode
|
||||
q.where = &whereClause{cond: cond{Lhs: parts[0].(string), Op: Eq, Rhs: parts[1]}, PlaceHolderGenerator: q.placeholderGenerator}
|
||||
}
|
||||
return q
|
||||
} else if len(parts) == 3 {
|
||||
// operator mode
|
||||
q.where = &whereClause{cond: cond{Lhs: parts[0].(string), Op: binaryOp(parts[1].(string)), Rhs: parts[2]}, PlaceHolderGenerator: q.placeholderGenerator}
|
||||
return q
|
||||
} else if len(parts) > 3 && parts[1].(string) == "IN" {
|
||||
q.where = &whereClause{cond: cond{Lhs: parts[0].(string), Op: binaryOp(parts[1].(string)), Rhs: parts[2:]}, PlaceHolderGenerator: q.placeholderGenerator}
|
||||
return q
|
||||
} else {
|
||||
q.err = fmt.Errorf("wrong number of arguments passed to Where")
|
||||
return q
|
||||
}
|
||||
}
|
||||
|
||||
type binaryOp string
|
||||
|
||||
const (
|
||||
Eq = "="
|
||||
GT = ">"
|
||||
LT = "<"
|
||||
GE = ">="
|
||||
LE = "<="
|
||||
NE = "!="
|
||||
Between = "BETWEEN"
|
||||
Like = "LIKE"
|
||||
In = "IN"
|
||||
)
|
||||
|
||||
type cond struct {
|
||||
PlaceHolderGenerator func(n int) []string
|
||||
|
||||
Lhs string
|
||||
Op binaryOp
|
||||
Rhs interface{}
|
||||
}
|
||||
|
||||
func (b cond) ToSql() (string, []interface{}, error) {
|
||||
var phs []string
|
||||
if b.Op == In {
|
||||
rhs, isInterfaceSlice := b.Rhs.([]interface{})
|
||||
if isInterfaceSlice {
|
||||
phs = b.PlaceHolderGenerator(len(rhs))
|
||||
return fmt.Sprintf("%s IN (%s)", b.Lhs, strings.Join(phs, ",")), rhs, nil
|
||||
} else if rawThing, isRaw := b.Rhs.(*raw); isRaw {
|
||||
return fmt.Sprintf("%s IN (%s)", b.Lhs, rawThing.sql), rawThing.args, nil
|
||||
} else {
|
||||
return "", nil, fmt.Errorf("Right hand side of Cond when operator is IN should be either a interface{} slice or *raw")
|
||||
}
|
||||
|
||||
} else {
|
||||
phs = b.PlaceHolderGenerator(1)
|
||||
return fmt.Sprintf("%s %s %s", b.Lhs, b.Op, pop(&phs)), []interface{}{b.Rhs}, nil
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
nextType_AND = "AND"
|
||||
nextType_OR = "OR"
|
||||
)
|
||||
|
||||
type whereClause struct {
|
||||
PlaceHolderGenerator func(n int) []string
|
||||
nextTyp string
|
||||
next *whereClause
|
||||
cond
|
||||
raw string
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
func (w whereClause) ToSql() (string, []interface{}, error) {
|
||||
var base string
|
||||
var args []interface{}
|
||||
var err error
|
||||
if w.raw != "" {
|
||||
base = w.raw
|
||||
args = w.args
|
||||
} else {
|
||||
w.cond.PlaceHolderGenerator = w.PlaceHolderGenerator
|
||||
base, args, err = w.cond.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if w.next == nil {
|
||||
return base, args, nil
|
||||
}
|
||||
if w.next != nil {
|
||||
next, nextArgs, err := w.next.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
base += " " + w.nextTyp + " " + next
|
||||
args = append(args, nextArgs...)
|
||||
return base, args, nil
|
||||
}
|
||||
|
||||
return base, args, nil
|
||||
}
|
||||
|
||||
//func (q *QueryBuilder[OUTPUT]) WhereKeyValue(m map) {}
|
||||
|
||||
// WhereIn adds a where clause to QueryBuilder using In operator.
|
||||
func (q *QueryBuilder[OUTPUT]) WhereIn(column string, values ...interface{}) *QueryBuilder[OUTPUT] {
|
||||
return q.Where(append([]interface{}{column, In}, values...)...)
|
||||
}
|
||||
|
||||
// AndWhere appends a where clause to query builder as And where clause.
|
||||
func (q *QueryBuilder[OUTPUT]) AndWhere(parts ...interface{}) *QueryBuilder[OUTPUT] {
|
||||
return q.addWhere(nextType_AND, parts...)
|
||||
}
|
||||
|
||||
// OrWhere appends a where clause to query builder as Or where clause.
|
||||
func (q *QueryBuilder[OUTPUT]) OrWhere(parts ...interface{}) *QueryBuilder[OUTPUT] {
|
||||
return q.addWhere(nextType_OR, parts...)
|
||||
}
|
||||
|
||||
func (q *QueryBuilder[OUTPUT]) addWhere(typ string, parts ...interface{}) *QueryBuilder[OUTPUT] {
|
||||
w := q.where
|
||||
for {
|
||||
if w == nil {
|
||||
break
|
||||
} else if w.next == nil {
|
||||
w.next = &whereClause{PlaceHolderGenerator: q.placeholderGenerator}
|
||||
w.nextTyp = typ
|
||||
w = w.next
|
||||
break
|
||||
} else {
|
||||
w = w.next
|
||||
}
|
||||
}
|
||||
if w == nil {
|
||||
w = &whereClause{PlaceHolderGenerator: q.placeholderGenerator}
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
w.raw = parts[0].(*raw).sql
|
||||
w.args = parts[0].(*raw).args
|
||||
return q
|
||||
} else if len(parts) == 2 {
|
||||
// Equal mode
|
||||
w.cond = cond{Lhs: parts[0].(string), Op: Eq, Rhs: parts[1]}
|
||||
return q
|
||||
} else if len(parts) == 3 {
|
||||
// operator mode
|
||||
w.cond = cond{Lhs: parts[0].(string), Op: binaryOp(parts[1].(string)), Rhs: parts[2]}
|
||||
return q
|
||||
} else {
|
||||
panic("wrong number of arguments passed to Where")
|
||||
}
|
||||
}
|
||||
|
||||
// Offset adds offset section to query builder.
|
||||
func (q *QueryBuilder[OUTPUT]) Offset(n int) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
q.offset = &Offset{N: n}
|
||||
return q
|
||||
}
|
||||
|
||||
// Limit adds limit section to query builder.
|
||||
func (q *QueryBuilder[OUTPUT]) Limit(n int) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
q.limit = &Limit{N: n}
|
||||
return q
|
||||
}
|
||||
|
||||
// Table sets table of QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) Table(t string) *QueryBuilder[OUTPUT] {
|
||||
q.table = t
|
||||
return q
|
||||
}
|
||||
|
||||
// SetSelect sets query type of QueryBuilder to Select.
|
||||
func (q *QueryBuilder[OUTPUT]) SetSelect() *QueryBuilder[OUTPUT] {
|
||||
q.typ = queryTypeSELECT
|
||||
return q
|
||||
}
|
||||
|
||||
// GroupBy adds a group by section to QueryBuilder.
|
||||
func (q *QueryBuilder[OUTPUT]) GroupBy(columns ...string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
if q.groupBy == nil {
|
||||
q.groupBy = &GroupBy{}
|
||||
}
|
||||
q.groupBy.Columns = append(q.groupBy.Columns, columns...)
|
||||
return q
|
||||
}
|
||||
|
||||
// Select adds columns to QueryBuilder select field list.
|
||||
func (q *QueryBuilder[OUTPUT]) Select(columns ...string) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
if q.selected == nil {
|
||||
q.selected = &selected{}
|
||||
}
|
||||
q.selected.Columns = append(q.selected.Columns, columns...)
|
||||
return q
|
||||
}
|
||||
|
||||
// FromQuery sets subquery of QueryBuilder to be given subquery so
|
||||
// when doing select instead of from table we do from(subquery).
|
||||
func (q *QueryBuilder[OUTPUT]) FromQuery(subQuery *QueryBuilder[OUTPUT]) *QueryBuilder[OUTPUT] {
|
||||
q.SetSelect()
|
||||
subQuery.SetSelect()
|
||||
subQuery.placeholderGenerator = q.placeholderGenerator
|
||||
subQueryString, args, err := subQuery.ToSql()
|
||||
q.err = err
|
||||
q.subQuery = &struct {
|
||||
q string
|
||||
args []interface{}
|
||||
placeholderGenerator func(n int) []string
|
||||
}{
|
||||
subQueryString, args, q.placeholderGenerator,
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *QueryBuilder[OUTPUT]) SetUpdate() *QueryBuilder[OUTPUT] {
|
||||
q.typ = queryTypeUPDATE
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *QueryBuilder[OUTPUT]) Set(keyValues ...any) *QueryBuilder[OUTPUT] {
|
||||
if len(keyValues)%2 != 0 {
|
||||
q.err = fmt.Errorf("when using Set, passed argument count should be even: %w", q.err)
|
||||
return q
|
||||
}
|
||||
q.SetUpdate()
|
||||
for i := 0; i < len(keyValues); i++ {
|
||||
if i != 0 && i%2 == 1 {
|
||||
q.sets = append(q.sets, [2]any{keyValues[i-1], keyValues[i]})
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *QueryBuilder[OUTPUT]) SetDialect(dialect *Dialect) *QueryBuilder[OUTPUT] {
|
||||
q.placeholderGenerator = dialect.PlaceHolderGenerator
|
||||
return q
|
||||
}
|
||||
func (q *QueryBuilder[OUTPUT]) SetDelete() *QueryBuilder[OUTPUT] {
|
||||
q.typ = queryTypeDelete
|
||||
return q
|
||||
}
|
||||
|
||||
type raw struct {
|
||||
sql string
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
// Raw creates a Raw sql query chunk that you can add to several components of QueryBuilder like
|
||||
// Wheres.
|
||||
func Raw(sql string, args ...interface{}) *raw {
|
||||
return &raw{sql: sql, args: args}
|
||||
}
|
||||
|
||||
func NewQueryBuilder[OUTPUT any](s *schema) *QueryBuilder[OUTPUT] {
|
||||
return &QueryBuilder[OUTPUT]{schema: s}
|
||||
}
|
||||
|
||||
type insertStmt struct {
|
||||
PlaceHolderGenerator func(n int) []string
|
||||
Table string
|
||||
Columns []string
|
||||
Values [][]interface{}
|
||||
Returning string
|
||||
}
|
||||
|
||||
func (i insertStmt) flatValues() []interface{} {
|
||||
var values []interface{}
|
||||
for _, row := range i.Values {
|
||||
values = append(values, row...)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func (i insertStmt) getValuesStr() string {
|
||||
phs := i.PlaceHolderGenerator(len(i.Values) * len(i.Values[0]))
|
||||
|
||||
var output []string
|
||||
for _, valueRow := range i.Values {
|
||||
output = append(output, fmt.Sprintf("(%s)", strings.Join(phs[:len(valueRow)], ",")))
|
||||
phs = phs[len(valueRow):]
|
||||
}
|
||||
return strings.Join(output, ",")
|
||||
}
|
||||
|
||||
func (i insertStmt) ToSql() (string, []interface{}) {
|
||||
base := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s",
|
||||
i.Table,
|
||||
strings.Join(i.Columns, ","),
|
||||
i.getValuesStr(),
|
||||
)
|
||||
if i.Returning != "" {
|
||||
base += "RETURNING " + i.Returning
|
||||
}
|
||||
return base, i.flatValues()
|
||||
}
|
||||
|
||||
func postgresPlaceholder(n int) []string {
|
||||
output := []string{}
|
||||
for i := 1; i < n+1; i++ {
|
||||
output = append(output, fmt.Sprintf("$%d", i))
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func questionMarks(n int) []string {
|
||||
output := []string{}
|
||||
for i := 0; i < n; i++ {
|
||||
output = append(output, "?")
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
@ -1,243 +0,0 @@
|
||||
//
|
||||
// query_test.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Dummy struct{}
|
||||
|
||||
func (d Dummy) ConfigureEntity(e *EntityConfigurator) {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
t.Run("only select * from Table", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil)
|
||||
s.Table("users").SetSelect()
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, "SELECT * FROM users", str)
|
||||
})
|
||||
t.Run("select with whereClause", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil)
|
||||
|
||||
s.Table("users").SetDialect(Dialects.MySQL).
|
||||
Where("age", 10).
|
||||
AndWhere("age", "<", 10).
|
||||
Where("name", "Amirreza").
|
||||
OrWhere("age", GT, 11).
|
||||
SetSelect()
|
||||
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []interface{}{10, 10, "Amirreza", 11}, args)
|
||||
assert.Equal(t, "SELECT * FROM users WHERE age = ? AND age < ? AND name = ? OR age > ?", str)
|
||||
})
|
||||
t.Run("select with order by", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").OrderBy("created_at", ASC).OrderBy("updated_at", DESC)
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, "SELECT * FROM users ORDER BY created_at ASC,updated_at DESC", str)
|
||||
})
|
||||
|
||||
t.Run("select with group by", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").GroupBy("created_at", "updated_at")
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, "SELECT * FROM users GROUP BY created_at,updated_at", str)
|
||||
})
|
||||
|
||||
t.Run("Select with limit", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").Limit(10)
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, "SELECT * FROM users LIMIT 10", str)
|
||||
})
|
||||
|
||||
t.Run("Select with offset", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").Offset(10)
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, "SELECT * FROM users OFFSET 10", str)
|
||||
})
|
||||
|
||||
t.Run("select with join", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").RightJoin("addresses", "users.id", "addresses.user_id")
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, `SELECT id,name FROM users RIGHT JOIN addresses ON users.id = addresses.user_id`, str)
|
||||
})
|
||||
|
||||
t.Run("select with multiple joins", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").
|
||||
Select("id", "name").
|
||||
RightJoin("addresses", "users.id", "addresses.user_id").
|
||||
LeftJoin("user_credits", "users.id", "user_credits.user_id")
|
||||
sql, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, `SELECT id,name FROM users RIGHT JOIN addresses ON users.id = addresses.user_id LEFT JOIN user_credits ON users.id = user_credits.user_id`, sql)
|
||||
})
|
||||
|
||||
t.Run("select with subquery", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).SetDialect(Dialects.MySQL)
|
||||
s.FromQuery(NewQueryBuilder[Dummy](nil).Table("users").Where("age", "<", 10))
|
||||
sql, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []interface{}{10}, args)
|
||||
assert.Equal(t, `SELECT * FROM (SELECT * FROM users WHERE age < ? )`, sql)
|
||||
|
||||
})
|
||||
|
||||
t.Run("select with inner join", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").InnerJoin("addresses", "users.id", "addresses.user_id")
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, `SELECT id,name FROM users INNER JOIN addresses ON users.id = addresses.user_id`, str)
|
||||
})
|
||||
|
||||
t.Run("select with join", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").Join("addresses", "users.id", "addresses.user_id")
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, `SELECT id,name FROM users INNER JOIN addresses ON users.id = addresses.user_id`, str)
|
||||
})
|
||||
|
||||
t.Run("select with full outer join", func(t *testing.T) {
|
||||
s := NewQueryBuilder[Dummy](nil).Table("users").Select("id", "name").FullOuterJoin("addresses", "users.id", "addresses.user_id")
|
||||
str, args, err := s.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Equal(t, `SELECT id,name FROM users FULL OUTER JOIN addresses ON users.id = addresses.user_id`, str)
|
||||
})
|
||||
t.Run("raw where", func(t *testing.T) {
|
||||
sql, args, err :=
|
||||
NewQueryBuilder[Dummy](nil).
|
||||
SetDialect(Dialects.MySQL).
|
||||
Table("users").
|
||||
Where(Raw("id = ?", 1)).
|
||||
AndWhere(Raw("age < ?", 10)).
|
||||
SetSelect().
|
||||
ToSql()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []interface{}{1, 10}, args)
|
||||
assert.Equal(t, `SELECT * FROM users WHERE id = ? AND age < ?`, sql)
|
||||
})
|
||||
t.Run("no sql type matched", func(t *testing.T) {
|
||||
sql, args, err := NewQueryBuilder[Dummy](nil).ToSql()
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, args)
|
||||
assert.Empty(t, sql)
|
||||
})
|
||||
|
||||
t.Run("raw where in", func(t *testing.T) {
|
||||
sql, args, err :=
|
||||
NewQueryBuilder[Dummy](nil).
|
||||
SetDialect(Dialects.MySQL).
|
||||
Table("users").
|
||||
WhereIn("id", Raw("SELECT user_id FROM user_books WHERE book_id = ?", 10)).
|
||||
SetSelect().
|
||||
ToSql()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []interface{}{10}, args)
|
||||
assert.Equal(t, `SELECT * FROM users WHERE id IN (SELECT user_id FROM user_books WHERE book_id = ?)`, sql)
|
||||
})
|
||||
t.Run("where in", func(t *testing.T) {
|
||||
sql, args, err :=
|
||||
NewQueryBuilder[Dummy](nil).
|
||||
SetDialect(Dialects.MySQL).
|
||||
Table("users").
|
||||
WhereIn("id", 1, 2, 3, 4, 5, 6).
|
||||
SetSelect().
|
||||
ToSql()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, []interface{}{1, 2, 3, 4, 5, 6}, args)
|
||||
assert.Equal(t, `SELECT * FROM users WHERE id IN (?,?,?,?,?,?)`, sql)
|
||||
|
||||
})
|
||||
}
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Run("update no whereClause", func(t *testing.T) {
|
||||
u := NewQueryBuilder[Dummy](nil).Table("users").Set("name", "amirreza").SetDialect(Dialects.MySQL)
|
||||
sql, args, err := u.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `UPDATE users SET name=?`, sql)
|
||||
assert.Equal(t, []interface{}{"amirreza"}, args)
|
||||
})
|
||||
t.Run("update with whereClause", func(t *testing.T) {
|
||||
u := NewQueryBuilder[Dummy](nil).Table("users").Set("name", "amirreza").Where("age", "<", 18).SetDialect(Dialects.MySQL)
|
||||
sql, args, err := u.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `UPDATE users SET name=? WHERE age < ?`, sql)
|
||||
assert.Equal(t, []interface{}{"amirreza", 18}, args)
|
||||
|
||||
})
|
||||
}
|
||||
func TestDelete(t *testing.T) {
|
||||
t.Run("delete without whereClause", func(t *testing.T) {
|
||||
d := NewQueryBuilder[Dummy](nil).Table("users").SetDelete()
|
||||
sql, args, err := d.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `DELETE FROM users`, sql)
|
||||
assert.Empty(t, args)
|
||||
})
|
||||
t.Run("delete with whereClause", func(t *testing.T) {
|
||||
d := NewQueryBuilder[Dummy](nil).Table("users").SetDialect(Dialects.MySQL).Where("created_at", ">", "2012-01-10").SetDelete()
|
||||
sql, args, err := d.ToSql()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `DELETE FROM users WHERE created_at > ?`, sql)
|
||||
assert.EqualValues(t, []interface{}{"2012-01-10"}, args)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
t.Run("insert into multiple rows", func(t *testing.T) {
|
||||
i := insertStmt{}
|
||||
i.Table = "users"
|
||||
i.PlaceHolderGenerator = Dialects.MySQL.PlaceHolderGenerator
|
||||
i.Columns = []string{"name", "age"}
|
||||
i.Values = append(i.Values, []interface{}{"amirreza", 11}, []interface{}{"parsa", 10})
|
||||
s, args := i.ToSql()
|
||||
assert.Equal(t, `INSERT INTO users (name,age) VALUES (?,?),(?,?)`, s)
|
||||
assert.EqualValues(t, []interface{}{"amirreza", 11, "parsa", 10}, args)
|
||||
})
|
||||
|
||||
t.Run("insert into single row", func(t *testing.T) {
|
||||
i := insertStmt{}
|
||||
i.Table = "users"
|
||||
i.PlaceHolderGenerator = Dialects.MySQL.PlaceHolderGenerator
|
||||
i.Columns = []string{"name", "age"}
|
||||
i.Values = append(i.Values, []interface{}{"amirreza", 11})
|
||||
s, args := i.ToSql()
|
||||
assert.Equal(t, `INSERT INTO users (name,age) VALUES (?,?)`, s)
|
||||
assert.Equal(t, []interface{}{"amirreza", 11}, args)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgresPlaceholder(t *testing.T) {
|
||||
t.Run("for 5 it should have 5", func(t *testing.T) {
|
||||
phs := postgresPlaceholder(5)
|
||||
assert.EqualValues(t, []string{"$1", "$2", "$3", "$4", "$5"}, phs)
|
||||
})
|
||||
}
|
@ -1,306 +0,0 @@
|
||||
//
|
||||
// schema.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func getConnectionFor(e Entity) *connection {
|
||||
configurator := newEntityConfigurator()
|
||||
e.ConfigureEntity(configurator)
|
||||
|
||||
if len(globalConnections) > 1 && (configurator.connection == "" || configurator.table == "") {
|
||||
panic("need table and DB name when having more than 1 DB registered")
|
||||
}
|
||||
if len(globalConnections) == 1 {
|
||||
for _, db := range globalConnections {
|
||||
return db
|
||||
}
|
||||
}
|
||||
if db, exists := globalConnections[fmt.Sprintf("%s", configurator.connection)]; exists {
|
||||
return db
|
||||
}
|
||||
panic("no db found")
|
||||
}
|
||||
|
||||
func getSchemaFor(e Entity) *schema {
|
||||
configurator := newEntityConfigurator()
|
||||
c := getConnectionFor(e)
|
||||
e.ConfigureEntity(configurator)
|
||||
s := c.getSchema(configurator.table)
|
||||
if s == nil {
|
||||
s = schemaOfHeavyReflectionStuff(e)
|
||||
c.setSchema(e, s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type schema struct {
|
||||
Connection string
|
||||
Table string
|
||||
fields []*field
|
||||
relations map[string]interface{}
|
||||
setPK func(o Entity, value interface{})
|
||||
getPK func(o Entity) interface{}
|
||||
columnConstraints []*FieldConfigurator
|
||||
}
|
||||
|
||||
func (s *schema) getField(sf reflect.StructField) *field {
|
||||
for _, f := range s.fields {
|
||||
if sf.Name == f.Name {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *schema) getDialect() *Dialect {
|
||||
return GetConnection(s.Connection).Dialect
|
||||
}
|
||||
func (s *schema) Columns(withPK bool) []string {
|
||||
var cols []string
|
||||
for _, field := range s.fields {
|
||||
if field.Virtual {
|
||||
continue
|
||||
}
|
||||
if !withPK && field.IsPK {
|
||||
continue
|
||||
}
|
||||
if s.getDialect().AddTableNameInSelectColumns {
|
||||
cols = append(cols, s.Table+"."+field.Name)
|
||||
} else {
|
||||
cols = append(cols, field.Name)
|
||||
}
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
func (s *schema) pkName() string {
|
||||
for _, field := range s.fields {
|
||||
if field.IsPK {
|
||||
return field.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func genericFieldsOf(obj Entity) []*field {
|
||||
t := reflect.TypeOf(obj)
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
|
||||
}
|
||||
if t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
}
|
||||
var ec EntityConfigurator
|
||||
obj.ConfigureEntity(&ec)
|
||||
|
||||
var fms []*field
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
ft := t.Field(i)
|
||||
fm := fieldMetadata(ft, ec.columnConstraints)
|
||||
fms = append(fms, fm...)
|
||||
}
|
||||
return fms
|
||||
}
|
||||
|
||||
func valuesOfField(vf reflect.Value) []interface{} {
|
||||
var values []interface{}
|
||||
if vf.Type().Kind() == reflect.Struct || vf.Type().Kind() == reflect.Ptr {
|
||||
t := vf.Type()
|
||||
if vf.Type().Kind() == reflect.Ptr {
|
||||
t = vf.Type().Elem()
|
||||
}
|
||||
if !t.Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) {
|
||||
// go into
|
||||
// it does not implement driver.Valuer interface
|
||||
for i := 0; i < vf.NumField(); i++ {
|
||||
vif := vf.Field(i)
|
||||
values = append(values, valuesOfField(vif)...)
|
||||
}
|
||||
} else {
|
||||
values = append(values, vf.Interface())
|
||||
}
|
||||
} else {
|
||||
values = append(values, vf.Interface())
|
||||
}
|
||||
return values
|
||||
}
|
||||
func genericValuesOf(o Entity, withPK bool) []interface{} {
|
||||
t := reflect.TypeOf(o)
|
||||
v := reflect.ValueOf(o)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
v = v.Elem()
|
||||
}
|
||||
fields := getSchemaFor(o).fields
|
||||
pkIdx := -1
|
||||
for i, field := range fields {
|
||||
if field.IsPK {
|
||||
pkIdx = i
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var values []interface{}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if !withPK && i == pkIdx {
|
||||
continue
|
||||
}
|
||||
if fields[i].Virtual {
|
||||
continue
|
||||
}
|
||||
vf := v.Field(i)
|
||||
values = append(values, valuesOfField(vf)...)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func genericSetPkValue(obj Entity, value interface{}) {
|
||||
genericSet(obj, getSchemaFor(obj).pkName(), value)
|
||||
}
|
||||
|
||||
func genericGetPKValue(obj Entity) interface{} {
|
||||
t := reflect.TypeOf(obj)
|
||||
val := reflect.ValueOf(obj)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
fields := getSchemaFor(obj).fields
|
||||
for i, field := range fields {
|
||||
if field.IsPK {
|
||||
return val.Field(i).Interface()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *schema) createdAt() *field {
|
||||
for _, f := range s.fields {
|
||||
if f.IsCreatedAt {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *schema) updatedAt() *field {
|
||||
for _, f := range s.fields {
|
||||
if f.IsUpdatedAt {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *schema) deletedAt() *field {
|
||||
for _, f := range s.fields {
|
||||
if f.IsDeletedAt {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func pointersOf(v reflect.Value) map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
actualV := v
|
||||
for actualV.Type().Kind() == reflect.Ptr {
|
||||
actualV = actualV.Elem()
|
||||
}
|
||||
for i := 0; i < actualV.NumField(); i++ {
|
||||
f := actualV.Field(i)
|
||||
if (f.Type().Kind() == reflect.Struct || f.Type().Kind() == reflect.Ptr) && !f.Type().Implements(reflect.TypeOf((*driver.Valuer)(nil)).Elem()) {
|
||||
fm := pointersOf(f)
|
||||
for k, p := range fm {
|
||||
m[k] = p
|
||||
}
|
||||
} else {
|
||||
fm := fieldMetadata(actualV.Type().Field(i), nil)[0]
|
||||
m[fm.Name] = actualV.Field(i)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
func genericSet(obj Entity, name string, value interface{}) {
|
||||
n2p := pointersOf(reflect.ValueOf(obj))
|
||||
var val interface{}
|
||||
for k, v := range n2p {
|
||||
if k == name {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
val.(reflect.Value).Set(reflect.ValueOf(value))
|
||||
}
|
||||
func schemaOfHeavyReflectionStuff(v Entity) *schema {
|
||||
userEntityConfigurator := newEntityConfigurator()
|
||||
v.ConfigureEntity(userEntityConfigurator)
|
||||
for _, relation := range userEntityConfigurator.resolveRelations {
|
||||
relation()
|
||||
}
|
||||
schema := &schema{}
|
||||
if userEntityConfigurator.connection != "" {
|
||||
schema.Connection = userEntityConfigurator.connection
|
||||
}
|
||||
if userEntityConfigurator.table != "" {
|
||||
schema.Table = userEntityConfigurator.table
|
||||
} else {
|
||||
panic("you need to have table name for getting schema.")
|
||||
}
|
||||
|
||||
schema.columnConstraints = userEntityConfigurator.columnConstraints
|
||||
if schema.Connection == "" {
|
||||
schema.Connection = "default"
|
||||
}
|
||||
if schema.fields == nil {
|
||||
schema.fields = genericFieldsOf(v)
|
||||
}
|
||||
if schema.getPK == nil {
|
||||
schema.getPK = genericGetPKValue
|
||||
}
|
||||
|
||||
if schema.setPK == nil {
|
||||
schema.setPK = genericSetPkValue
|
||||
}
|
||||
|
||||
schema.relations = userEntityConfigurator.relations
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
func (s *schema) getTable() string {
|
||||
return s.Table
|
||||
}
|
||||
|
||||
func (s *schema) getSQLDB() *sql.DB {
|
||||
return s.getConnection().DB
|
||||
}
|
||||
|
||||
func (s *schema) getConnection() *connection {
|
||||
if len(globalConnections) > 1 && (s.Connection == "" || s.Table == "") {
|
||||
panic("need table and DB name when having more than 1 DB registered")
|
||||
}
|
||||
if len(globalConnections) == 1 {
|
||||
for _, db := range globalConnections {
|
||||
return db
|
||||
}
|
||||
}
|
||||
if db, exists := globalConnections[fmt.Sprintf("%s", s.Connection)]; exists {
|
||||
return db
|
||||
}
|
||||
panic("no db found")
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
//
|
||||
// schema_test.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
err = SetupConnections(ConnectionConfig{
|
||||
Name: "default",
|
||||
DB: db,
|
||||
Dialect: Dialects.SQLite3,
|
||||
})
|
||||
// orm.Schematic()
|
||||
_, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, body text, created_at TIMESTAMP, updated_at TIMESTAMP, deleted_at TIMESTAMP)`)
|
||||
_, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS emails (id INTEGER PRIMARY KEY, post_id INTEGER, email text)`)
|
||||
_, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS header_pictures (id INTEGER PRIMARY KEY, post_id INTEGER, link text)`)
|
||||
_, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, post_id INTEGER, body text)`)
|
||||
_, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY, title text)`)
|
||||
_, err = GetConnection("default").DB.Exec(`CREATE TABLE IF NOT EXISTS post_categories (post_id INTEGER, category_id INTEGER, PRIMARY KEY(post_id, category_id))`)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
ID int64
|
||||
Name string
|
||||
Timestamps
|
||||
}
|
||||
|
||||
func (o Object) ConfigureEntity(e *EntityConfigurator) {
|
||||
e.Table("objects").Connection("default")
|
||||
}
|
||||
|
||||
func TestGenericFieldsOf(t *testing.T) {
|
||||
t.Run("fields of with id and timestamps embedded", func(t *testing.T) {
|
||||
fs := genericFieldsOf(&Object{})
|
||||
assert.Len(t, fs, 5)
|
||||
assert.Equal(t, "id", fs[0].Name)
|
||||
assert.True(t, fs[0].IsPK)
|
||||
assert.Equal(t, "name", fs[1].Name)
|
||||
assert.Equal(t, "created_at", fs[2].Name)
|
||||
assert.Equal(t, "updated_at", fs[3].Name)
|
||||
assert.Equal(t, "deleted_at", fs[4].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenericValuesOf(t *testing.T) {
|
||||
t.Run("values of", func(t *testing.T) {
|
||||
|
||||
setup(t)
|
||||
vs := genericValuesOf(Object{}, true)
|
||||
assert.Len(t, vs, 5)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEntityConfigurator(t *testing.T) {
|
||||
t.Run("test has many with user provided values", func(t *testing.T) {
|
||||
setup(t)
|
||||
var ec EntityConfigurator
|
||||
ec.Table("users").Connection("default").HasMany(Object{}, HasManyConfig{
|
||||
"objects", "user_id",
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// timestamps.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package orm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Timestamps struct {
|
||||
CreatedAt sql.NullTime
|
||||
UpdatedAt sql.NullTime
|
||||
DeletedAt sql.NullTime
|
||||
}
|
@ -1,507 +0,0 @@
|
||||
//
|
||||
// pluralize.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PluralizeRule -- pluralize rule expression and replacement value.
|
||||
type PluralizeRule struct {
|
||||
expression *regexp.Regexp
|
||||
replacement string
|
||||
}
|
||||
|
||||
// PluralizeClient -- pluralize client.
|
||||
type PluralizeClient struct {
|
||||
pluralRules []PluralizeRule
|
||||
singularRules []PluralizeRule
|
||||
uncountables map[string]bool
|
||||
irregularSingles map[string]string
|
||||
irregularPlurals map[string]string
|
||||
interpolateExpr *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewPluralizeClient() *PluralizeClient {
|
||||
client := PluralizeClient{}
|
||||
client.init()
|
||||
|
||||
return &client
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) init() {
|
||||
c.pluralRules = make([]PluralizeRule, 0)
|
||||
c.singularRules = make([]PluralizeRule, 0)
|
||||
c.uncountables = make(map[string]bool)
|
||||
c.irregularSingles = make(map[string]string)
|
||||
c.irregularPlurals = make(map[string]string)
|
||||
|
||||
c.loadIrregularRules()
|
||||
c.loadPluralizationRules()
|
||||
c.loadSingularizationRules()
|
||||
c.loadUncountableRules()
|
||||
c.interpolateExpr = regexp.MustCompile(`\$(\d{1,2})`)
|
||||
}
|
||||
|
||||
// Pluralize -- Pluralize or singularize a word based on the passed in count.
|
||||
//
|
||||
// word: the word to pluralize
|
||||
// count: how many of the word exist
|
||||
// inclusive: whether to prefix with the number (e.g. 3 ducks)
|
||||
func (c *PluralizeClient) Pluralize(word string, count int, inclusive bool) string {
|
||||
pluralized := func() func(string) string {
|
||||
if count == 1 {
|
||||
return c.Singular
|
||||
}
|
||||
|
||||
return c.Plural
|
||||
}
|
||||
|
||||
if inclusive {
|
||||
return fmt.Sprintf("%d %s", count, pluralized()(word))
|
||||
}
|
||||
|
||||
return pluralized()(word)
|
||||
}
|
||||
|
||||
// Plural -- Pluralize a word.
|
||||
func (c *PluralizeClient) Plural(word string) string {
|
||||
return c.replaceWord(c.irregularSingles, c.irregularPlurals, c.pluralRules)(word)
|
||||
}
|
||||
|
||||
// IsPlural -- Check if a word is plural.
|
||||
func (c *PluralizeClient) IsPlural(word string) bool {
|
||||
return c.checkWord(c.irregularSingles, c.irregularPlurals, c.pluralRules)(word)
|
||||
}
|
||||
|
||||
// Singular -- Singularize a word.
|
||||
func (c *PluralizeClient) Singular(word string) string {
|
||||
return c.replaceWord(c.irregularPlurals, c.irregularSingles, c.singularRules)(word)
|
||||
}
|
||||
|
||||
// IsSingular -- Check if a word is singular.
|
||||
func (c *PluralizeClient) IsSingular(word string) bool {
|
||||
return c.checkWord(c.irregularPlurals, c.irregularSingles, c.singularRules)(word)
|
||||
}
|
||||
|
||||
// AddPluralRule -- Add a pluralization rule to the collection.
|
||||
func (c *PluralizeClient) AddPluralRule(rule string, replacement string) {
|
||||
c.pluralRules = append(c.pluralRules, PluralizeRule{sanitizeRule(rule), replacement})
|
||||
}
|
||||
|
||||
// AddSingularRule -- Add a singularization rule to the collection.
|
||||
func (c *PluralizeClient) AddSingularRule(rule string, replacement string) {
|
||||
c.singularRules = append(c.singularRules, PluralizeRule{sanitizeRule(rule), replacement})
|
||||
}
|
||||
|
||||
// AddUncountableRule -- Add an uncountable word rule.
|
||||
func (c *PluralizeClient) AddUncountableRule(word string) {
|
||||
if !isExpr(word) {
|
||||
c.uncountables[strings.ToLower(word)] = true
|
||||
return
|
||||
}
|
||||
|
||||
c.AddPluralRule(word, `$0`)
|
||||
c.AddSingularRule(word, `$0`)
|
||||
}
|
||||
|
||||
// AddIrregularRule -- Add an irregular word definition.
|
||||
func (c *PluralizeClient) AddIrregularRule(single string, plural string) {
|
||||
p := strings.ToLower(plural)
|
||||
s := strings.ToLower(single)
|
||||
|
||||
c.irregularSingles[s] = p
|
||||
c.irregularPlurals[p] = s
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) replaceWord(replaceMap map[string]string, keepMap map[string]string, rules []PluralizeRule) func(w string) string { //nolint:lll
|
||||
f := func(word string) string {
|
||||
// Get the correct token and case restoration functions.
|
||||
var token = strings.ToLower(word)
|
||||
|
||||
// Check against the keep object map.
|
||||
if _, ok := keepMap[token]; ok {
|
||||
return restoreCase(word, token)
|
||||
}
|
||||
|
||||
// Check against the replacement map for a direct word replacement.
|
||||
if replaceToken, ok := replaceMap[token]; ok {
|
||||
return restoreCase(word, replaceToken)
|
||||
}
|
||||
|
||||
// Run all the rules against the word.
|
||||
return c.sanitizeWord(token, word, rules)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) checkWord(replaceMap map[string]string, keepMap map[string]string, rules []PluralizeRule) func(w string) bool {
|
||||
f := func(word string) bool {
|
||||
var token = strings.ToLower(word)
|
||||
|
||||
if _, ok := keepMap[token]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := replaceMap[token]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return c.sanitizeWord(token, token, rules) == token
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) interpolate(str string, args []string) string {
|
||||
lookup := map[string]string{}
|
||||
|
||||
for _, submatch := range c.interpolateExpr.FindAllStringSubmatch(str, -1) {
|
||||
element, _ := strconv.Atoi(submatch[1])
|
||||
lookup[submatch[0]] = args[element]
|
||||
}
|
||||
|
||||
result := c.interpolateExpr.ReplaceAllStringFunc(str, func(repl string) string {
|
||||
return lookup[repl]
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) replace(word string, rule PluralizeRule) string {
|
||||
return rule.expression.ReplaceAllStringFunc(word, func(w string) string {
|
||||
match := rule.expression.FindString(word)
|
||||
index := rule.expression.FindStringIndex(word)[0]
|
||||
args := rule.expression.FindAllStringSubmatch(word, -1)[0]
|
||||
|
||||
result := c.interpolate(rule.replacement, args)
|
||||
|
||||
if match == `` {
|
||||
return restoreCase(word[index-1:index], result)
|
||||
}
|
||||
return restoreCase(match, result)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) sanitizeWord(token string, word string, rules []PluralizeRule) string {
|
||||
// If empty string
|
||||
if len(token) == 0 {
|
||||
return word
|
||||
}
|
||||
// If does not need fixup
|
||||
if _, ok := c.uncountables[token]; ok {
|
||||
return word
|
||||
}
|
||||
|
||||
// Iterate over the sanitization rules and use the first one to match.
|
||||
// NOTE: iterate rules array in reverse order specific => general rules
|
||||
for i := len(rules) - 1; i >= 0; i-- {
|
||||
if rules[i].expression.MatchString(word) {
|
||||
return c.replace(word, rules[i])
|
||||
}
|
||||
}
|
||||
|
||||
return word
|
||||
}
|
||||
|
||||
func sanitizeRule(rule string) *regexp.Regexp {
|
||||
if isExpr(rule) {
|
||||
return regexp.MustCompile(rule)
|
||||
}
|
||||
|
||||
return regexp.MustCompile(`(?i)^` + rule + `$`)
|
||||
}
|
||||
|
||||
func restoreCase(word string, token string) string {
|
||||
// Tokens are an exact match.
|
||||
if word == token {
|
||||
return token
|
||||
}
|
||||
|
||||
// Lower cased words. E.g. "hello".
|
||||
if word == strings.ToLower(word) {
|
||||
return strings.ToLower(token)
|
||||
}
|
||||
|
||||
// Upper cased words. E.g. "WHISKY".
|
||||
if word == strings.ToUpper(word) {
|
||||
return strings.ToUpper(token)
|
||||
}
|
||||
|
||||
// Title cased words. E.g. "Title".
|
||||
if word[:1] == strings.ToUpper(word[:1]) {
|
||||
return strings.ToUpper(token[:1]) + strings.ToLower(token[1:])
|
||||
}
|
||||
|
||||
// Lower cased words. E.g. "test".
|
||||
return strings.ToLower(token)
|
||||
}
|
||||
|
||||
// isExpr -- helper to detect if string represents an expression by checking first character to be `(`.
|
||||
func isExpr(s string) bool {
|
||||
return s[:1] == `(`
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) loadIrregularRules() { //nolint:funlen
|
||||
var irregularRules = []struct {
|
||||
single string
|
||||
plural string
|
||||
}{
|
||||
// Pronouns.
|
||||
{`I`, `we`},
|
||||
{`me`, `us`},
|
||||
{`he`, `they`},
|
||||
{`she`, `they`},
|
||||
{`them`, `them`},
|
||||
{`myself`, `ourselves`},
|
||||
{`yourself`, `yourselves`},
|
||||
{`itself`, `themselves`},
|
||||
{`herself`, `themselves`},
|
||||
{`himself`, `themselves`},
|
||||
{`themself`, `themselves`},
|
||||
{`is`, `are`},
|
||||
{`was`, `were`},
|
||||
{`has`, `have`},
|
||||
{`this`, `these`},
|
||||
{`that`, `those`},
|
||||
{`my`, `our`},
|
||||
{`its`, `their`},
|
||||
{`his`, `their`},
|
||||
{`her`, `their`},
|
||||
// Words ending in with a consonant and `o`.
|
||||
{`echo`, `echoes`},
|
||||
{`dingo`, `dingoes`},
|
||||
{`volcano`, `volcanoes`},
|
||||
{`tornado`, `tornadoes`},
|
||||
{`torpedo`, `torpedoes`},
|
||||
// Ends with `us`.
|
||||
{`genus`, `genera`},
|
||||
{`viscus`, `viscera`},
|
||||
// Ends with `ma`.
|
||||
{`stigma`, `stigmata`},
|
||||
{`stoma`, `stomata`},
|
||||
{`dogma`, `dogmata`},
|
||||
{`lemma`, `lemmata`},
|
||||
{`schema`, `schemata`},
|
||||
{`anathema`, `anathemata`},
|
||||
// Other irregular rules.
|
||||
{`ox`, `oxen`},
|
||||
{`axe`, `axes`},
|
||||
{`die`, `dice`},
|
||||
{`yes`, `yeses`},
|
||||
{`foot`, `feet`},
|
||||
{`eave`, `eaves`},
|
||||
{`goose`, `geese`},
|
||||
{`tooth`, `teeth`},
|
||||
{`quiz`, `quizzes`},
|
||||
{`human`, `humans`},
|
||||
{`proof`, `proofs`},
|
||||
{`carve`, `carves`},
|
||||
{`valve`, `valves`},
|
||||
{`looey`, `looies`},
|
||||
{`thief`, `thieves`},
|
||||
{`groove`, `grooves`},
|
||||
{`pickaxe`, `pickaxes`},
|
||||
{`passerby`, `passersby`},
|
||||
{`canvas`, `canvases`},
|
||||
{`sms`, `sms`},
|
||||
}
|
||||
|
||||
for _, r := range irregularRules {
|
||||
c.AddIrregularRule(r.single, r.plural)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) loadPluralizationRules() {
|
||||
var pluralizationRules = []struct {
|
||||
rule string
|
||||
replacement string
|
||||
}{
|
||||
{`(?i)s?$`, `s`},
|
||||
{`(?i)[^[:ascii:]]$`, `$0`},
|
||||
{`(?i)([^aeiou]ese)$`, `$1`},
|
||||
{`(?i)(ax|test)is$`, `$1es`},
|
||||
{`(?i)(alias|[^aou]us|t[lm]as|gas|ris)$`, `$1es`},
|
||||
{`(?i)(e[mn]u)s?$`, `$1s`},
|
||||
{`(?i)([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$`, `$1`},
|
||||
{`(?i)(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$`, `$1i`}, //nolint:lll,misspell
|
||||
{`(?i)(alumn|alg|vertebr)(?:a|ae)$`, `$1ae`},
|
||||
{`(?i)(seraph|cherub)(?:im)?$`, `$1im`},
|
||||
{`(?i)(her|at|gr)o$`, `$1oes`},
|
||||
{`(?i)(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$`, `$1a`}, //nolint:lll,misspell
|
||||
{`(?i)(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$`, `$1a`},
|
||||
{`(?i)sis$`, `ses`},
|
||||
{`(?i)(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$`, `$1$2ves`},
|
||||
{`(?i)([^aeiouy]|qu)y$`, `$1ies`},
|
||||
{`(?i)([^ch][ieo][ln])ey$`, `$1ies`},
|
||||
{`(?i)(x|ch|ss|sh|zz)$`, `$1es`},
|
||||
{`(?i)(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$`, `$1ices`},
|
||||
{`(?i)\b((?:tit)?m|l)(?:ice|ouse)$`, `$1ice`},
|
||||
{`(?i)(pe)(?:rson|ople)$`, `$1ople`},
|
||||
{`(?i)(child)(?:ren)?$`, `$1ren`},
|
||||
{`(?i)eaux$`, `$0`},
|
||||
{`(?i)m[ae]n$`, `men`},
|
||||
{`thou`, `you`},
|
||||
}
|
||||
|
||||
for _, r := range pluralizationRules {
|
||||
c.AddPluralRule(r.rule, r.replacement)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) loadSingularizationRules() {
|
||||
var singularizationRules = []struct {
|
||||
rule string
|
||||
replacement string
|
||||
}{
|
||||
{`(?i)s$`, ``},
|
||||
{`(?i)(ss)$`, `$1`},
|
||||
{`(?i)(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$`, `$1fe`},
|
||||
{`(?i)(ar|(?:wo|[ae])l|[eo][ao])ves$`, `$1f`},
|
||||
{`(?i)ies$`, `y`},
|
||||
{`(?i)(dg|ss|ois|lk|ok|wn|mb|th|ch|ec|oal|is|ck|ix|sser|ts|wb)ies$`, `$1ie`},
|
||||
{`(?i)\b(l|(?:neck|cross|hog|aun)?t|coll|faer|food|gen|goon|group|hipp|junk|vegg|(?:pork)?p|charl|calor|cut)ies$`, `$1ie`}, //nolint:lll
|
||||
{`(?i)\b(mon|smil)ies$`, `$1ey`},
|
||||
{`(?i)\b((?:tit)?m|l)ice$`, `$1ouse`},
|
||||
{`(?i)(seraph|cherub)im$`, `$1`},
|
||||
{`(?i)(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$`, `$1`},
|
||||
{`(?i)(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$`, `$1sis`},
|
||||
{`(?i)(movie|twelve|abuse|e[mn]u)s$`, `$1`},
|
||||
{`(?i)(test)(?:is|es)$`, `$1is`},
|
||||
{`(?i)(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$`, `$1us`}, //nolint:lll,misspell
|
||||
{`(?i)(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$`, `$1um`}, //nolint:lll,misspell
|
||||
{`(?i)(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$`, `$1on`},
|
||||
{`(?i)(alumn|alg|vertebr)ae$`, `$1a`},
|
||||
{`(?i)(cod|mur|sil|vert|ind)ices$`, `$1ex`},
|
||||
{`(?i)(matr|append)ices$`, `$1ix`},
|
||||
{`(?i)(pe)(rson|ople)$`, `$1rson`},
|
||||
{`(?i)(child)ren$`, `$1`},
|
||||
{`(?i)(eau)x?$`, `$1`},
|
||||
{`(?i)men$`, `man`},
|
||||
}
|
||||
|
||||
for _, r := range singularizationRules {
|
||||
c.AddSingularRule(r.rule, r.replacement)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PluralizeClient) loadUncountableRules() { //nolint:funlen
|
||||
var uncountableRules = []string{
|
||||
// Singular words with no plurals.
|
||||
`adulthood`,
|
||||
`advice`,
|
||||
`agenda`,
|
||||
`aid`,
|
||||
`aircraft`,
|
||||
`alcohol`,
|
||||
`ammo`,
|
||||
`analytics`,
|
||||
`anime`,
|
||||
`athletics`,
|
||||
`audio`,
|
||||
`bison`,
|
||||
`blood`,
|
||||
`bream`,
|
||||
`buffalo`,
|
||||
`butter`,
|
||||
`carp`,
|
||||
`cash`,
|
||||
`chassis`,
|
||||
`chess`,
|
||||
`clothing`,
|
||||
`cod`,
|
||||
`commerce`,
|
||||
`cooperation`,
|
||||
`corps`,
|
||||
`debris`,
|
||||
`diabetes`,
|
||||
`digestion`,
|
||||
`elk`,
|
||||
`energy`,
|
||||
`equipment`,
|
||||
`excretion`,
|
||||
`expertise`,
|
||||
`firmware`,
|
||||
`flounder`,
|
||||
`fun`,
|
||||
`gallows`,
|
||||
`garbage`,
|
||||
`graffiti`,
|
||||
`hardware`,
|
||||
`headquarters`,
|
||||
`health`,
|
||||
`herpes`,
|
||||
`highjinks`,
|
||||
`homework`,
|
||||
`housework`,
|
||||
`information`,
|
||||
`jeans`,
|
||||
`justice`,
|
||||
`kudos`,
|
||||
`labour`,
|
||||
`literature`,
|
||||
`machinery`,
|
||||
`mackerel`,
|
||||
`mail`,
|
||||
`media`,
|
||||
`mews`,
|
||||
`moose`,
|
||||
`music`,
|
||||
`mud`,
|
||||
`manga`,
|
||||
`news`,
|
||||
`only`,
|
||||
`personnel`,
|
||||
`pike`,
|
||||
`plankton`,
|
||||
`pliers`,
|
||||
`police`,
|
||||
`pollution`,
|
||||
`premises`,
|
||||
`rain`,
|
||||
`research`,
|
||||
`rice`,
|
||||
`salmon`,
|
||||
`scissors`,
|
||||
`series`,
|
||||
`sewage`,
|
||||
`shambles`,
|
||||
`shrimp`,
|
||||
`software`,
|
||||
`staff`,
|
||||
`swine`,
|
||||
`tennis`,
|
||||
`traffic`,
|
||||
`transportation`,
|
||||
`trout`,
|
||||
`tuna`,
|
||||
`wealth`,
|
||||
`welfare`,
|
||||
`whiting`,
|
||||
`wildebeest`,
|
||||
`wildlife`,
|
||||
`you`,
|
||||
// Regexes.
|
||||
`(?i)pok[eé]mon$`, //
|
||||
`(?i)[^aeiou]ese$`, // "chinese", "japanese"
|
||||
`(?i)deer$`, // "deer", "reindeer"
|
||||
`(?i)(fish)$`, // "fish", "blowfish", "angelfish"
|
||||
`(?i)measles$`, //
|
||||
`(?i)o[iu]s$`, // "carnivorous"
|
||||
`(?i)pox$`, // "chickpox", "smallpox"
|
||||
`(?i)sheep$`, //
|
||||
}
|
||||
|
||||
for _, w := range uncountableRules {
|
||||
c.AddUncountableRule(w)
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package table
|
||||
|
||||
import "git.hexq.cn/tiglog/golib/helper/text"
|
||||
|
||||
// ColumnConfig contains configurations that determine and modify the way the
|
||||
// contents of the column get rendered.
|
||||
type ColumnConfig struct {
|
||||
// Name is the name of the Column as it appears in the first Header row.
|
||||
// If a Header is not provided, or the name is not found in the header, this
|
||||
// will not work.
|
||||
Name string
|
||||
// Number is the Column # from left. When specified, it overrides the Name
|
||||
// property. If you know the exact Column number, use this instead of Name.
|
||||
Number int
|
||||
|
||||
// Align defines the horizontal alignment
|
||||
Align text.Align
|
||||
// AlignFooter defines the horizontal alignment of Footer rows
|
||||
AlignFooter text.Align
|
||||
// AlignHeader defines the horizontal alignment of Header rows
|
||||
AlignHeader text.Align
|
||||
|
||||
// AutoMerge merges cells with similar values and prevents separators from
|
||||
// being drawn. Caveats:
|
||||
// * VAlign is applied on the individual cell and not on the merged cell
|
||||
// * Does not work in CSV/HTML/Markdown render modes
|
||||
// * Does not work well with horizontal auto-merge (RowConfig.AutoMerge)
|
||||
//
|
||||
// Works best when:
|
||||
// * Style().Options.SeparateRows == true
|
||||
// * Style().Color.Row == Style().Color.RowAlternate (or not set)
|
||||
AutoMerge bool
|
||||
|
||||
// Colors defines the colors to be used on the column
|
||||
Colors text.Colors
|
||||
// ColorsFooter defines the colors to be used on the column in Footer rows
|
||||
ColorsFooter text.Colors
|
||||
// ColorsHeader defines the colors to be used on the column in Header rows
|
||||
ColorsHeader text.Colors
|
||||
|
||||
// Hidden when set to true will prevent the column from being rendered.
|
||||
// This is useful in cases like needing a column for sorting, but not for
|
||||
// display.
|
||||
Hidden bool
|
||||
|
||||
// Transformer is a custom-function that changes the way the value gets
|
||||
// rendered to the console. Refer to text/transformer.go for ready-to-use
|
||||
// Transformer functions.
|
||||
Transformer text.Transformer
|
||||
// TransformerFooter is like Transformer but for Footer rows
|
||||
TransformerFooter text.Transformer
|
||||
// TransformerHeader is like Transformer but for Header rows
|
||||
TransformerHeader text.Transformer
|
||||
|
||||
// VAlign defines the vertical alignment
|
||||
VAlign text.VAlign
|
||||
// VAlignFooter defines the vertical alignment in Footer rows
|
||||
VAlignFooter text.VAlign
|
||||
// VAlignHeader defines the vertical alignment in Header rows
|
||||
VAlignHeader text.VAlign
|
||||
|
||||
// WidthMax defines the maximum character length of the column
|
||||
WidthMax int
|
||||
// WidthEnforcer enforces the WidthMax value on the column contents;
|
||||
// default: text.WrapText
|
||||
WidthMaxEnforcer WidthEnforcer
|
||||
// WidthMin defines the minimum character length of the column
|
||||
WidthMin int
|
||||
}
|
||||
|
||||
func (c ColumnConfig) getWidthMaxEnforcer() WidthEnforcer {
|
||||
if c.WidthMax == 0 {
|
||||
return widthEnforcerNone
|
||||
}
|
||||
if c.WidthMaxEnforcer != nil {
|
||||
return c.WidthMaxEnforcer
|
||||
}
|
||||
return text.WrapText
|
||||
}
|
||||
|
||||
// RowConfig contains configurations that determine and modify the way the
|
||||
// contents of a row get rendered.
|
||||
type RowConfig struct {
|
||||
// AutoMerge merges cells with similar values and prevents separators from
|
||||
// being drawn. Caveats:
|
||||
// * Align is overridden to text.AlignCenter on the merged cell (unless set
|
||||
// by AutoMergeAlign value below)
|
||||
// * Does not work in CSV/HTML/Markdown render modes
|
||||
// * Does not work well with vertical auto-merge (ColumnConfig.AutoMerge)
|
||||
AutoMerge bool
|
||||
|
||||
// Alignment to use on a merge (defaults to text.AlignCenter)
|
||||
AutoMergeAlign text.Align
|
||||
}
|
||||
|
||||
func (rc RowConfig) getAutoMergeAlign() text.Align {
|
||||
if rc.AutoMergeAlign == text.AlignDefault {
|
||||
return text.AlignCenter
|
||||
}
|
||||
return rc.AutoMergeAlign
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
//
|
||||
// config_test.go
|
||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/text"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestColumnConfig_getWidthMaxEnforcer(t *testing.T) {
|
||||
t.Run("no width enforcer", func(t *testing.T) {
|
||||
cc := ColumnConfig{}
|
||||
|
||||
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 0))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 5))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 10))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 100))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1000))
|
||||
})
|
||||
|
||||
t.Run("default width enforcer", func(t *testing.T) {
|
||||
cc := ColumnConfig{
|
||||
WidthMax: 10,
|
||||
}
|
||||
|
||||
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||
assert.Equal(t, "", widthEnforcer("1234567890", 0))
|
||||
assert.Equal(t, "1\n2\n3\n4\n5\n6\n7\n8\n9\n0", widthEnforcer("1234567890", 1))
|
||||
assert.Equal(t, "12345\n67890", widthEnforcer("1234567890", 5))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 10))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 100))
|
||||
assert.Equal(t, "1234567890", widthEnforcer("1234567890", 1000))
|
||||
})
|
||||
|
||||
t.Run("custom width enforcer (1)", func(t *testing.T) {
|
||||
cc := ColumnConfig{
|
||||
WidthMax: 10,
|
||||
WidthMaxEnforcer: text.Trim,
|
||||
}
|
||||
|
||||
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||
assert.Equal(t, text.Trim("1234567890", 0), widthEnforcer("1234567890", 0))
|
||||
assert.Equal(t, text.Trim("1234567890", 1), widthEnforcer("1234567890", 1))
|
||||
assert.Equal(t, text.Trim("1234567890", 5), widthEnforcer("1234567890", 5))
|
||||
assert.Equal(t, text.Trim("1234567890", 10), widthEnforcer("1234567890", 10))
|
||||
assert.Equal(t, text.Trim("1234567890", 100), widthEnforcer("1234567890", 100))
|
||||
assert.Equal(t, text.Trim("1234567890", 1000), widthEnforcer("1234567890", 1000))
|
||||
})
|
||||
|
||||
t.Run("custom width enforcer (2)", func(t *testing.T) {
|
||||
cc := ColumnConfig{
|
||||
WidthMax: 10,
|
||||
WidthMaxEnforcer: func(col string, maxLen int) string {
|
||||
return "foo"
|
||||
},
|
||||
}
|
||||
|
||||
widthEnforcer := cc.getWidthMaxEnforcer()
|
||||
assert.Equal(t, "foo", widthEnforcer("1234567890", 0))
|
||||
assert.Equal(t, "foo", widthEnforcer("1234567890", 1))
|
||||
assert.Equal(t, "foo", widthEnforcer("1234567890", 5))
|
||||
assert.Equal(t, "foo", widthEnforcer("1234567890", 10))
|
||||
assert.Equal(t, "foo", widthEnforcer("1234567890", 100))
|
||||
assert.Equal(t, "foo", widthEnforcer("1234567890", 1000))
|
||||
})
|
||||
}
|
@ -1,408 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/text"
|
||||
)
|
||||
|
||||
// Render renders the Table in a human-readable "pretty" format. Example:
|
||||
//
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
func (t *Table) Render() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
t.renderTitle(&out)
|
||||
|
||||
// top-most border
|
||||
t.renderRowsBorderTop(&out)
|
||||
|
||||
// header rows
|
||||
t.renderRowsHeader(&out)
|
||||
|
||||
// (data) rows
|
||||
t.renderRows(&out, t.rows, renderHint{})
|
||||
|
||||
// footer rows
|
||||
t.renderRowsFooter(&out)
|
||||
|
||||
// bottom-most border
|
||||
t.renderRowsBorderBottom(&out)
|
||||
|
||||
// caption
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(t.caption)
|
||||
}
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) renderColumn(out *strings.Builder, row rowStr, colIdx int, maxColumnLength int, hint renderHint) int {
|
||||
numColumnsRendered := 1
|
||||
|
||||
// when working on the first column, and autoIndex is true, insert a new
|
||||
// column with the row number on it.
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
hintAutoIndex := hint
|
||||
hintAutoIndex.isAutoIndexColumn = true
|
||||
t.renderColumnAutoIndex(out, hintAutoIndex)
|
||||
}
|
||||
|
||||
// when working on column number 2 or more, render the column separator
|
||||
if colIdx > 0 {
|
||||
t.renderColumnSeparator(out, row, colIdx, hint)
|
||||
}
|
||||
|
||||
// extract the text, convert-case if not-empty and align horizontally
|
||||
mergeVertically := t.shouldMergeCellsVertically(colIdx, hint)
|
||||
var colStr string
|
||||
if mergeVertically {
|
||||
// leave colStr empty; align will expand the column as necessary
|
||||
} else if colIdx < len(row) {
|
||||
colStr = t.getFormat(hint).Apply(row[colIdx])
|
||||
}
|
||||
align := t.getAlign(colIdx, hint)
|
||||
|
||||
// if horizontal cell merges are enabled, look ahead and see how many cells
|
||||
// have the same content and merge them all until a cell with a different
|
||||
// content is found; override alignment to Center in this case
|
||||
rowConfig := t.getRowConfig(hint)
|
||||
if rowConfig.AutoMerge && !hint.isSeparatorRow {
|
||||
// get the real row to consider all lines in each column instead of just
|
||||
// looking at the current "line"
|
||||
rowUnwrapped := t.getRow(hint.rowNumber-1, hint)
|
||||
for idx := colIdx + 1; idx < len(rowUnwrapped); idx++ {
|
||||
if rowUnwrapped[colIdx] != rowUnwrapped[idx] {
|
||||
break
|
||||
}
|
||||
align = rowConfig.getAutoMergeAlign()
|
||||
maxColumnLength += t.getMaxColumnLengthForMerging(idx)
|
||||
numColumnsRendered++
|
||||
}
|
||||
}
|
||||
colStr = align.Apply(colStr, maxColumnLength)
|
||||
|
||||
// pad both sides of the column
|
||||
if !hint.isSeparatorRow || (hint.isSeparatorRow && mergeVertically) {
|
||||
colStr = t.style.Box.PaddingLeft + colStr + t.style.Box.PaddingRight
|
||||
}
|
||||
|
||||
t.renderColumnColorized(out, colIdx, colStr, hint)
|
||||
|
||||
return colIdx + numColumnsRendered
|
||||
}
|
||||
|
||||
func (t *Table) renderColumnAutoIndex(out *strings.Builder, hint renderHint) {
|
||||
var outAutoIndex strings.Builder
|
||||
outAutoIndex.Grow(t.maxColumnLengths[0])
|
||||
|
||||
if hint.isSeparatorRow {
|
||||
numChars := t.autoIndexVIndexMaxLength + utf8.RuneCountInString(t.style.Box.PaddingLeft) +
|
||||
utf8.RuneCountInString(t.style.Box.PaddingRight)
|
||||
chars := t.style.Box.MiddleHorizontal
|
||||
if hint.isAutoIndexColumn && hint.isHeaderOrFooterSeparator() {
|
||||
chars = text.RepeatAndTrim(" ", len(t.style.Box.MiddleHorizontal))
|
||||
}
|
||||
outAutoIndex.WriteString(text.RepeatAndTrim(chars, numChars))
|
||||
} else {
|
||||
outAutoIndex.WriteString(t.style.Box.PaddingLeft)
|
||||
rowNumStr := fmt.Sprint(hint.rowNumber)
|
||||
if hint.isHeaderRow || hint.isFooterRow || hint.rowLineNumber > 1 {
|
||||
rowNumStr = strings.Repeat(" ", t.autoIndexVIndexMaxLength)
|
||||
}
|
||||
outAutoIndex.WriteString(text.AlignRight.Apply(rowNumStr, t.autoIndexVIndexMaxLength))
|
||||
outAutoIndex.WriteString(t.style.Box.PaddingRight)
|
||||
}
|
||||
|
||||
if t.style.Color.IndexColumn != nil {
|
||||
colors := t.style.Color.IndexColumn
|
||||
if hint.isFooterRow {
|
||||
colors = t.style.Color.Footer
|
||||
}
|
||||
out.WriteString(colors.Sprint(outAutoIndex.String()))
|
||||
} else {
|
||||
out.WriteString(outAutoIndex.String())
|
||||
}
|
||||
hint.isAutoIndexColumn = true
|
||||
t.renderColumnSeparator(out, rowStr{}, 0, hint)
|
||||
}
|
||||
|
||||
func (t *Table) renderColumnColorized(out *strings.Builder, colIdx int, colStr string, hint renderHint) {
|
||||
colors := t.getColumnColors(colIdx, hint)
|
||||
if colors != nil {
|
||||
out.WriteString(colors.Sprint(colStr))
|
||||
} else if hint.isHeaderRow && t.style.Color.Header != nil {
|
||||
out.WriteString(t.style.Color.Header.Sprint(colStr))
|
||||
} else if hint.isFooterRow && t.style.Color.Footer != nil {
|
||||
out.WriteString(t.style.Color.Footer.Sprint(colStr))
|
||||
} else if hint.isRegularRow() {
|
||||
if colIdx == t.indexColumn-1 && t.style.Color.IndexColumn != nil {
|
||||
out.WriteString(t.style.Color.IndexColumn.Sprint(colStr))
|
||||
} else if hint.rowNumber%2 == 0 && t.style.Color.RowAlternate != nil {
|
||||
out.WriteString(t.style.Color.RowAlternate.Sprint(colStr))
|
||||
} else if t.style.Color.Row != nil {
|
||||
out.WriteString(t.style.Color.Row.Sprint(colStr))
|
||||
} else {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
} else {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderColumnSeparator(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||
if t.style.Options.SeparateColumns {
|
||||
separator := t.getColumnSeparator(row, colIdx, hint)
|
||||
|
||||
colors := t.getSeparatorColors(hint)
|
||||
if colors.EscapeSeq() != "" {
|
||||
out.WriteString(colors.Sprint(separator))
|
||||
} else {
|
||||
out.WriteString(separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
// if the output has content, it means that this call is working on line
|
||||
// number 2 or more; separate them with a newline
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
// use a brand-new strings.Builder if a row length limit has been set
|
||||
var outLine *strings.Builder
|
||||
if t.allowedRowLength > 0 {
|
||||
outLine = &strings.Builder{}
|
||||
} else {
|
||||
outLine = out
|
||||
}
|
||||
// grow the strings.Builder to the maximum possible row length
|
||||
outLine.Grow(t.maxRowLength)
|
||||
|
||||
nextColIdx := 0
|
||||
t.renderMarginLeft(outLine, hint)
|
||||
for colIdx, maxColumnLength := range t.maxColumnLengths {
|
||||
if colIdx != nextColIdx {
|
||||
continue
|
||||
}
|
||||
nextColIdx = t.renderColumn(outLine, row, colIdx, maxColumnLength, hint)
|
||||
}
|
||||
t.renderMarginRight(outLine, hint)
|
||||
|
||||
// merge the strings.Builder objects if a new one was created earlier
|
||||
if outLine != out {
|
||||
t.renderLineMergeOutputs(out, outLine)
|
||||
}
|
||||
|
||||
// if a page size has been set, and said number of lines has already
|
||||
// been rendered, and the header is not being rendered right now, render
|
||||
// the header all over again with a spacing line
|
||||
if hint.isRegularRow() {
|
||||
t.numLinesRendered++
|
||||
if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() {
|
||||
t.renderRowsFooter(out)
|
||||
t.renderRowsBorderBottom(out)
|
||||
out.WriteString(t.style.Box.PageSeparator)
|
||||
t.renderRowsBorderTop(out)
|
||||
t.renderRowsHeader(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) {
|
||||
outLineStr := outLine.String()
|
||||
if text.RuneWidthWithoutEscSequences(outLineStr) > t.allowedRowLength {
|
||||
trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow)
|
||||
if trimLength > 0 {
|
||||
out.WriteString(text.Trim(outLineStr, trimLength))
|
||||
out.WriteString(t.style.Box.UnfinishedRow)
|
||||
}
|
||||
} else {
|
||||
out.WriteString(outLineStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) {
|
||||
out.WriteString(t.style.Format.Direction.Modifier())
|
||||
if t.style.Options.DrawBorder {
|
||||
border := t.getBorderLeft(hint)
|
||||
colors := t.getBorderColors(hint)
|
||||
if colors.EscapeSeq() != "" {
|
||||
out.WriteString(colors.Sprint(border))
|
||||
} else {
|
||||
out.WriteString(border)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) {
|
||||
if t.style.Options.DrawBorder {
|
||||
border := t.getBorderRight(hint)
|
||||
colors := t.getBorderColors(hint)
|
||||
if colors.EscapeSeq() != "" {
|
||||
out.WriteString(colors.Sprint(border))
|
||||
} else {
|
||||
out.WriteString(border)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
if len(row) > 0 {
|
||||
// fit every column into the allowedColumnLength/maxColumnLength limit
|
||||
// and in the process find the max. number of lines in any column in
|
||||
// this row
|
||||
colMaxLines, rowWrapped := t.wrapRow(row)
|
||||
|
||||
// if there is just 1 line in all columns, add the row as such; else
|
||||
// split each column into individual lines and render them one-by-one
|
||||
if colMaxLines == 1 {
|
||||
hint.isLastLineOfRow = true
|
||||
t.renderLine(out, rowWrapped, hint)
|
||||
} else {
|
||||
// convert one row into N # of rows based on colMaxLines
|
||||
rowLines := make([]rowStr, len(row))
|
||||
for colIdx, colStr := range rowWrapped {
|
||||
rowLines[colIdx] = t.getVAlign(colIdx, hint).ApplyStr(colStr, colMaxLines)
|
||||
}
|
||||
for colLineIdx := 0; colLineIdx < colMaxLines; colLineIdx++ {
|
||||
rowLine := make(rowStr, len(rowLines))
|
||||
for colIdx, colLines := range rowLines {
|
||||
rowLine[colIdx] = colLines[colLineIdx]
|
||||
}
|
||||
hint.isLastLineOfRow = colLineIdx == colMaxLines-1
|
||||
hint.rowLineNumber = colLineIdx + 1
|
||||
t.renderLine(out, rowLine, hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowSeparator(out *strings.Builder, hint renderHint) {
|
||||
if hint.isBorderTop || hint.isBorderBottom {
|
||||
if !t.style.Options.DrawBorder {
|
||||
return
|
||||
}
|
||||
} else if hint.isHeaderRow && !t.style.Options.SeparateHeader {
|
||||
return
|
||||
} else if hint.isFooterRow && !t.style.Options.SeparateFooter {
|
||||
return
|
||||
}
|
||||
hint.isSeparatorRow = true
|
||||
t.renderLine(out, t.rowSeparator, hint)
|
||||
}
|
||||
|
||||
func (t *Table) renderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
for rowIdx, row := range rows {
|
||||
hint.isFirstRow = rowIdx == 0
|
||||
hint.isLastRow = rowIdx == len(rows)-1
|
||||
hint.rowNumber = rowIdx + 1
|
||||
t.renderRow(out, row, hint)
|
||||
|
||||
if (t.style.Options.SeparateRows && rowIdx < len(rows)-1) || // last row before footer
|
||||
(t.separators[rowIdx] && rowIdx != len(rows)-1) { // manually added separator not after last row
|
||||
hint.isFirstRow = false
|
||||
t.renderRowSeparator(out, hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsBorderBottom(out *strings.Builder) {
|
||||
if len(t.rowsFooter) > 0 {
|
||||
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: true, rowNumber: len(t.rowsFooter)})
|
||||
} else {
|
||||
t.renderRowSeparator(out, renderHint{isBorderBottom: true, isFooterRow: false, rowNumber: len(t.rows)})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsBorderTop(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 || t.autoIndex {
|
||||
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: true, rowNumber: 0})
|
||||
} else {
|
||||
t.renderRowSeparator(out, renderHint{isBorderTop: true, isHeaderRow: false, rowNumber: 0})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsFooter(out *strings.Builder) {
|
||||
if len(t.rowsFooter) > 0 {
|
||||
t.renderRowSeparator(out, renderHint{
|
||||
isFooterRow: true,
|
||||
isFirstRow: true,
|
||||
isSeparatorRow: true,
|
||||
})
|
||||
t.renderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderRowsHeader(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 || t.autoIndex {
|
||||
hintSeparator := renderHint{isHeaderRow: true, isLastRow: true, isSeparatorRow: true}
|
||||
|
||||
if len(t.rowsHeader) > 0 {
|
||||
t.renderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
hintSeparator.rowNumber = len(t.rowsHeader)
|
||||
} else if t.autoIndex {
|
||||
t.renderRow(out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
hintSeparator.rowNumber = 1
|
||||
}
|
||||
t.renderRowSeparator(out, hintSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderTitle(out *strings.Builder) {
|
||||
if t.title != "" {
|
||||
colors := t.style.Title.Colors
|
||||
colorsBorder := t.getBorderColors(renderHint{isTitleRow: true})
|
||||
rowLength := t.maxRowLength
|
||||
if t.allowedRowLength != 0 && t.allowedRowLength < rowLength {
|
||||
rowLength = t.allowedRowLength
|
||||
}
|
||||
if t.style.Options.DrawBorder {
|
||||
lenBorder := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.TopLeft+t.style.Box.TopRight)
|
||||
out.WriteString(colorsBorder.Sprint(t.style.Box.TopLeft))
|
||||
out.WriteString(colorsBorder.Sprint(text.RepeatAndTrim(t.style.Box.MiddleHorizontal, lenBorder)))
|
||||
out.WriteString(colorsBorder.Sprint(t.style.Box.TopRight))
|
||||
}
|
||||
|
||||
lenText := rowLength - text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft+t.style.Box.PaddingRight)
|
||||
if t.style.Options.DrawBorder {
|
||||
lenText -= text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
|
||||
}
|
||||
titleText := text.WrapText(t.title, lenText)
|
||||
for _, titleLine := range strings.Split(titleText, "\n") {
|
||||
t.renderTitleLine(out, lenText, titleLine, colors, colorsBorder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) renderTitleLine(out *strings.Builder, lenText int, titleLine string, colors text.Colors, colorsBorder text.Colors) {
|
||||
titleLine = strings.TrimSpace(titleLine)
|
||||
titleLine = t.style.Title.Format.Apply(titleLine)
|
||||
titleLine = t.style.Title.Align.Apply(titleLine, lenText)
|
||||
titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight
|
||||
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
if t.style.Options.DrawBorder {
|
||||
out.WriteString(colorsBorder.Sprint(t.style.Box.Left))
|
||||
}
|
||||
out.WriteString(colors.Sprint(titleLine))
|
||||
if t.style.Options.DrawBorder {
|
||||
out.WriteString(colorsBorder.Sprint(t.style.Box.Right))
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
func TestTable_Render_BiDiText(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendHeader(Row{"תאריך", "סכום", "מחלקה", "תגים"})
|
||||
table.AppendRow(Row{"2020-01-01", 5.0, "מחלקה1", []string{"תג1", "תג2"}})
|
||||
table.AppendRow(Row{"2021-02-01", 5.0, "מחלקה1", []string{"תג1"}})
|
||||
table.AppendRow(Row{"2022-03-01", 5.0, "מחלקה2", []string{"תג1"}})
|
||||
table.AppendFooter(Row{"סהכ", 30})
|
||||
table.SetAutoIndex(true)
|
||||
|
||||
//table.Style().Format.Direction = text.Default
|
||||
compareOutput(t, table.Render(), `
|
||||
+---+------------+------+--------+-----------+
|
||||
| | תאריך | סכום | מחלקה | תגים |
|
||||
+---+------------+------+--------+-----------+
|
||||
| 1 | 2020-01-01 | 5 | מחלקה1 | [תג1 תג2] |
|
||||
| 2 | 2021-02-01 | 5 | מחלקה1 | [תג1] |
|
||||
| 3 | 2022-03-01 | 5 | מחלקה2 | [תג1] |
|
||||
+---+------------+------+--------+-----------+
|
||||
| | סהכ | 30 | | |
|
||||
+---+------------+------+--------+-----------+`)
|
||||
|
||||
table.Style().Format.Direction = text.LeftToRight
|
||||
compareOutput(t, table.Render(), `
|
||||
+---+------------+------+--------+-----------+
|
||||
| | תאריך | סכום | מחלקה | תגים |
|
||||
+---+------------+------+--------+-----------+
|
||||
| 1 | 2020-01-01 | 5 | מחלקה1 | [תג1 תג2] |
|
||||
| 2 | 2021-02-01 | 5 | מחלקה1 | [תג1] |
|
||||
| 3 | 2022-03-01 | 5 | מחלקה2 | [תג1] |
|
||||
+---+------------+------+--------+-----------+
|
||||
| | סהכ | 30 | | |
|
||||
+---+------------+------+--------+-----------+`)
|
||||
|
||||
table.Style().Format.Direction = text.RightToLeft
|
||||
compareOutput(t, table.Render(), `
|
||||
+---+------------+------+--------+-----------+
|
||||
| | תאריך | סכום | מחלקה | תגים |
|
||||
+---+------------+------+--------+-----------+
|
||||
| 1 | 2020-01-01 | 5 | מחלקה1 | [תג1 תג2] |
|
||||
| 2 | 2021-02-01 | 5 | מחלקה1 | [תג1] |
|
||||
| 3 | 2022-03-01 | 5 | מחלקה2 | [תג1] |
|
||||
+---+------------+------+--------+-----------+
|
||||
| | סהכ | 30 | | |
|
||||
+---+------------+------+--------+-----------+`)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// RenderCSV renders the Table in CSV format. Example:
|
||||
// #,First Name,Last Name,Salary,
|
||||
// 1,Arya,Stark,3000,
|
||||
// 20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
// 300,Tyrion,Lannister,5000,
|
||||
// ,,Total,10000,
|
||||
func (t *Table) RenderCSV() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
if t.title != "" {
|
||||
out.WriteString(t.title)
|
||||
}
|
||||
if t.autoIndex && len(t.rowsHeader) == 0 {
|
||||
t.csvRenderRow(&out, t.getAutoIndexColumnIDs(), renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
}
|
||||
t.csvRenderRows(&out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
t.csvRenderRows(&out, t.rows, renderHint{})
|
||||
t.csvRenderRows(&out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(t.caption)
|
||||
}
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) csvFixCommas(str string) string {
|
||||
return strings.Replace(str, ",", "\\,", -1)
|
||||
}
|
||||
|
||||
func (t *Table) csvFixDoubleQuotes(str string) string {
|
||||
return strings.Replace(str, "\"", "\\\"", -1)
|
||||
}
|
||||
|
||||
func (t *Table) csvRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
// when working on line number 2 or more, insert a newline first
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
// generate the columns to render in CSV format and append to "out"
|
||||
for colIdx, colStr := range row {
|
||||
// auto-index column
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
if hint.isRegularRow() {
|
||||
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||
}
|
||||
out.WriteRune(',')
|
||||
}
|
||||
if colIdx > 0 {
|
||||
out.WriteRune(',')
|
||||
}
|
||||
if strings.ContainsAny(colStr, "\",\n") {
|
||||
out.WriteRune('"')
|
||||
out.WriteString(t.csvFixCommas(t.csvFixDoubleQuotes(colStr)))
|
||||
out.WriteRune('"')
|
||||
} else if utf8.RuneCountInString(colStr) > 0 {
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
}
|
||||
for colIdx := len(row); colIdx < t.numColumns; colIdx++ {
|
||||
out.WriteRune(',')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) csvRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
for rowIdx, row := range rows {
|
||||
hint.rowNumber = rowIdx + 1
|
||||
t.csvRenderRow(out, row, hint)
|
||||
}
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_RenderCSV(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(testRowMultiLine)
|
||||
tw.AppendRow(testRowTabs)
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SetCaption(testCaption)
|
||||
tw.SetTitle(testTitle1)
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), `
|
||||
Game of Thrones
|
||||
#,First Name,Last Name,Salary,
|
||||
1,Arya,Stark,3000,
|
||||
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
300,Tyrion,Lannister,5000,
|
||||
0,Winter,Is,0,"Coming.
|
||||
The North Remembers!
|
||||
This is known."
|
||||
0,Valar,Morghulis,0,Faceless Men
|
||||
,,Total,10000,
|
||||
A Song of Ice and Fire`)
|
||||
}
|
||||
|
||||
func TestTable_RenderCSV_AutoIndex(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
for rowIdx := 0; rowIdx < 10; rowIdx++ {
|
||||
row := make(Row, 10)
|
||||
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
for rowIdx := 0; rowIdx < 1; rowIdx++ {
|
||||
row := make(Row, 10)
|
||||
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
|
||||
}
|
||||
tw.AppendFooter(row)
|
||||
}
|
||||
tw.SetAutoIndex(true)
|
||||
tw.SetStyle(StyleLight)
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), `
|
||||
,A,B,C,D,E,F,G,H,I,J
|
||||
1,A1,B1,C1,D1,E1,F1,G1,H1,I1,J1
|
||||
2,A2,B2,C2,D2,E2,F2,G2,H2,I2,J2
|
||||
3,A3,B3,C3,D3,E3,F3,G3,H3,I3,J3
|
||||
4,A4,B4,C4,D4,E4,F4,G4,H4,I4,J4
|
||||
5,A5,B5,C5,D5,E5,F5,G5,H5,I5,J5
|
||||
6,A6,B6,C6,D6,E6,F6,G6,H6,I6,J6
|
||||
7,A7,B7,C7,D7,E7,F7,G7,H7,I7,J7
|
||||
8,A8,B8,C8,D8,E8,F8,G8,H8,I8,J8
|
||||
9,A9,B9,C9,D9,E9,F9,G9,H9,I9,J9
|
||||
10,A10,B10,C10,D10,E10,F10,G10,H10,I10,J10
|
||||
,AF,BF,CF,DF,EF,FF,GF,HF,IF,JF`)
|
||||
}
|
||||
|
||||
func TestTable_RenderCSV_Empty(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
assert.Empty(t, tw.RenderCSV())
|
||||
}
|
||||
|
||||
func TestTable_RenderCSV_HiddenColumns(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendFooter(testFooter)
|
||||
|
||||
// ensure sorting is done before hiding the columns
|
||||
tw.SortBy([]SortBy{
|
||||
{Name: "Salary", Mode: DscNumeric},
|
||||
})
|
||||
|
||||
t.Run("every column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), "")
|
||||
})
|
||||
|
||||
t.Run("first column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), `
|
||||
First Name,Last Name,Salary,
|
||||
>>Tyrion,Lannister<<,5013,
|
||||
>>Arya,Stark<<,3013,
|
||||
>>Jon,Snow<<,2013,"~You know nothing\, Jon Snow!~"
|
||||
,Total,10000,`)
|
||||
})
|
||||
|
||||
t.Run("column hidden in the middle", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), `
|
||||
#,Last Name,Salary,
|
||||
307,Lannister<<,5013,
|
||||
8,Stark<<,3013,
|
||||
27,Snow<<,2013,"~You know nothing\, Jon Snow!~"
|
||||
,Total,10000,`)
|
||||
})
|
||||
|
||||
t.Run("last column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), `
|
||||
#,First Name,Last Name,Salary
|
||||
307,>>Tyrion,Lannister<<,5013
|
||||
8,>>Arya,Stark<<,3013
|
||||
27,>>Jon,Snow<<,2013
|
||||
,,Total,10000`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTable_RenderCSV_Sorted(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||
|
||||
compareOutput(t, tw.RenderCSV(), `
|
||||
#,First Name,Last Name,Salary,
|
||||
300,Tyrion,Lannister,5000,
|
||||
20,Jon,Snow,2000,"You know nothing\, Jon Snow!"
|
||||
1,Arya,Stark,3000,
|
||||
11,Sansa,Stark,6000,
|
||||
,,Total,10000,`)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package table
|
||||
|
||||
// renderHint has hints for the Render*() logic
|
||||
type renderHint struct {
|
||||
isAutoIndexColumn bool // auto-index column?
|
||||
isAutoIndexRow bool // auto-index row?
|
||||
isBorderBottom bool // bottom-border?
|
||||
isBorderTop bool // top-border?
|
||||
isFirstRow bool // first-row of header/footer/regular-rows?
|
||||
isFooterRow bool // footer row?
|
||||
isHeaderRow bool // header row?
|
||||
isLastLineOfRow bool // last-line of the current row?
|
||||
isLastRow bool // last-row of header/footer/regular-rows?
|
||||
isSeparatorRow bool // separator row?
|
||||
isTitleRow bool // title row?
|
||||
rowLineNumber int // the line number for a multi-line row
|
||||
rowNumber int // the row number/index
|
||||
}
|
||||
|
||||
func (h *renderHint) isBorderOrSeparator() bool {
|
||||
return h.isBorderTop || h.isSeparatorRow || h.isBorderBottom
|
||||
}
|
||||
|
||||
func (h *renderHint) isRegularRow() bool {
|
||||
return !h.isHeaderRow && !h.isFooterRow
|
||||
}
|
||||
|
||||
func (h *renderHint) isRegularNonSeparatorRow() bool {
|
||||
return !h.isHeaderRow && !h.isFooterRow && !h.isSeparatorRow
|
||||
}
|
||||
|
||||
func (h *renderHint) isHeaderOrFooterSeparator() bool {
|
||||
return h.isSeparatorRow && !h.isBorderBottom && !h.isBorderTop &&
|
||||
((h.isHeaderRow && !h.isLastRow) || (h.isFooterRow && (!h.isFirstRow || h.rowNumber > 0)))
|
||||
}
|
||||
|
||||
func (h *renderHint) isLastLineOfLastRow() bool {
|
||||
return h.isLastLineOfRow && h.isLastRow
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHTMLCSSClass stores the css-class to use when none-provided via
|
||||
// SetHTMLCSSClass(cssClass string).
|
||||
DefaultHTMLCSSClass = "go-pretty-table"
|
||||
)
|
||||
|
||||
// RenderHTML renders the Table in HTML format. Example:
|
||||
// <table class="go-pretty-table">
|
||||
// <thead>
|
||||
// <tr>
|
||||
// <th align="right">#</th>
|
||||
// <th>First Name</th>
|
||||
// <th>Last Name</th>
|
||||
// <th align="right">Salary</th>
|
||||
// <th> </th>
|
||||
// </tr>
|
||||
// </thead>
|
||||
// <tbody>
|
||||
// <tr>
|
||||
// <td align="right">1</td>
|
||||
// <td>Arya</td>
|
||||
// <td>Stark</td>
|
||||
// <td align="right">3000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">20</td>
|
||||
// <td>Jon</td>
|
||||
// <td>Snow</td>
|
||||
// <td align="right">2000</td>
|
||||
// <td>You know nothing, Jon Snow!</td>
|
||||
// </tr>
|
||||
// <tr>
|
||||
// <td align="right">300</td>
|
||||
// <td>Tyrion</td>
|
||||
// <td>Lannister</td>
|
||||
// <td align="right">5000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tbody>
|
||||
// <tfoot>
|
||||
// <tr>
|
||||
// <td align="right"> </td>
|
||||
// <td> </td>
|
||||
// <td>Total</td>
|
||||
// <td align="right">10000</td>
|
||||
// <td> </td>
|
||||
// </tr>
|
||||
// </tfoot>
|
||||
// </table>
|
||||
func (t *Table) RenderHTML() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
out.WriteString("<table class=\"")
|
||||
if t.htmlCSSClass != "" {
|
||||
out.WriteString(t.htmlCSSClass)
|
||||
} else {
|
||||
out.WriteString(t.style.HTML.CSSClass)
|
||||
}
|
||||
out.WriteString("\">\n")
|
||||
t.htmlRenderTitle(&out)
|
||||
t.htmlRenderRowsHeader(&out)
|
||||
t.htmlRenderRows(&out, t.rows, renderHint{})
|
||||
t.htmlRenderRowsFooter(&out)
|
||||
t.htmlRenderCaption(&out)
|
||||
out.WriteString("</table>")
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (string, string) {
|
||||
// get the column contents
|
||||
var colStr string
|
||||
if colIdx < len(row) {
|
||||
colStr = row[colIdx]
|
||||
}
|
||||
|
||||
// header uses "th" instead of "td"
|
||||
colTagName := "td"
|
||||
if hint.isHeaderRow {
|
||||
colTagName = "th"
|
||||
}
|
||||
|
||||
return colStr, colTagName
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderCaption(out *strings.Builder) {
|
||||
if t.caption != "" {
|
||||
out.WriteString(" <caption class=\"caption\" style=\"caption-side: bottom;\">")
|
||||
out.WriteString(t.caption)
|
||||
out.WriteString("</caption>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) {
|
||||
if t.style.HTML.EscapeText {
|
||||
colStr = html.EscapeString(colStr)
|
||||
}
|
||||
if t.style.HTML.Newline != "\n" {
|
||||
colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1)
|
||||
}
|
||||
out.WriteString(colStr)
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, row rowStr, colIdx int, hint renderHint) {
|
||||
// determine the HTML "align"/"valign" property values
|
||||
align := t.getAlign(colIdx, hint).HTMLProperty()
|
||||
vAlign := t.getVAlign(colIdx, hint).HTMLProperty()
|
||||
// determine the HTML "class" property values for the colors
|
||||
class := t.getColumnColors(colIdx, hint).HTMLProperty()
|
||||
|
||||
if align != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(align)
|
||||
}
|
||||
if class != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(class)
|
||||
}
|
||||
if vAlign != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(vAlign)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint) {
|
||||
if hint.isHeaderRow {
|
||||
out.WriteString(" <th>")
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
out.WriteString("</th>\n")
|
||||
} else if hint.isFooterRow {
|
||||
out.WriteString(" <td>")
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
out.WriteString("</td>\n")
|
||||
} else {
|
||||
out.WriteString(" <td align=\"right\">")
|
||||
out.WriteString(fmt.Sprint(hint.rowNumber))
|
||||
out.WriteString("</td>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
out.WriteString(" <tr>\n")
|
||||
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||
// auto-index column
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
t.htmlRenderColumnAutoIndex(out, hint)
|
||||
}
|
||||
|
||||
colStr, colTagName := t.htmlGetColStrAndTag(row, colIdx, hint)
|
||||
// write the row
|
||||
out.WriteString(" <")
|
||||
out.WriteString(colTagName)
|
||||
t.htmlRenderColumnAttributes(out, row, colIdx, hint)
|
||||
out.WriteString(">")
|
||||
if len(colStr) == 0 {
|
||||
out.WriteString(t.style.HTML.EmptyColumn)
|
||||
} else {
|
||||
t.htmlRenderColumn(out, colStr)
|
||||
}
|
||||
out.WriteString("</")
|
||||
out.WriteString(colTagName)
|
||||
out.WriteString(">\n")
|
||||
}
|
||||
out.WriteString(" </tr>\n")
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
if len(rows) > 0 {
|
||||
// determine that tag to use based on the type of the row
|
||||
rowsTag := "tbody"
|
||||
if hint.isHeaderRow {
|
||||
rowsTag = "thead"
|
||||
} else if hint.isFooterRow {
|
||||
rowsTag = "tfoot"
|
||||
}
|
||||
|
||||
var renderedTagOpen, shouldRenderTagClose bool
|
||||
for idx, row := range rows {
|
||||
hint.rowNumber = idx + 1
|
||||
if len(row) > 0 {
|
||||
if !renderedTagOpen {
|
||||
out.WriteString(" <")
|
||||
out.WriteString(rowsTag)
|
||||
out.WriteString(">\n")
|
||||
renderedTagOpen = true
|
||||
}
|
||||
t.htmlRenderRow(out, row, hint)
|
||||
shouldRenderTagClose = true
|
||||
}
|
||||
}
|
||||
if shouldRenderTagClose {
|
||||
out.WriteString(" </")
|
||||
out.WriteString(rowsTag)
|
||||
out.WriteString(">\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRowsFooter(out *strings.Builder) {
|
||||
if len(t.rowsFooter) > 0 {
|
||||
t.htmlRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderRowsHeader(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 {
|
||||
t.htmlRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
} else if t.autoIndex {
|
||||
hint := renderHint{isAutoIndexRow: true, isHeaderRow: true}
|
||||
t.htmlRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, hint)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) htmlRenderTitle(out *strings.Builder) {
|
||||
if t.title != "" {
|
||||
align := t.style.Title.Align.HTMLProperty()
|
||||
colors := t.style.Title.Colors.HTMLProperty()
|
||||
title := t.style.Title.Format.Apply(t.title)
|
||||
|
||||
out.WriteString(" <caption class=\"title\"")
|
||||
if align != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(align)
|
||||
}
|
||||
if colors != "" {
|
||||
out.WriteRune(' ')
|
||||
out.WriteString(colors)
|
||||
}
|
||||
out.WriteRune('>')
|
||||
out.WriteString(title)
|
||||
out.WriteString("</caption>\n")
|
||||
}
|
||||
}
|
@ -1,519 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_RenderHTML(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(testRowMultiLine)
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SetColumnConfigs([]ColumnConfig{
|
||||
{Name: "Salary", VAlign: text.VAlignBottom},
|
||||
{Number: 5, VAlign: text.VAlignBottom},
|
||||
})
|
||||
tw.SetTitle(testTitle1)
|
||||
tw.SetCaption(testCaption)
|
||||
tw.Style().Title = TitleOptions{
|
||||
Align: text.AlignLeft,
|
||||
Colors: text.Colors{text.BgBlack, text.Bold, text.FgHiBlue},
|
||||
Format: text.FormatTitle,
|
||||
}
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table">
|
||||
<caption class="title" align="left" class="bg-black bold fg-hi-blue">Game Of Thrones</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="right">#</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">1</td>
|
||||
<td>Arya</td>
|
||||
<td>Stark</td>
|
||||
<td align="right" valign="bottom">3000</td>
|
||||
<td valign="bottom"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">20</td>
|
||||
<td>Jon</td>
|
||||
<td>Snow</td>
|
||||
<td align="right" valign="bottom">2000</td>
|
||||
<td valign="bottom">You know nothing, Jon Snow!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">300</td>
|
||||
<td>Tyrion</td>
|
||||
<td>Lannister</td>
|
||||
<td align="right" valign="bottom">5000</td>
|
||||
<td valign="bottom"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">0</td>
|
||||
<td>Winter</td>
|
||||
<td>Is</td>
|
||||
<td align="right" valign="bottom">0</td>
|
||||
<td valign="bottom">Coming.<br/>The North Remembers!<br/>This is known.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td align="right"> </td>
|
||||
<td> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<caption class="caption" style="caption-side: bottom;">A Song of Ice and Fire</caption>
|
||||
</table>`)
|
||||
}
|
||||
|
||||
func TestTable_RenderHTML_AutoIndex(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
for rowIdx := 0; rowIdx < 3; rowIdx++ {
|
||||
row := make(Row, 3)
|
||||
for colIdx := 0; colIdx < 3; colIdx++ {
|
||||
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
for rowIdx := 0; rowIdx < 1; rowIdx++ {
|
||||
row := make(Row, 3)
|
||||
for colIdx := 0; colIdx < 3; colIdx++ {
|
||||
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
|
||||
}
|
||||
tw.AppendFooter(row)
|
||||
}
|
||||
tw.SetOutputMirror(nil)
|
||||
tw.SetAutoIndex(true)
|
||||
tw.SetStyle(StyleLight)
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th align="center">A</th>
|
||||
<th align="center">B</th>
|
||||
<th align="center">C</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">1</td>
|
||||
<td>A1</td>
|
||||
<td>B1</td>
|
||||
<td>C1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">2</td>
|
||||
<td>A2</td>
|
||||
<td>B2</td>
|
||||
<td>C2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">3</td>
|
||||
<td>A3</td>
|
||||
<td>B3</td>
|
||||
<td>C3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>AF</td>
|
||||
<td>BF</td>
|
||||
<td>CF</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`)
|
||||
}
|
||||
|
||||
func TestTable_RenderHTML_Colored(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(testRowMultiLine)
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SetCaption(testCaption)
|
||||
tw.SetTitle(testTitle1)
|
||||
tw.Style().HTML.CSSClass = "go-pretty-table-colored"
|
||||
colorsBlackOnWhite := text.Colors{text.BgWhite, text.FgBlack}
|
||||
tw.SetColumnConfigs([]ColumnConfig{
|
||||
{
|
||||
Name: "#",
|
||||
Colors: text.Colors{text.Bold},
|
||||
ColorsHeader: colorsBlackOnWhite,
|
||||
}, {
|
||||
Name: "First Name",
|
||||
Colors: text.Colors{text.FgCyan},
|
||||
ColorsHeader: colorsBlackOnWhite,
|
||||
}, {
|
||||
Name: "Last Name",
|
||||
Colors: text.Colors{text.FgMagenta},
|
||||
ColorsHeader: colorsBlackOnWhite,
|
||||
ColorsFooter: colorsBlackOnWhite,
|
||||
}, {
|
||||
Name: "Salary",
|
||||
Colors: text.Colors{text.FgYellow},
|
||||
ColorsHeader: colorsBlackOnWhite,
|
||||
ColorsFooter: colorsBlackOnWhite,
|
||||
VAlign: text.VAlignBottom,
|
||||
}, {
|
||||
Number: 5,
|
||||
Colors: text.Colors{text.FgBlack},
|
||||
ColorsHeader: colorsBlackOnWhite,
|
||||
VAlign: text.VAlignBottom,
|
||||
},
|
||||
})
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table-colored">
|
||||
<caption class="title">Game of Thrones</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="right" class="bg-white fg-black">#</th>
|
||||
<th class="bg-white fg-black">First Name</th>
|
||||
<th class="bg-white fg-black">Last Name</th>
|
||||
<th align="right" class="bg-white fg-black">Salary</th>
|
||||
<th class="bg-white fg-black"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right" class="bold">1</td>
|
||||
<td class="fg-cyan">Arya</td>
|
||||
<td class="fg-magenta">Stark</td>
|
||||
<td align="right" class="fg-yellow" valign="bottom">3000</td>
|
||||
<td class="fg-black" valign="bottom"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right" class="bold">20</td>
|
||||
<td class="fg-cyan">Jon</td>
|
||||
<td class="fg-magenta">Snow</td>
|
||||
<td align="right" class="fg-yellow" valign="bottom">2000</td>
|
||||
<td class="fg-black" valign="bottom">You know nothing, Jon Snow!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right" class="bold">300</td>
|
||||
<td class="fg-cyan">Tyrion</td>
|
||||
<td class="fg-magenta">Lannister</td>
|
||||
<td align="right" class="fg-yellow" valign="bottom">5000</td>
|
||||
<td class="fg-black" valign="bottom"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right" class="bold">0</td>
|
||||
<td class="fg-cyan">Winter</td>
|
||||
<td class="fg-magenta">Is</td>
|
||||
<td align="right" class="fg-yellow" valign="bottom">0</td>
|
||||
<td class="fg-black" valign="bottom">Coming.<br/>The North Remembers!<br/>This is known.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td align="right"> </td>
|
||||
<td> </td>
|
||||
<td class="bg-white fg-black">Total</td>
|
||||
<td align="right" class="bg-white fg-black">10000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<caption class="caption" style="caption-side: bottom;">A Song of Ice and Fire</caption>
|
||||
</table>`)
|
||||
}
|
||||
|
||||
func TestTable_RenderHTML_CustomStyle(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRow(Row{1, "Arya", "Stark", 3000, "<a href=\"https://duckduckgo.com/?q=arya+stark+not+today\">Not today.</a>"})
|
||||
tw.AppendRow(Row{1, "Jon", "Snow", 2000, "You know\nnothing,\nJon Snow!"})
|
||||
tw.AppendRow(Row{300, "Tyrion", "Lannister", 5000})
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SetAutoIndex(true)
|
||||
tw.Style().HTML = HTMLOptions{
|
||||
CSSClass: "game-of-thrones",
|
||||
EmptyColumn: "<!-- test --> ",
|
||||
EscapeText: false,
|
||||
Newline: "<!-- newline -->",
|
||||
}
|
||||
tw.SetOutputMirror(nil)
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="game-of-thrones">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><!-- test --> </th>
|
||||
<th align="right">#</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
<th><!-- test --> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">1</td>
|
||||
<td align="right">1</td>
|
||||
<td>Arya</td>
|
||||
<td>Stark</td>
|
||||
<td align="right">3000</td>
|
||||
<td><a href="https://duckduckgo.com/?q=arya+stark+not+today">Not today.</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">2</td>
|
||||
<td align="right">1</td>
|
||||
<td>Jon</td>
|
||||
<td>Snow</td>
|
||||
<td align="right">2000</td>
|
||||
<td>You know<!-- newline -->nothing,<!-- newline -->Jon Snow!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">3</td>
|
||||
<td align="right">300</td>
|
||||
<td>Tyrion</td>
|
||||
<td>Lannister</td>
|
||||
<td align="right">5000</td>
|
||||
<td><!-- test --> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><!-- test --> </td>
|
||||
<td align="right"><!-- test --> </td>
|
||||
<td><!-- test --> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
<td><!-- test --> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`)
|
||||
}
|
||||
|
||||
func TestTable_RenderHTML_Empty(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
assert.Empty(t, tw.RenderHTML())
|
||||
}
|
||||
|
||||
func TestTable_RenderHTML_HiddenColumns(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendFooter(testFooter)
|
||||
|
||||
// ensure sorting is done before hiding the columns
|
||||
tw.SortBy([]SortBy{
|
||||
{Name: "Salary", Mode: DscNumeric},
|
||||
})
|
||||
|
||||
t.Run("every column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), "")
|
||||
})
|
||||
|
||||
t.Run("first column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>>>Tyrion</td>
|
||||
<td>Lannister<<</td>
|
||||
<td align="right">5013</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>>Arya</td>
|
||||
<td>Stark<<</td>
|
||||
<td align="right">3013</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>>>Jon</td>
|
||||
<td>Snow<<</td>
|
||||
<td align="right">2013</td>
|
||||
<td>~You know nothing, Jon Snow!~</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`)
|
||||
})
|
||||
|
||||
t.Run("column hidden in the middle", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="right">#</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">307</td>
|
||||
<td>Lannister<<</td>
|
||||
<td align="right">5013</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">8</td>
|
||||
<td>Stark<<</td>
|
||||
<td align="right">3013</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">27</td>
|
||||
<td>Snow<<</td>
|
||||
<td align="right">2013</td>
|
||||
<td>~You know nothing, Jon Snow!~</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td align="right"> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`)
|
||||
})
|
||||
|
||||
t.Run("last column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="right">#</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">307</td>
|
||||
<td>>>Tyrion</td>
|
||||
<td>Lannister<<</td>
|
||||
<td align="right">5013</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">8</td>
|
||||
<td>>>Arya</td>
|
||||
<td>Stark<<</td>
|
||||
<td align="right">3013</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">27</td>
|
||||
<td>>>Jon</td>
|
||||
<td>Snow<<</td>
|
||||
<td align="right">2013</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td align="right"> </td>
|
||||
<td> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTable_RenderHTML_Sorted(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||
|
||||
compareOutput(t, tw.RenderHTML(), `
|
||||
<table class="go-pretty-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="right">#</th>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th align="right">Salary</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">300</td>
|
||||
<td>Tyrion</td>
|
||||
<td>Lannister</td>
|
||||
<td align="right">5000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">20</td>
|
||||
<td>Jon</td>
|
||||
<td>Snow</td>
|
||||
<td align="right">2000</td>
|
||||
<td>You know nothing, Jon Snow!</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">1</td>
|
||||
<td>Arya</td>
|
||||
<td>Stark</td>
|
||||
<td align="right">3000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">11</td>
|
||||
<td>Sansa</td>
|
||||
<td>Stark</td>
|
||||
<td align="right">6000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td align="right"> </td>
|
||||
<td> </td>
|
||||
<td>Total</td>
|
||||
<td align="right">10000</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>`)
|
||||
}
|
@ -1,294 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/text"
|
||||
)
|
||||
|
||||
func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr {
|
||||
// update t.numColumns if this row is the longest seen till now
|
||||
if len(row) > t.numColumns {
|
||||
// init the slice for the first time; and pad it the rest of the time
|
||||
if t.numColumns == 0 {
|
||||
t.columnIsNonNumeric = make([]bool, len(row))
|
||||
} else {
|
||||
t.columnIsNonNumeric = append(t.columnIsNonNumeric, make([]bool, len(row)-t.numColumns)...)
|
||||
}
|
||||
// update t.numColumns
|
||||
t.numColumns = len(row)
|
||||
}
|
||||
|
||||
// convert each column to string and figure out if it has non-numeric data
|
||||
rowOut := make(rowStr, len(row))
|
||||
for colIdx, col := range row {
|
||||
// if the column is not a number, keep track of it
|
||||
if !hint.isHeaderRow && !hint.isFooterRow && !t.columnIsNonNumeric[colIdx] && !isNumber(col) {
|
||||
t.columnIsNonNumeric[colIdx] = true
|
||||
}
|
||||
|
||||
rowOut[colIdx] = t.analyzeAndStringifyColumn(colIdx, col, hint)
|
||||
}
|
||||
return rowOut
|
||||
}
|
||||
|
||||
func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint renderHint) string {
|
||||
// convert to a string and store it in the row
|
||||
var colStr string
|
||||
if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil {
|
||||
colStr = transformer(col)
|
||||
} else if colStrVal, ok := col.(string); ok {
|
||||
colStr = colStrVal
|
||||
} else {
|
||||
colStr = fmt.Sprint(col)
|
||||
}
|
||||
if strings.Contains(colStr, "\t") {
|
||||
colStr = strings.Replace(colStr, "\t", " ", -1)
|
||||
}
|
||||
if strings.Contains(colStr, "\r") {
|
||||
colStr = strings.Replace(colStr, "\r", "", -1)
|
||||
}
|
||||
return fmt.Sprintf("%s%s", t.style.Format.Direction.Modifier(), colStr)
|
||||
}
|
||||
|
||||
func (t *Table) extractMaxColumnLengths(rows []rowStr, hint renderHint) {
|
||||
for rowIdx, row := range rows {
|
||||
hint.rowNumber = rowIdx + 1
|
||||
t.extractMaxColumnLengthsFromRow(row, t.getMergedColumnIndices(row, hint))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) extractMaxColumnLengthsFromRow(row rowStr, mci mergedColumnIndices) {
|
||||
for colIdx, colStr := range row {
|
||||
longestLineLen := text.LongestLineLen(colStr)
|
||||
maxColWidth := t.getColumnWidthMax(colIdx)
|
||||
if maxColWidth > 0 && maxColWidth < longestLineLen {
|
||||
longestLineLen = maxColWidth
|
||||
}
|
||||
mergedColumnsLength := mci.mergedLength(colIdx, t.maxColumnLengths)
|
||||
if longestLineLen > mergedColumnsLength {
|
||||
if mergedColumnsLength > 0 {
|
||||
t.extractMaxColumnLengthsFromRowForMergedColumns(colIdx, longestLineLen, mci)
|
||||
} else {
|
||||
t.maxColumnLengths[colIdx] = longestLineLen
|
||||
}
|
||||
} else if maxColWidth == 0 && longestLineLen > t.maxColumnLengths[colIdx] {
|
||||
t.maxColumnLengths[colIdx] = longestLineLen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) extractMaxColumnLengthsFromRowForMergedColumns(colIdx int, mergedColumnLength int, mci mergedColumnIndices) {
|
||||
numMergedColumns := mci.len(colIdx)
|
||||
mergedColumnLength -= (numMergedColumns - 1) * text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
|
||||
maxLengthSplitAcrossColumns := mergedColumnLength / numMergedColumns
|
||||
if maxLengthSplitAcrossColumns > t.maxColumnLengths[colIdx] {
|
||||
t.maxColumnLengths[colIdx] = maxLengthSplitAcrossColumns
|
||||
}
|
||||
for otherColIdx := range mci[colIdx] {
|
||||
if maxLengthSplitAcrossColumns > t.maxColumnLengths[otherColIdx] {
|
||||
t.maxColumnLengths[otherColIdx] = maxLengthSplitAcrossColumns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRender() {
|
||||
// pick a default style if none was set until now
|
||||
t.Style()
|
||||
|
||||
// reset rendering state
|
||||
t.reset()
|
||||
|
||||
// initialize the column configs and normalize them
|
||||
t.initForRenderColumnConfigs()
|
||||
|
||||
// initialize and stringify all the raw rows
|
||||
t.initForRenderRows()
|
||||
|
||||
// find the longest continuous line in each column
|
||||
t.initForRenderColumnLengths()
|
||||
|
||||
// generate a separator row and calculate maximum row length
|
||||
t.initForRenderRowSeparator()
|
||||
|
||||
// reset the counter for the number of lines rendered
|
||||
t.numLinesRendered = 0
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderColumnConfigs() {
|
||||
t.columnConfigMap = map[int]ColumnConfig{}
|
||||
for _, colCfg := range t.columnConfigs {
|
||||
// find the column number if none provided; this logic can work only if
|
||||
// a header row is present and has a column with the given name
|
||||
if colCfg.Number == 0 {
|
||||
for _, row := range t.rowsHeaderRaw {
|
||||
colCfg.Number = row.findColumnNumber(colCfg.Name)
|
||||
if colCfg.Number > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if colCfg.Number > 0 {
|
||||
t.columnConfigMap[colCfg.Number-1] = colCfg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderColumnLengths() {
|
||||
t.maxColumnLengths = make([]int, t.numColumns)
|
||||
t.extractMaxColumnLengths(t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
t.extractMaxColumnLengths(t.rows, renderHint{})
|
||||
t.extractMaxColumnLengths(t.rowsFooter, renderHint{isFooterRow: true})
|
||||
|
||||
// increase the column lengths if any are under the limits
|
||||
for colIdx := range t.maxColumnLengths {
|
||||
minWidth := t.getColumnWidthMin(colIdx)
|
||||
if minWidth > 0 && t.maxColumnLengths[colIdx] < minWidth {
|
||||
t.maxColumnLengths[colIdx] = minWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderHideColumns() {
|
||||
if !t.hasHiddenColumns() {
|
||||
return
|
||||
}
|
||||
colIdxMap := t.hideColumns()
|
||||
|
||||
// re-create columnIsNonNumeric with new column indices
|
||||
columnIsNonNumeric := make([]bool, t.numColumns)
|
||||
for oldColIdx, nonNumeric := range t.columnIsNonNumeric {
|
||||
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
|
||||
columnIsNonNumeric[newColIdx] = nonNumeric
|
||||
}
|
||||
}
|
||||
t.columnIsNonNumeric = columnIsNonNumeric
|
||||
|
||||
// re-create columnConfigMap with new column indices
|
||||
columnConfigMap := make(map[int]ColumnConfig)
|
||||
for oldColIdx, cc := range t.columnConfigMap {
|
||||
if newColIdx, ok := colIdxMap[oldColIdx]; ok {
|
||||
columnConfigMap[newColIdx] = cc
|
||||
}
|
||||
}
|
||||
t.columnConfigMap = columnConfigMap
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderRows() {
|
||||
// auto-index: calc the index column's max length
|
||||
t.autoIndexVIndexMaxLength = len(fmt.Sprint(len(t.rowsRaw)))
|
||||
|
||||
// stringify all the rows to make it easy to render
|
||||
if t.rowPainter != nil {
|
||||
t.rowsColors = make([]text.Colors, len(t.rowsRaw))
|
||||
}
|
||||
t.rows = t.initForRenderRowsStringify(t.rowsRaw, renderHint{})
|
||||
t.rowsFooter = t.initForRenderRowsStringify(t.rowsFooterRaw, renderHint{isFooterRow: true})
|
||||
t.rowsHeader = t.initForRenderRowsStringify(t.rowsHeaderRaw, renderHint{isHeaderRow: true})
|
||||
|
||||
// sort the rows as requested
|
||||
t.initForRenderSortRows()
|
||||
|
||||
// suppress columns without any content
|
||||
t.initForRenderSuppressColumns()
|
||||
|
||||
// strip out hidden columns
|
||||
t.initForRenderHideColumns()
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderRowsStringify(rows []Row, hint renderHint) []rowStr {
|
||||
rowsStr := make([]rowStr, len(rows))
|
||||
for idx, row := range rows {
|
||||
if t.rowPainter != nil && hint.isRegularRow() {
|
||||
t.rowsColors[idx] = t.rowPainter(row)
|
||||
}
|
||||
rowsStr[idx] = t.analyzeAndStringify(row, hint)
|
||||
}
|
||||
return rowsStr
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderRowSeparator() {
|
||||
t.maxRowLength = 0
|
||||
if t.autoIndex {
|
||||
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft)
|
||||
t.maxRowLength += len(fmt.Sprint(len(t.rows)))
|
||||
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight)
|
||||
if t.style.Options.SeparateColumns {
|
||||
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator)
|
||||
}
|
||||
}
|
||||
if t.style.Options.SeparateColumns {
|
||||
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.MiddleSeparator) * (t.numColumns - 1)
|
||||
}
|
||||
t.rowSeparator = make(rowStr, t.numColumns)
|
||||
for colIdx, maxColumnLength := range t.maxColumnLengths {
|
||||
maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingLeft + t.style.Box.PaddingRight)
|
||||
t.maxRowLength += maxColumnLength
|
||||
t.rowSeparator[colIdx] = text.RepeatAndTrim(t.style.Box.MiddleHorizontal, maxColumnLength)
|
||||
}
|
||||
if t.style.Options.DrawBorder {
|
||||
t.maxRowLength += text.RuneWidthWithoutEscSequences(t.style.Box.Left + t.style.Box.Right)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderSortRows() {
|
||||
if len(t.sortBy) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// sort the rows
|
||||
sortedRowIndices := t.getSortedRowIndices()
|
||||
sortedRows := make([]rowStr, len(t.rows))
|
||||
for idx := range t.rows {
|
||||
sortedRows[idx] = t.rows[sortedRowIndices[idx]]
|
||||
}
|
||||
t.rows = sortedRows
|
||||
|
||||
// sort the rowsColors
|
||||
if len(t.rowsColors) > 0 {
|
||||
sortedRowsColors := make([]text.Colors, len(t.rows))
|
||||
for idx := range t.rows {
|
||||
sortedRowsColors[idx] = t.rowsColors[sortedRowIndices[idx]]
|
||||
}
|
||||
t.rowsColors = sortedRowsColors
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) initForRenderSuppressColumns() {
|
||||
shouldSuppressColumn := func(colIdx int) bool {
|
||||
for _, row := range t.rows {
|
||||
if colIdx < len(row) && row[colIdx] != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if t.suppressEmptyColumns {
|
||||
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||
if shouldSuppressColumn(colIdx) {
|
||||
cc := t.columnConfigMap[colIdx]
|
||||
cc.Hidden = true
|
||||
t.columnConfigMap[colIdx] = cc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset initializes all the variables used to maintain rendering information
|
||||
// that are written to in this file
|
||||
func (t *Table) reset() {
|
||||
t.autoIndexVIndexMaxLength = 0
|
||||
t.columnConfigMap = nil
|
||||
t.columnIsNonNumeric = nil
|
||||
t.maxColumnLengths = nil
|
||||
t.maxRowLength = 0
|
||||
t.numColumns = 0
|
||||
t.numLinesRendered = 0
|
||||
t.rowSeparator = nil
|
||||
t.rows = nil
|
||||
t.rowsColors = nil
|
||||
t.rowsFooter = nil
|
||||
t.rowsHeader = nil
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RenderMarkdown renders the Table in Markdown format. Example:
|
||||
// | # | First Name | Last Name | Salary | |
|
||||
// | ---:| --- | --- | ---:| --- |
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// | | | Total | 10000 | |
|
||||
func (t *Table) RenderMarkdown() string {
|
||||
t.initForRender()
|
||||
|
||||
var out strings.Builder
|
||||
if t.numColumns > 0 {
|
||||
t.markdownRenderTitle(&out)
|
||||
t.markdownRenderRowsHeader(&out)
|
||||
t.markdownRenderRows(&out, t.rows, renderHint{})
|
||||
t.markdownRenderRowsFooter(&out)
|
||||
t.markdownRenderCaption(&out)
|
||||
}
|
||||
return t.render(&out)
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderCaption(out *strings.Builder) {
|
||||
if t.caption != "" {
|
||||
out.WriteRune('\n')
|
||||
out.WriteRune('_')
|
||||
out.WriteString(t.caption)
|
||||
out.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
|
||||
// when working on line number 2 or more, insert a newline first
|
||||
if out.Len() > 0 {
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
|
||||
// render each column up to the max. columns seen in all the rows
|
||||
out.WriteRune('|')
|
||||
for colIdx := 0; colIdx < t.numColumns; colIdx++ {
|
||||
t.markdownRenderRowAutoIndex(out, colIdx, hint)
|
||||
|
||||
if hint.isSeparatorRow {
|
||||
out.WriteString(t.getAlign(colIdx, hint).MarkdownProperty())
|
||||
} else {
|
||||
var colStr string
|
||||
if colIdx < len(row) {
|
||||
colStr = row[colIdx]
|
||||
}
|
||||
out.WriteRune(' ')
|
||||
if strings.Contains(colStr, "|") {
|
||||
colStr = strings.Replace(colStr, "|", "\\|", -1)
|
||||
}
|
||||
if strings.Contains(colStr, "\n") {
|
||||
colStr = strings.Replace(colStr, "\n", "<br/>", -1)
|
||||
}
|
||||
out.WriteString(colStr)
|
||||
out.WriteRune(' ')
|
||||
}
|
||||
out.WriteRune('|')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRowAutoIndex(out *strings.Builder, colIdx int, hint renderHint) {
|
||||
if colIdx == 0 && t.autoIndex {
|
||||
out.WriteRune(' ')
|
||||
if hint.isSeparatorRow {
|
||||
out.WriteString("---:")
|
||||
} else if hint.isRegularRow() {
|
||||
out.WriteString(fmt.Sprintf("%d ", hint.rowNumber))
|
||||
}
|
||||
out.WriteRune('|')
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
|
||||
if len(rows) > 0 {
|
||||
for idx, row := range rows {
|
||||
hint.rowNumber = idx + 1
|
||||
t.markdownRenderRow(out, row, hint)
|
||||
|
||||
if idx == len(rows)-1 && hint.isHeaderRow {
|
||||
t.markdownRenderRow(out, t.rowSeparator, renderHint{isSeparatorRow: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRowsFooter(out *strings.Builder) {
|
||||
t.markdownRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
|
||||
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderRowsHeader(out *strings.Builder) {
|
||||
if len(t.rowsHeader) > 0 {
|
||||
t.markdownRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
|
||||
} else if t.autoIndex {
|
||||
t.markdownRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, renderHint{isAutoIndexRow: true, isHeaderRow: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) markdownRenderTitle(out *strings.Builder) {
|
||||
if t.title != "" {
|
||||
out.WriteString("# ")
|
||||
out.WriteString(t.title)
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_RenderMarkdown(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(testRowNewLines)
|
||||
tw.AppendRow(testRowPipes)
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SetCaption(testCaption)
|
||||
tw.SetTitle(testTitle1)
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), `
|
||||
# Game of Thrones
|
||||
| # | First Name | Last Name | Salary | |
|
||||
| ---:| --- | --- | ---:| --- |
|
||||
| 1 | Arya | Stark | 3000 | |
|
||||
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
| 300 | Tyrion | Lannister | 5000 | |
|
||||
| 0 | Valar | Morghulis | 0 | Faceless<br/>Men |
|
||||
| 0 | Valar | Morghulis | 0 | Faceless\|Men |
|
||||
| | | Total | 10000 | |
|
||||
_A Song of Ice and Fire_`)
|
||||
}
|
||||
|
||||
func TestTable_RenderMarkdown_AutoIndex(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
for rowIdx := 0; rowIdx < 10; rowIdx++ {
|
||||
row := make(Row, 10)
|
||||
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||
row[colIdx] = fmt.Sprintf("%s%d", AutoIndexColumnID(colIdx), rowIdx+1)
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
for rowIdx := 0; rowIdx < 1; rowIdx++ {
|
||||
row := make(Row, 10)
|
||||
for colIdx := 0; colIdx < 10; colIdx++ {
|
||||
row[colIdx] = AutoIndexColumnID(colIdx) + "F"
|
||||
}
|
||||
tw.AppendFooter(row)
|
||||
}
|
||||
tw.SetAutoIndex(true)
|
||||
tw.SetStyle(StyleLight)
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), `
|
||||
| | A | B | C | D | E | F | G | H | I | J |
|
||||
| ---:| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | A1 | B1 | C1 | D1 | E1 | F1 | G1 | H1 | I1 | J1 |
|
||||
| 2 | A2 | B2 | C2 | D2 | E2 | F2 | G2 | H2 | I2 | J2 |
|
||||
| 3 | A3 | B3 | C3 | D3 | E3 | F3 | G3 | H3 | I3 | J3 |
|
||||
| 4 | A4 | B4 | C4 | D4 | E4 | F4 | G4 | H4 | I4 | J4 |
|
||||
| 5 | A5 | B5 | C5 | D5 | E5 | F5 | G5 | H5 | I5 | J5 |
|
||||
| 6 | A6 | B6 | C6 | D6 | E6 | F6 | G6 | H6 | I6 | J6 |
|
||||
| 7 | A7 | B7 | C7 | D7 | E7 | F7 | G7 | H7 | I7 | J7 |
|
||||
| 8 | A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8 | I8 | J8 |
|
||||
| 9 | A9 | B9 | C9 | D9 | E9 | F9 | G9 | H9 | I9 | J9 |
|
||||
| 10 | A10 | B10 | C10 | D10 | E10 | F10 | G10 | H10 | I10 | J10 |
|
||||
| | AF | BF | CF | DF | EF | FF | GF | HF | IF | JF |`)
|
||||
}
|
||||
|
||||
func TestTable_RenderMarkdown_Empty(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
assert.Empty(t, tw.RenderMarkdown())
|
||||
}
|
||||
|
||||
func TestTable_RenderMarkdown_HiddenColumns(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendFooter(testFooter)
|
||||
|
||||
// ensure sorting is done before hiding the columns
|
||||
tw.SortBy([]SortBy{
|
||||
{Name: "Salary", Mode: DscNumeric},
|
||||
})
|
||||
|
||||
t.Run("every column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0, 1, 2, 3, 4}))
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), "")
|
||||
})
|
||||
|
||||
t.Run("first column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{0}))
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), `
|
||||
| First Name | Last Name | Salary | |
|
||||
| --- | --- | ---:| --- |
|
||||
| >>Tyrion | Lannister<< | 5013 | |
|
||||
| >>Arya | Stark<< | 3013 | |
|
||||
| >>Jon | Snow<< | 2013 | ~You know nothing, Jon Snow!~ |
|
||||
| | Total | 10000 | |`)
|
||||
})
|
||||
|
||||
t.Run("column hidden in the middle", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{1}))
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), `
|
||||
| # | Last Name | Salary | |
|
||||
| ---:| --- | ---:| --- |
|
||||
| 307 | Lannister<< | 5013 | |
|
||||
| 8 | Stark<< | 3013 | |
|
||||
| 27 | Snow<< | 2013 | ~You know nothing, Jon Snow!~ |
|
||||
| | Total | 10000 | |`)
|
||||
})
|
||||
|
||||
t.Run("last column hidden", func(t *testing.T) {
|
||||
tw.SetColumnConfigs(generateColumnConfigsWithHiddenColumns([]int{4}))
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), `
|
||||
| # | First Name | Last Name | Salary |
|
||||
| ---:| --- | --- | ---:|
|
||||
| 307 | >>Tyrion | Lannister<< | 5013 |
|
||||
| 8 | >>Arya | Stark<< | 3013 |
|
||||
| 27 | >>Jon | Snow<< | 2013 |
|
||||
| | | Total | 10000 |`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTable_RendeMarkdown_Sorted(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
tw.AppendHeader(testHeader)
|
||||
tw.AppendRows(testRows)
|
||||
tw.AppendRow(Row{11, "Sansa", "Stark", 6000})
|
||||
tw.AppendFooter(testFooter)
|
||||
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||
|
||||
compareOutput(t, tw.RenderMarkdown(), `
|
||||
| # | First Name | Last Name | Salary | |
|
||||
| ---:| --- | --- | ---:| --- |
|
||||
| 300 | Tyrion | Lannister | 5000 | |
|
||||
| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
| 1 | Arya | Stark | 3000 | |
|
||||
| 11 | Sansa | Stark | 6000 | |
|
||||
| | | Total | 10000 | |`)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,127 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SortBy defines What to sort (Column Name or Number), and How to sort (Mode).
|
||||
type SortBy struct {
|
||||
// Name is the name of the Column as it appears in the first Header row.
|
||||
// If a Header is not provided, or the name is not found in the header, this
|
||||
// will not work.
|
||||
Name string
|
||||
// Number is the Column # from left. When specified, it overrides the Name
|
||||
// property. If you know the exact Column number, use this instead of Name.
|
||||
Number int
|
||||
|
||||
// Mode tells the Writer how to Sort. Asc/Dsc/etc.
|
||||
Mode SortMode
|
||||
}
|
||||
|
||||
// SortMode defines How to sort.
|
||||
type SortMode int
|
||||
|
||||
const (
|
||||
// Asc sorts the column in Ascending order alphabetically.
|
||||
Asc SortMode = iota
|
||||
// AscNumeric sorts the column in Ascending order numerically.
|
||||
AscNumeric
|
||||
// Dsc sorts the column in Descending order alphabetically.
|
||||
Dsc
|
||||
// DscNumeric sorts the column in Descending order numerically.
|
||||
DscNumeric
|
||||
)
|
||||
|
||||
type rowsSorter struct {
|
||||
rows []rowStr
|
||||
sortBy []SortBy
|
||||
sortedIndices []int
|
||||
}
|
||||
|
||||
// getSortedRowIndices sorts and returns the row indices in Sorted order as
|
||||
// directed by Table.sortBy which can be set using Table.SortBy(...)
|
||||
func (t *Table) getSortedRowIndices() []int {
|
||||
sortedIndices := make([]int, len(t.rows))
|
||||
for idx := range t.rows {
|
||||
sortedIndices[idx] = idx
|
||||
}
|
||||
|
||||
if t.sortBy != nil && len(t.sortBy) > 0 {
|
||||
sort.Sort(rowsSorter{
|
||||
rows: t.rows,
|
||||
sortBy: t.parseSortBy(t.sortBy),
|
||||
sortedIndices: sortedIndices,
|
||||
})
|
||||
}
|
||||
|
||||
return sortedIndices
|
||||
}
|
||||
|
||||
func (t *Table) parseSortBy(sortBy []SortBy) []SortBy {
|
||||
var resSortBy []SortBy
|
||||
for _, col := range sortBy {
|
||||
colNum := 0
|
||||
if col.Number > 0 && col.Number <= t.numColumns {
|
||||
colNum = col.Number
|
||||
} else if col.Name != "" && len(t.rowsHeader) > 0 {
|
||||
for idx, colName := range t.rowsHeader[0] {
|
||||
if col.Name == colName {
|
||||
colNum = idx + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if colNum > 0 {
|
||||
resSortBy = append(resSortBy, SortBy{
|
||||
Name: col.Name,
|
||||
Number: colNum,
|
||||
Mode: col.Mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
return resSortBy
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Len() int {
|
||||
return len(rs.rows)
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Swap(i, j int) {
|
||||
rs.sortedIndices[i], rs.sortedIndices[j] = rs.sortedIndices[j], rs.sortedIndices[i]
|
||||
}
|
||||
|
||||
func (rs rowsSorter) Less(i, j int) bool {
|
||||
realI, realJ := rs.sortedIndices[i], rs.sortedIndices[j]
|
||||
for _, col := range rs.sortBy {
|
||||
rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1
|
||||
if colIdx < len(rowI) && colIdx < len(rowJ) {
|
||||
shouldContinue, returnValue := rs.lessColumns(rowI, rowJ, colIdx, col)
|
||||
if !shouldContinue {
|
||||
return returnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rs rowsSorter) lessColumns(rowI rowStr, rowJ rowStr, colIdx int, col SortBy) (bool, bool) {
|
||||
if rowI[colIdx] == rowJ[colIdx] {
|
||||
return true, false
|
||||
} else if col.Mode == Asc {
|
||||
return false, rowI[colIdx] < rowJ[colIdx]
|
||||
} else if col.Mode == Dsc {
|
||||
return false, rowI[colIdx] > rowJ[colIdx]
|
||||
}
|
||||
|
||||
iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64)
|
||||
jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64)
|
||||
if iErr == nil && jErr == nil {
|
||||
if col.Mode == AscNumeric {
|
||||
return false, iVal < jVal
|
||||
} else if col.Mode == DscNumeric {
|
||||
return false, jVal < iVal
|
||||
}
|
||||
}
|
||||
return true, false
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTable_sortRows_WithName(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"})
|
||||
table.AppendRows([]Row{
|
||||
{1, "Arya", "Stark", 3000},
|
||||
{11, "Sansa", "Stark", 3000},
|
||||
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
{300, "Tyrion", "Lannister", 5000},
|
||||
})
|
||||
table.SetStyle(StyleDefault)
|
||||
table.initForRenderRows()
|
||||
|
||||
// sort by nothing
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
// sort by "#"
|
||||
table.SortBy([]SortBy{{Name: "#", Mode: AscNumeric}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "#", Mode: DscNumeric}})
|
||||
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||
|
||||
// sort by First Name, Last Name
|
||||
table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Asc}})
|
||||
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "First Name", Mode: Asc}, {Name: "Last Name", Mode: Dsc}})
|
||||
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}})
|
||||
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Dsc}})
|
||||
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||
|
||||
// sort by Last Name, First Name
|
||||
table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Asc}})
|
||||
assert.Equal(t, []int{3, 2, 0, 1}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "Last Name", Mode: Asc}, {Name: "First Name", Mode: Dsc}})
|
||||
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Asc}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "First Name", Mode: Dsc}})
|
||||
assert.Equal(t, []int{1, 0, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
// sort by Unknown Column
|
||||
table.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Foo Bar", Mode: Dsc}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
// sort by Salary
|
||||
table.SortBy([]SortBy{{Name: "Salary", Mode: AscNumeric}})
|
||||
assert.Equal(t, []int{2, 0, 1, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Name: "Salary", Mode: DscNumeric}})
|
||||
assert.Equal(t, []int{3, 0, 1, 2}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy(nil)
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
}
|
||||
|
||||
func TestTable_sortRows_WithoutName(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRows([]Row{
|
||||
{1, "Arya", "Stark", 3000},
|
||||
{11, "Sansa", "Stark", 3000},
|
||||
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
{300, "Tyrion", "Lannister", 5000},
|
||||
})
|
||||
table.SetStyle(StyleDefault)
|
||||
table.initForRenderRows()
|
||||
|
||||
// sort by nothing
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
// sort by "#"
|
||||
table.SortBy([]SortBy{{Number: 1, Mode: AscNumeric}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 1, Mode: DscNumeric}})
|
||||
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||
|
||||
// sort by First Name, Last Name
|
||||
table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Asc}})
|
||||
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 2, Mode: Asc}, {Number: 3, Mode: Dsc}})
|
||||
assert.Equal(t, []int{0, 2, 1, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Asc}})
|
||||
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 2, Mode: Dsc}, {Number: 3, Mode: Dsc}})
|
||||
assert.Equal(t, []int{3, 1, 2, 0}, table.getSortedRowIndices())
|
||||
|
||||
// sort by Last Name, First Name
|
||||
table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Asc}})
|
||||
assert.Equal(t, []int{3, 2, 0, 1}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 3, Mode: Asc}, {Number: 2, Mode: Dsc}})
|
||||
assert.Equal(t, []int{3, 2, 1, 0}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Asc}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 2, Mode: Dsc}})
|
||||
assert.Equal(t, []int{1, 0, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
// sort by Unknown Column
|
||||
table.SortBy([]SortBy{{Number: 3, Mode: Dsc}, {Number: 99, Mode: Dsc}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
|
||||
// sort by Salary
|
||||
table.SortBy([]SortBy{{Number: 4, Mode: AscNumeric}})
|
||||
assert.Equal(t, []int{2, 0, 1, 3}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy([]SortBy{{Number: 4, Mode: DscNumeric}})
|
||||
assert.Equal(t, []int{3, 0, 1, 2}, table.getSortedRowIndices())
|
||||
|
||||
table.SortBy(nil)
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
}
|
||||
|
||||
func TestTable_sortRows_InvalidMode(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRows([]Row{
|
||||
{1, "Arya", "Stark", 3000},
|
||||
{11, "Sansa", "Stark", 3000},
|
||||
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
{300, "Tyrion", "Lannister", 5000},
|
||||
})
|
||||
table.SetStyle(StyleDefault)
|
||||
table.initForRenderRows()
|
||||
|
||||
// sort by "First Name"
|
||||
table.SortBy([]SortBy{{Number: 2, Mode: AscNumeric}})
|
||||
assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices())
|
||||
}
|
@ -1,879 +0,0 @@
|
||||
package table
|
||||
|
||||
import "git.hexq.cn/tiglog/golib/helper/text"
|
||||
|
||||
// Style declares how to render the Table and provides very fine-grained control
|
||||
// on how the Table gets rendered on the Console.
|
||||
type Style struct {
|
||||
Name string // name of the Style
|
||||
Box BoxStyle // characters to use for the boxes
|
||||
Color ColorOptions // colors to use for the rows and columns
|
||||
Format FormatOptions // formatting options for the rows and columns
|
||||
HTML HTMLOptions // rendering options for HTML mode
|
||||
Options Options // misc. options for the table
|
||||
Title TitleOptions // formation options for the title text
|
||||
}
|
||||
|
||||
var (
|
||||
// StyleDefault renders a Table like below:
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | | | TOTAL | 10000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
StyleDefault = Style{
|
||||
Name: "StyleDefault",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleBold renders a Table like below:
|
||||
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
|
||||
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
|
||||
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
|
||||
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
StyleBold = Style{
|
||||
Name: "StyleBold",
|
||||
Box: StyleBoxBold,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleColoredBright renders a Table without any borders or separators,
|
||||
// and with Black text on Cyan background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBright = Style{
|
||||
Name: "StyleColoredBright",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBright,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsDark,
|
||||
}
|
||||
|
||||
// StyleColoredDark renders a Table without any borders or separators, and
|
||||
// with Header/Footer in Cyan text and other rows with White text, all on
|
||||
// Black background.
|
||||
StyleColoredDark = Style{
|
||||
Name: "StyleColoredDark",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsDark,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBright,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnBlueWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Blue background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnBlueWhite = Style{
|
||||
Name: "StyleColoredBlackOnBlueWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnBlueWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlueOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnCyanWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Cyan background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnCyanWhite = Style{
|
||||
Name: "StyleColoredBlackOnCyanWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnCyanWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsCyanOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnGreenWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Green background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnGreenWhite = Style{
|
||||
Name: "StyleColoredBlackOnGreenWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnGreenWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsGreenOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnMagentaWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Magenta background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnMagentaWhite = Style{
|
||||
Name: "StyleColoredBlackOnMagentaWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnMagentaWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsMagentaOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnYellowWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Yellow background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnYellowWhite = Style{
|
||||
Name: "StyleColoredBlackOnYellowWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnYellowWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsYellowOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlackOnRedWhite renders a Table without any borders or
|
||||
// separators, and with Black text on Red background for Header/Footer and
|
||||
// White background for other rows.
|
||||
StyleColoredBlackOnRedWhite = Style{
|
||||
Name: "StyleColoredBlackOnRedWhite",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlackOnRedWhite,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsRedOnBlack,
|
||||
}
|
||||
|
||||
// StyleColoredBlueWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Blue text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredBlueWhiteOnBlack = Style{
|
||||
Name: "StyleColoredBlueWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsBlueWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnBlue,
|
||||
}
|
||||
|
||||
// StyleColoredCyanWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Cyan text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredCyanWhiteOnBlack = Style{
|
||||
Name: "StyleColoredCyanWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsCyanWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnCyan,
|
||||
}
|
||||
|
||||
// StyleColoredGreenWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Green text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredGreenWhiteOnBlack = Style{
|
||||
Name: "StyleColoredGreenWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsGreenWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnGreen,
|
||||
}
|
||||
|
||||
// StyleColoredMagentaWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Magenta text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredMagentaWhiteOnBlack = Style{
|
||||
Name: "StyleColoredMagentaWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsMagentaWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnMagenta,
|
||||
}
|
||||
|
||||
// StyleColoredRedWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Red text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredRedWhiteOnBlack = Style{
|
||||
Name: "StyleColoredRedWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsRedWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnRed,
|
||||
}
|
||||
|
||||
// StyleColoredYellowWhiteOnBlack renders a Table without any borders or
|
||||
// separators, and with Header/Footer in Yellow text and other rows with
|
||||
// White text, all on Black background.
|
||||
StyleColoredYellowWhiteOnBlack = Style{
|
||||
Name: "StyleColoredYellowWhiteOnBlack",
|
||||
Box: StyleBoxDefault,
|
||||
Color: ColorOptionsYellowWhiteOnBlack,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsNoBordersAndSeparators,
|
||||
Title: TitleOptionsBlackOnYellow,
|
||||
}
|
||||
|
||||
// StyleDouble renders a Table like below:
|
||||
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
|
||||
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
|
||||
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
|
||||
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ ║ ║ TOTAL ║ 10000 ║ ║
|
||||
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
|
||||
StyleDouble = Style{
|
||||
Name: "StyleDouble",
|
||||
Box: StyleBoxDouble,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleLight renders a Table like below:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
StyleLight = Style{
|
||||
Name: "StyleLight",
|
||||
Box: StyleBoxLight,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// StyleRounded renders a Table like below:
|
||||
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
|
||||
StyleRounded = Style{
|
||||
Name: "StyleRounded",
|
||||
Box: StyleBoxRounded,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
|
||||
// styleTest renders a Table like below:
|
||||
// (-----^------------^-----------^--------^-----------------------------)
|
||||
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< >|< >|<TOTAL >|< 10000>|< >]
|
||||
// \-----v------------v-----------v--------v-----------------------------/
|
||||
styleTest = Style{
|
||||
Name: "styleTest",
|
||||
Box: styleBoxTest,
|
||||
Color: ColorOptionsDefault,
|
||||
Format: FormatOptionsDefault,
|
||||
HTML: DefaultHTMLOptions,
|
||||
Options: OptionsDefault,
|
||||
Title: TitleOptionsDefault,
|
||||
}
|
||||
)
|
||||
|
||||
// BoxStyle defines the characters/strings to use to render the borders and
|
||||
// separators for the Table.
|
||||
type BoxStyle struct {
|
||||
BottomLeft string
|
||||
BottomRight string
|
||||
BottomSeparator string
|
||||
EmptySeparator string
|
||||
Left string
|
||||
LeftSeparator string
|
||||
MiddleHorizontal string
|
||||
MiddleSeparator string
|
||||
MiddleVertical string
|
||||
PaddingLeft string
|
||||
PaddingRight string
|
||||
PageSeparator string
|
||||
Right string
|
||||
RightSeparator string
|
||||
TopLeft string
|
||||
TopRight string
|
||||
TopSeparator string
|
||||
UnfinishedRow string
|
||||
}
|
||||
|
||||
var (
|
||||
// StyleBoxDefault defines a Boxed-Table like below:
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | | | TOTAL | 10000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
StyleBoxDefault = BoxStyle{
|
||||
BottomLeft: "+",
|
||||
BottomRight: "+",
|
||||
BottomSeparator: "+",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")),
|
||||
Left: "|",
|
||||
LeftSeparator: "+",
|
||||
MiddleHorizontal: "-",
|
||||
MiddleSeparator: "+",
|
||||
MiddleVertical: "|",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "|",
|
||||
RightSeparator: "+",
|
||||
TopLeft: "+",
|
||||
TopRight: "+",
|
||||
TopSeparator: "+",
|
||||
UnfinishedRow: " ~",
|
||||
}
|
||||
|
||||
// StyleBoxBold defines a Boxed-Table like below:
|
||||
// ┏━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ # ┃ FIRST NAME ┃ LAST NAME ┃ SALARY ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 1 ┃ Arya ┃ Stark ┃ 3000 ┃ ┃
|
||||
// ┃ 20 ┃ Jon ┃ Snow ┃ 2000 ┃ You know nothing, Jon Snow! ┃
|
||||
// ┃ 300 ┃ Tyrion ┃ Lannister ┃ 5000 ┃ ┃
|
||||
// ┣━━━━━╋━━━━━━━━━━━━╋━━━━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ ┃ ┃ TOTAL ┃ 10000 ┃ ┃
|
||||
// ┗━━━━━┻━━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
StyleBoxBold = BoxStyle{
|
||||
BottomLeft: "┗",
|
||||
BottomRight: "┛",
|
||||
BottomSeparator: "┻",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("╋")),
|
||||
Left: "┃",
|
||||
LeftSeparator: "┣",
|
||||
MiddleHorizontal: "━",
|
||||
MiddleSeparator: "╋",
|
||||
MiddleVertical: "┃",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "┃",
|
||||
RightSeparator: "┫",
|
||||
TopLeft: "┏",
|
||||
TopRight: "┓",
|
||||
TopSeparator: "┳",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// StyleBoxDouble defines a Boxed-Table like below:
|
||||
// ╔═════╦════════════╦═══════════╦════════╦═════════════════════════════╗
|
||||
// ║ # ║ FIRST NAME ║ LAST NAME ║ SALARY ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ 1 ║ Arya ║ Stark ║ 3000 ║ ║
|
||||
// ║ 20 ║ Jon ║ Snow ║ 2000 ║ You know nothing, Jon Snow! ║
|
||||
// ║ 300 ║ Tyrion ║ Lannister ║ 5000 ║ ║
|
||||
// ╠═════╬════════════╬═══════════╬════════╬═════════════════════════════╣
|
||||
// ║ ║ ║ TOTAL ║ 10000 ║ ║
|
||||
// ╚═════╩════════════╩═══════════╩════════╩═════════════════════════════╝
|
||||
StyleBoxDouble = BoxStyle{
|
||||
BottomLeft: "╚",
|
||||
BottomRight: "╝",
|
||||
BottomSeparator: "╩",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("╬")),
|
||||
Left: "║",
|
||||
LeftSeparator: "╠",
|
||||
MiddleHorizontal: "═",
|
||||
MiddleSeparator: "╬",
|
||||
MiddleVertical: "║",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "║",
|
||||
RightSeparator: "╣",
|
||||
TopLeft: "╔",
|
||||
TopRight: "╗",
|
||||
TopSeparator: "╦",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// StyleBoxLight defines a Boxed-Table like below:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
StyleBoxLight = BoxStyle{
|
||||
BottomLeft: "└",
|
||||
BottomRight: "┘",
|
||||
BottomSeparator: "┴",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")),
|
||||
Left: "│",
|
||||
LeftSeparator: "├",
|
||||
MiddleHorizontal: "─",
|
||||
MiddleSeparator: "┼",
|
||||
MiddleVertical: "│",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "│",
|
||||
RightSeparator: "┤",
|
||||
TopLeft: "┌",
|
||||
TopRight: "┐",
|
||||
TopSeparator: "┬",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// StyleBoxRounded defines a Boxed-Table like below:
|
||||
// ╭─────┬────────────┬───────────┬────────┬─────────────────────────────╮
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// ╰─────┴────────────┴───────────┴────────┴─────────────────────────────╯
|
||||
StyleBoxRounded = BoxStyle{
|
||||
BottomLeft: "╰",
|
||||
BottomRight: "╯",
|
||||
BottomSeparator: "┴",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")),
|
||||
Left: "│",
|
||||
LeftSeparator: "├",
|
||||
MiddleHorizontal: "─",
|
||||
MiddleSeparator: "┼",
|
||||
MiddleVertical: "│",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: " ",
|
||||
PageSeparator: "\n",
|
||||
Right: "│",
|
||||
RightSeparator: "┤",
|
||||
TopLeft: "╭",
|
||||
TopRight: "╮",
|
||||
TopSeparator: "┬",
|
||||
UnfinishedRow: " ≈",
|
||||
}
|
||||
|
||||
// styleBoxTest defines a Boxed-Table like below:
|
||||
// (-----^------------^-----------^--------^-----------------------------)
|
||||
// [< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||
// [< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||
// [<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||
// {-----+------------+-----------+--------+-----------------------------}
|
||||
// [< >|< >|<TOTAL >|< 10000>|< >]
|
||||
// \-----v------------v-----------v--------v-----------------------------/
|
||||
styleBoxTest = BoxStyle{
|
||||
BottomLeft: "\\",
|
||||
BottomRight: "/",
|
||||
BottomSeparator: "v",
|
||||
EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")),
|
||||
Left: "[",
|
||||
LeftSeparator: "{",
|
||||
MiddleHorizontal: "--",
|
||||
MiddleSeparator: "+",
|
||||
MiddleVertical: "|",
|
||||
PaddingLeft: "<",
|
||||
PaddingRight: ">",
|
||||
PageSeparator: "\n",
|
||||
Right: "]",
|
||||
RightSeparator: "}",
|
||||
TopLeft: "(",
|
||||
TopRight: ")",
|
||||
TopSeparator: "^",
|
||||
UnfinishedRow: " ~~~",
|
||||
}
|
||||
)
|
||||
|
||||
// ColorOptions defines the ANSI colors to use for parts of the Table.
|
||||
type ColorOptions struct {
|
||||
Border text.Colors // borders (if nil, uses one of the below)
|
||||
Footer text.Colors // footer row(s) colors
|
||||
Header text.Colors // header row(s) colors
|
||||
IndexColumn text.Colors // index-column colors (row #, etc.)
|
||||
Row text.Colors // regular row(s) colors
|
||||
RowAlternate text.Colors // regular row(s) colors for the even-numbered rows
|
||||
Separator text.Colors // separators (if nil, uses one of the above)
|
||||
}
|
||||
|
||||
var (
|
||||
// ColorOptionsDefault defines sensible ANSI color options - basically NONE.
|
||||
ColorOptionsDefault = ColorOptions{}
|
||||
|
||||
// ColorOptionsBright renders dark text on bright background.
|
||||
ColorOptionsBright = ColorOptionsBlackOnCyanWhite
|
||||
|
||||
// ColorOptionsDark renders bright text on dark background.
|
||||
ColorOptionsDark = ColorOptionsCyanWhiteOnBlack
|
||||
|
||||
// ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background.
|
||||
ColorOptionsBlackOnBlueWhite = ColorOptions{
|
||||
Footer: text.Colors{text.BgBlue, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiBlue, text.FgBlack},
|
||||
IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background.
|
||||
ColorOptionsBlackOnCyanWhite = ColorOptions{
|
||||
Footer: text.Colors{text.BgCyan, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||
IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnGreenWhite renders Black text on Green/White
|
||||
// background.
|
||||
ColorOptionsBlackOnGreenWhite = ColorOptions{
|
||||
Footer: text.Colors{text.BgGreen, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiGreen, text.FgBlack},
|
||||
IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White
|
||||
// background.
|
||||
ColorOptionsBlackOnMagentaWhite = ColorOptions{
|
||||
Footer: text.Colors{text.BgMagenta, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiMagenta, text.FgBlack},
|
||||
IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnRedWhite renders Black text on Red/White background.
|
||||
ColorOptionsBlackOnRedWhite = ColorOptions{
|
||||
Footer: text.Colors{text.BgRed, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiRed, text.FgBlack},
|
||||
IndexColumn: text.Colors{text.BgHiRed, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White
|
||||
// background.
|
||||
ColorOptionsBlackOnYellowWhite = ColorOptions{
|
||||
Footer: text.Colors{text.BgYellow, text.FgBlack},
|
||||
Header: text.Colors{text.BgHiYellow, text.FgBlack},
|
||||
IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack},
|
||||
Row: text.Colors{text.BgHiWhite, text.FgBlack},
|
||||
RowAlternate: text.Colors{text.BgWhite, text.FgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background.
|
||||
ColorOptionsBlueWhiteOnBlack = ColorOptions{
|
||||
Footer: text.Colors{text.FgBlue, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiBlue, text.BgHiBlack},
|
||||
IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background.
|
||||
ColorOptionsCyanWhiteOnBlack = ColorOptions{
|
||||
Footer: text.Colors{text.FgCyan, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiCyan, text.BgHiBlack},
|
||||
IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsGreenWhiteOnBlack renders Green/White text on Black
|
||||
// background.
|
||||
ColorOptionsGreenWhiteOnBlack = ColorOptions{
|
||||
Footer: text.Colors{text.FgGreen, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiGreen, text.BgHiBlack},
|
||||
IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black
|
||||
// background.
|
||||
ColorOptionsMagentaWhiteOnBlack = ColorOptions{
|
||||
Footer: text.Colors{text.FgMagenta, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiMagenta, text.BgHiBlack},
|
||||
IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsRedWhiteOnBlack renders Red/White text on Black background.
|
||||
ColorOptionsRedWhiteOnBlack = ColorOptions{
|
||||
Footer: text.Colors{text.FgRed, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiRed, text.BgHiBlack},
|
||||
IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
|
||||
// ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black
|
||||
// background.
|
||||
ColorOptionsYellowWhiteOnBlack = ColorOptions{
|
||||
Footer: text.Colors{text.FgYellow, text.BgHiBlack},
|
||||
Header: text.Colors{text.FgHiYellow, text.BgHiBlack},
|
||||
IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack},
|
||||
Row: text.Colors{text.FgHiWhite, text.BgBlack},
|
||||
RowAlternate: text.Colors{text.FgWhite, text.BgBlack},
|
||||
}
|
||||
)
|
||||
|
||||
// FormatOptions defines the text-formatting to perform on parts of the Table.
|
||||
type FormatOptions struct {
|
||||
Direction text.Direction // (forced) BiDi direction for each Column
|
||||
Footer text.Format // footer row(s) text format
|
||||
Header text.Format // header row(s) text format
|
||||
Row text.Format // (data) row(s) text format
|
||||
}
|
||||
|
||||
var (
|
||||
// FormatOptionsDefault defines sensible formatting options.
|
||||
FormatOptionsDefault = FormatOptions{
|
||||
Footer: text.FormatUpper,
|
||||
Header: text.FormatUpper,
|
||||
Row: text.FormatDefault,
|
||||
}
|
||||
)
|
||||
|
||||
// HTMLOptions defines the global options to control HTML rendering.
|
||||
type HTMLOptions struct {
|
||||
CSSClass string // CSS class to set on the overall <table> tag
|
||||
EmptyColumn string // string to replace "" columns with (entire content being "")
|
||||
EscapeText bool // escape text into HTML-safe content?
|
||||
Newline string // string to replace "\n" characters with
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultHTMLOptions defines sensible HTML rendering defaults.
|
||||
DefaultHTMLOptions = HTMLOptions{
|
||||
CSSClass: DefaultHTMLCSSClass,
|
||||
EmptyColumn: " ",
|
||||
EscapeText: true,
|
||||
Newline: "<br/>",
|
||||
}
|
||||
)
|
||||
|
||||
// Options defines the global options that determine how the Table is
|
||||
// rendered.
|
||||
type Options struct {
|
||||
// DoNotColorBordersAndSeparators disables coloring all the borders and row
|
||||
// or column separators.
|
||||
DoNotColorBordersAndSeparators bool
|
||||
|
||||
// DrawBorder enables or disables drawing the border around the Table.
|
||||
// Example of a table where it is disabled:
|
||||
// # │ FIRST NAME │ LAST NAME │ SALARY │
|
||||
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
|
||||
// 1 │ Arya │ Stark │ 3000 │
|
||||
// 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow!
|
||||
// 300 │ Tyrion │ Lannister │ 5000 │
|
||||
// ─────┼────────────┼───────────┼────────┼─────────────────────────────
|
||||
// │ │ TOTAL │ 10000 │
|
||||
DrawBorder bool
|
||||
|
||||
// SeparateColumns enables or disable drawing border between columns.
|
||||
// Example of a table where it is disabled:
|
||||
// ┌─────────────────────────────────────────────────────────────────┐
|
||||
// │ # FIRST NAME LAST NAME SALARY │
|
||||
// ├─────────────────────────────────────────────────────────────────┤
|
||||
// │ 1 Arya Stark 3000 │
|
||||
// │ 20 Jon Snow 2000 You know nothing, Jon Snow! │
|
||||
// │ 300 Tyrion Lannister 5000 │
|
||||
// │ TOTAL 10000 │
|
||||
// └─────────────────────────────────────────────────────────────────┘
|
||||
SeparateColumns bool
|
||||
|
||||
// SeparateFooter enables or disable drawing border between the footer and
|
||||
// the rows. Example of a table where it is disabled:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
SeparateFooter bool
|
||||
|
||||
// SeparateHeader enables or disable drawing border between the header and
|
||||
// the rows. Example of a table where it is disabled:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
SeparateHeader bool
|
||||
|
||||
// SeparateRows enables or disables drawing separators between each row.
|
||||
// Example of a table where it is enabled:
|
||||
// ┌─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
// │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
// ├─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
// │ │ │ TOTAL │ 10000 │ │
|
||||
// └─────┴────────────┴───────────┴────────┴─────────────────────────────┘
|
||||
SeparateRows bool
|
||||
}
|
||||
|
||||
var (
|
||||
// OptionsDefault defines sensible global options.
|
||||
OptionsDefault = Options{
|
||||
DrawBorder: true,
|
||||
SeparateColumns: true,
|
||||
SeparateFooter: true,
|
||||
SeparateHeader: true,
|
||||
SeparateRows: false,
|
||||
}
|
||||
|
||||
// OptionsNoBorders sets up a table without any borders.
|
||||
OptionsNoBorders = Options{
|
||||
DrawBorder: false,
|
||||
SeparateColumns: true,
|
||||
SeparateFooter: true,
|
||||
SeparateHeader: true,
|
||||
SeparateRows: false,
|
||||
}
|
||||
|
||||
// OptionsNoBordersAndSeparators sets up a table without any borders or
|
||||
// separators.
|
||||
OptionsNoBordersAndSeparators = Options{
|
||||
DrawBorder: false,
|
||||
SeparateColumns: false,
|
||||
SeparateFooter: false,
|
||||
SeparateHeader: false,
|
||||
SeparateRows: false,
|
||||
}
|
||||
)
|
||||
|
||||
// TitleOptions defines the way the title text is to be rendered.
|
||||
type TitleOptions struct {
|
||||
Align text.Align
|
||||
Colors text.Colors
|
||||
Format text.Format
|
||||
}
|
||||
|
||||
var (
|
||||
// TitleOptionsDefault defines sensible title options - basically NONE.
|
||||
TitleOptionsDefault = TitleOptions{}
|
||||
|
||||
// TitleOptionsBright renders Bright Bold text on Dark background.
|
||||
TitleOptionsBright = TitleOptionsBlackOnCyan
|
||||
|
||||
// TitleOptionsDark renders Dark Bold text on Bright background.
|
||||
TitleOptionsDark = TitleOptionsCyanOnBlack
|
||||
|
||||
// TitleOptionsBlackOnBlue renders Black text on Blue background.
|
||||
TitleOptionsBlackOnBlue = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnBlueWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnCyan renders Black Bold text on Cyan background.
|
||||
TitleOptionsBlackOnCyan = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnCyanWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnGreen renders Black Bold text onGreen background.
|
||||
TitleOptionsBlackOnGreen = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnGreenWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnMagenta renders Black Bold text on Magenta background.
|
||||
TitleOptionsBlackOnMagenta = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnMagentaWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnRed renders Black Bold text on Red background.
|
||||
TitleOptionsBlackOnRed = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnRedWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlackOnYellow renders Black Bold text on Yellow background.
|
||||
TitleOptionsBlackOnYellow = TitleOptions{
|
||||
Colors: append(ColorOptionsBlackOnYellowWhite.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsBlueOnBlack renders Blue Bold text on Black background.
|
||||
TitleOptionsBlueOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsBlueWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsCyanOnBlack renders Cyan Bold text on Black background.
|
||||
TitleOptionsCyanOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsCyanWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsGreenOnBlack renders Green Bold text on Black background.
|
||||
TitleOptionsGreenOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsGreenWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsMagentaOnBlack renders Magenta Bold text on Black background.
|
||||
TitleOptionsMagentaOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsMagentaWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsRedOnBlack renders Red Bold text on Black background.
|
||||
TitleOptionsRedOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsRedWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
|
||||
// TitleOptionsYellowOnBlack renders Yellow Bold text on Black background.
|
||||
TitleOptionsYellowOnBlack = TitleOptions{
|
||||
Colors: append(ColorOptionsYellowWhiteOnBlack.Header, text.Bold),
|
||||
}
|
||||
)
|
@ -1,784 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/text"
|
||||
)
|
||||
|
||||
// Row defines a single row in the Table.
|
||||
type Row []interface{}
|
||||
|
||||
func (r Row) findColumnNumber(colName string) int {
|
||||
for colIdx, col := range r {
|
||||
if fmt.Sprint(col) == colName {
|
||||
return colIdx + 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// RowPainter is a custom function that takes a Row as input and returns the
|
||||
// text.Colors{} to use on the entire row
|
||||
type RowPainter func(row Row) text.Colors
|
||||
|
||||
// rowStr defines a single row in the Table comprised of just string objects.
|
||||
type rowStr []string
|
||||
|
||||
// areEqual returns true if the contents of the 2 given columns are the same
|
||||
func (row rowStr) areEqual(colIdx1 int, colIdx2 int) bool {
|
||||
return colIdx1 >= 0 && colIdx2 < len(row) && row[colIdx1] == row[colIdx2]
|
||||
}
|
||||
|
||||
// Table helps print a 2-dimensional array in a human readable pretty-table.
|
||||
type Table struct {
|
||||
// allowedRowLength is the max allowed length for a row (or line of output)
|
||||
allowedRowLength int
|
||||
// enable automatic indexing of the rows and columns like a spreadsheet?
|
||||
autoIndex bool
|
||||
// autoIndexVIndexMaxLength denotes the length in chars for the last rownum
|
||||
autoIndexVIndexMaxLength int
|
||||
// caption stores the text to be rendered just below the table; and doesn't
|
||||
// get used when rendered as a CSV
|
||||
caption string
|
||||
// columnIsNonNumeric stores if a column contains non-numbers in all rows
|
||||
columnIsNonNumeric []bool
|
||||
// columnConfigs stores the custom-configuration for 1 or more columns
|
||||
columnConfigs []ColumnConfig
|
||||
// columnConfigMap stores the custom-configuration by column
|
||||
// number and is generated before rendering
|
||||
columnConfigMap map[int]ColumnConfig
|
||||
// htmlCSSClass stores the HTML CSS Class to use on the <table> node
|
||||
htmlCSSClass string
|
||||
// indexColumn stores the number of the column considered as the "index"
|
||||
indexColumn int
|
||||
// maxColumnLengths stores the length of the longest line in each column
|
||||
maxColumnLengths []int
|
||||
// maxRowLength stores the length of the longest row
|
||||
maxRowLength int
|
||||
// numColumns stores the (max.) number of columns seen
|
||||
numColumns int
|
||||
// numLinesRendered keeps track of the number of lines rendered and helps in
|
||||
// paginating long tables
|
||||
numLinesRendered int
|
||||
// outputMirror stores an io.Writer where the "Render" functions would write
|
||||
outputMirror io.Writer
|
||||
// pageSize stores the maximum lines to render before rendering the header
|
||||
// again (to denote a page break) - useful when you are dealing with really
|
||||
// long tables
|
||||
pageSize int
|
||||
// rows stores the rows that make up the body (in string form)
|
||||
rows []rowStr
|
||||
// rowsColors stores the text.Colors over-rides for each row as defined by
|
||||
// rowPainter
|
||||
rowsColors []text.Colors
|
||||
// rowsConfigs stores RowConfig for each row
|
||||
rowsConfigMap map[int]RowConfig
|
||||
// rowsRaw stores the rows that make up the body
|
||||
rowsRaw []Row
|
||||
// rowsFooter stores the rows that make up the footer (in string form)
|
||||
rowsFooter []rowStr
|
||||
// rowsFooterConfigs stores RowConfig for each footer row
|
||||
rowsFooterConfigMap map[int]RowConfig
|
||||
// rowsFooterRaw stores the rows that make up the footer
|
||||
rowsFooterRaw []Row
|
||||
// rowsHeader stores the rows that make up the header (in string form)
|
||||
rowsHeader []rowStr
|
||||
// rowsHeaderConfigs stores RowConfig for each header row
|
||||
rowsHeaderConfigMap map[int]RowConfig
|
||||
// rowsHeaderRaw stores the rows that make up the header
|
||||
rowsHeaderRaw []Row
|
||||
// rowPainter is a custom function that given a Row, returns the colors to
|
||||
// use on the entire row
|
||||
rowPainter RowPainter
|
||||
// rowSeparator is a dummy row that contains the separator columns (dashes
|
||||
// that make up the separator between header/body/footer
|
||||
rowSeparator rowStr
|
||||
// separators is used to keep track of all rowIndices after which a
|
||||
// separator has to be rendered
|
||||
separators map[int]bool
|
||||
// sortBy stores a map of Column
|
||||
sortBy []SortBy
|
||||
// style contains all the strings used to draw the table, and more
|
||||
style *Style
|
||||
// suppressEmptyColumns hides columns which have no content on all regular
|
||||
// rows
|
||||
suppressEmptyColumns bool
|
||||
// title contains the text to appear above the table
|
||||
title string
|
||||
}
|
||||
|
||||
// AppendFooter appends the row to the List of footers to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against this row.
|
||||
func (t *Table) AppendFooter(row Row, config ...RowConfig) {
|
||||
t.rowsFooterRaw = append(t.rowsFooterRaw, row)
|
||||
if len(config) > 0 {
|
||||
if t.rowsFooterConfigMap == nil {
|
||||
t.rowsFooterConfigMap = make(map[int]RowConfig)
|
||||
}
|
||||
t.rowsFooterConfigMap[len(t.rowsFooterRaw)-1] = config[0]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendHeader appends the row to the List of headers to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against this row.
|
||||
func (t *Table) AppendHeader(row Row, config ...RowConfig) {
|
||||
t.rowsHeaderRaw = append(t.rowsHeaderRaw, row)
|
||||
if len(config) > 0 {
|
||||
if t.rowsHeaderConfigMap == nil {
|
||||
t.rowsHeaderConfigMap = make(map[int]RowConfig)
|
||||
}
|
||||
t.rowsHeaderConfigMap[len(t.rowsHeaderRaw)-1] = config[0]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendRow appends the row to the List of rows to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against this row.
|
||||
func (t *Table) AppendRow(row Row, config ...RowConfig) {
|
||||
t.rowsRaw = append(t.rowsRaw, row)
|
||||
if len(config) > 0 {
|
||||
if t.rowsConfigMap == nil {
|
||||
t.rowsConfigMap = make(map[int]RowConfig)
|
||||
}
|
||||
t.rowsConfigMap[len(t.rowsRaw)-1] = config[0]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendRows appends the rows to the List of rows to render.
|
||||
//
|
||||
// Only the first item in the "config" will be tagged against all the rows.
|
||||
func (t *Table) AppendRows(rows []Row, config ...RowConfig) {
|
||||
for _, row := range rows {
|
||||
t.AppendRow(row, config...)
|
||||
}
|
||||
}
|
||||
|
||||
// AppendSeparator helps render a separator row after the current last row. You
|
||||
// could call this function over and over, but it will be a no-op unless you
|
||||
// call AppendRow or AppendRows in between. Likewise, if the last thing you
|
||||
// append is a separator, it will not be rendered in addition to the usual table
|
||||
// separator.
|
||||
//
|
||||
// ******************************************************************************
|
||||
// Please note the following caveats:
|
||||
// 1. SetPageSize(): this may end up creating consecutive separator rows near
|
||||
// the end of a page or at the beginning of a page
|
||||
// 2. SortBy(): since SortBy could inherently alter the ordering of rows, the
|
||||
// separators may not appear after the row it was originally intended to
|
||||
// follow
|
||||
//
|
||||
// ******************************************************************************
|
||||
func (t *Table) AppendSeparator() {
|
||||
if t.separators == nil {
|
||||
t.separators = make(map[int]bool)
|
||||
}
|
||||
if len(t.rowsRaw) > 0 {
|
||||
t.separators[len(t.rowsRaw)-1] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Length returns the number of rows to be rendered.
|
||||
func (t *Table) Length() int {
|
||||
return len(t.rowsRaw)
|
||||
}
|
||||
|
||||
// ResetFooters resets and clears all the Footer rows appended earlier.
|
||||
func (t *Table) ResetFooters() {
|
||||
t.rowsFooterRaw = nil
|
||||
}
|
||||
|
||||
// ResetHeaders resets and clears all the Header rows appended earlier.
|
||||
func (t *Table) ResetHeaders() {
|
||||
t.rowsHeaderRaw = nil
|
||||
}
|
||||
|
||||
// ResetRows resets and clears all the rows appended earlier.
|
||||
func (t *Table) ResetRows() {
|
||||
t.rowsRaw = nil
|
||||
t.separators = nil
|
||||
}
|
||||
|
||||
// SetAllowedRowLength sets the maximum allowed length or a row (or line of
|
||||
// output) when rendered as a table. Rows that are longer than this limit will
|
||||
// be "snipped" to the length. Length has to be a positive value to take effect.
|
||||
func (t *Table) SetAllowedRowLength(length int) {
|
||||
t.allowedRowLength = length
|
||||
}
|
||||
|
||||
// SetAutoIndex adds a generated header with columns such as "A", "B", "C", etc.
|
||||
// and a leading column with the row number similar to what you'd see on any
|
||||
// spreadsheet application. NOTE: Appending a Header will void this
|
||||
// functionality.
|
||||
func (t *Table) SetAutoIndex(autoIndex bool) {
|
||||
t.autoIndex = autoIndex
|
||||
}
|
||||
|
||||
// SetCaption sets the text to be rendered just below the table. This will not
|
||||
// show up when the Table is rendered as a CSV.
|
||||
func (t *Table) SetCaption(format string, a ...interface{}) {
|
||||
t.caption = fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// SetColumnConfigs sets the configs for each Column.
|
||||
func (t *Table) SetColumnConfigs(configs []ColumnConfig) {
|
||||
t.columnConfigs = configs
|
||||
}
|
||||
|
||||
// SetHTMLCSSClass sets the the HTML CSS Class to use on the <table> node
|
||||
// when rendering the Table in HTML format.
|
||||
//
|
||||
// Deprecated: in favor of Style().HTML.CSSClass
|
||||
func (t *Table) SetHTMLCSSClass(cssClass string) {
|
||||
t.htmlCSSClass = cssClass
|
||||
}
|
||||
|
||||
// SetIndexColumn sets the given Column # as the column that has the row
|
||||
// "Number". Valid values range from 1 to N. Note that this is not 0-indexed.
|
||||
func (t *Table) SetIndexColumn(colNum int) {
|
||||
t.indexColumn = colNum
|
||||
}
|
||||
|
||||
// SetOutputMirror sets an io.Writer for all the Render functions to "Write" to
|
||||
// in addition to returning a string.
|
||||
func (t *Table) SetOutputMirror(mirror io.Writer) {
|
||||
t.outputMirror = mirror
|
||||
}
|
||||
|
||||
// SetPageSize sets the maximum number of lines to render before rendering the
|
||||
// header rows again. This can be useful when dealing with tables containing a
|
||||
// long list of rows that can span pages. Please note that the pagination logic
|
||||
// will not consider Header/Footer lines for paging.
|
||||
func (t *Table) SetPageSize(numLines int) {
|
||||
t.pageSize = numLines
|
||||
}
|
||||
|
||||
// SetRowPainter sets the RowPainter function which determines the colors to use
|
||||
// on a row. Before rendering, this function is invoked on all rows and the
|
||||
// color of each row is determined. This color takes precedence over other ways
|
||||
// to set color (ColumnConfig.Color*, SetColor*()).
|
||||
func (t *Table) SetRowPainter(painter RowPainter) {
|
||||
t.rowPainter = painter
|
||||
}
|
||||
|
||||
// SetStyle overrides the DefaultStyle with the provided one.
|
||||
func (t *Table) SetStyle(style Style) {
|
||||
t.style = &style
|
||||
}
|
||||
|
||||
// SetTitle sets the title text to be rendered above the table.
|
||||
func (t *Table) SetTitle(format string, a ...interface{}) {
|
||||
t.title = fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// SortBy sets the rules for sorting the Rows in the order specified. i.e., the
|
||||
// first SortBy instruction takes precedence over the second and so on. Any
|
||||
// duplicate instructions on the same column will be discarded while sorting.
|
||||
func (t *Table) SortBy(sortBy []SortBy) {
|
||||
t.sortBy = sortBy
|
||||
}
|
||||
|
||||
// Style returns the current style.
|
||||
func (t *Table) Style() *Style {
|
||||
if t.style == nil {
|
||||
tempStyle := StyleDefault
|
||||
t.style = &tempStyle
|
||||
}
|
||||
return t.style
|
||||
}
|
||||
|
||||
// SuppressEmptyColumns hides columns when the column is empty in ALL the
|
||||
// regular rows.
|
||||
func (t *Table) SuppressEmptyColumns() {
|
||||
t.suppressEmptyColumns = true
|
||||
}
|
||||
|
||||
func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
|
||||
align := text.AlignDefault
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isHeaderRow {
|
||||
align = cfg.AlignHeader
|
||||
} else if hint.isFooterRow {
|
||||
align = cfg.AlignFooter
|
||||
} else {
|
||||
align = cfg.Align
|
||||
}
|
||||
}
|
||||
if align == text.AlignDefault {
|
||||
if !t.columnIsNonNumeric[colIdx] {
|
||||
align = text.AlignRight
|
||||
} else if hint.isAutoIndexRow {
|
||||
align = text.AlignCenter
|
||||
}
|
||||
}
|
||||
return align
|
||||
}
|
||||
|
||||
func (t *Table) getAutoIndexColumnIDs() rowStr {
|
||||
row := make(rowStr, t.numColumns)
|
||||
for colIdx := range row {
|
||||
row[colIdx] = AutoIndexColumnID(colIdx)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func (t *Table) getBorderColors(hint renderHint) text.Colors {
|
||||
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||
return nil
|
||||
} else if t.style.Color.Border != nil {
|
||||
return t.style.Color.Border
|
||||
} else if hint.isTitleRow {
|
||||
return t.style.Title.Colors
|
||||
} else if hint.isHeaderRow {
|
||||
return t.style.Color.Header
|
||||
} else if hint.isFooterRow {
|
||||
return t.style.Color.Footer
|
||||
} else if t.autoIndex {
|
||||
return t.style.Color.IndexColumn
|
||||
} else if hint.rowNumber%2 == 0 && t.style.Color.RowAlternate != nil {
|
||||
return t.style.Color.RowAlternate
|
||||
}
|
||||
return t.style.Color.Row
|
||||
}
|
||||
|
||||
func (t *Table) getBorderLeft(hint renderHint) string {
|
||||
border := t.style.Box.Left
|
||||
if hint.isBorderTop {
|
||||
if t.title != "" {
|
||||
border = t.style.Box.LeftSeparator
|
||||
} else {
|
||||
border = t.style.Box.TopLeft
|
||||
}
|
||||
} else if hint.isBorderBottom {
|
||||
border = t.style.Box.BottomLeft
|
||||
} else if hint.isSeparatorRow {
|
||||
if t.autoIndex && hint.isHeaderOrFooterSeparator() {
|
||||
border = t.style.Box.Left
|
||||
} else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) {
|
||||
border = t.style.Box.Left
|
||||
} else {
|
||||
border = t.style.Box.LeftSeparator
|
||||
}
|
||||
}
|
||||
return border
|
||||
}
|
||||
|
||||
func (t *Table) getBorderRight(hint renderHint) string {
|
||||
border := t.style.Box.Right
|
||||
if hint.isBorderTop {
|
||||
if t.title != "" {
|
||||
border = t.style.Box.RightSeparator
|
||||
} else {
|
||||
border = t.style.Box.TopRight
|
||||
}
|
||||
} else if hint.isBorderBottom {
|
||||
border = t.style.Box.BottomRight
|
||||
} else if hint.isSeparatorRow {
|
||||
if t.shouldMergeCellsVertically(t.numColumns-1, hint) {
|
||||
border = t.style.Box.Right
|
||||
} else {
|
||||
border = t.style.Box.RightSeparator
|
||||
}
|
||||
}
|
||||
return border
|
||||
}
|
||||
|
||||
func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors {
|
||||
if hint.isBorderOrSeparator() {
|
||||
if colors := t.getColumnColorsForBorderOrSeparator(colIdx, hint); colors != nil {
|
||||
return colors
|
||||
}
|
||||
}
|
||||
if t.rowPainter != nil && hint.isRegularNonSeparatorRow() && !t.isIndexColumn(colIdx, hint) {
|
||||
if colors := t.rowsColors[hint.rowNumber-1]; colors != nil {
|
||||
return colors
|
||||
}
|
||||
}
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isSeparatorRow {
|
||||
return nil
|
||||
} else if hint.isHeaderRow {
|
||||
return cfg.ColorsHeader
|
||||
} else if hint.isFooterRow {
|
||||
return cfg.ColorsFooter
|
||||
}
|
||||
return cfg.Colors
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) getColumnColorsForBorderOrSeparator(colIdx int, hint renderHint) text.Colors {
|
||||
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||
return text.Colors{} // not nil to force caller to paint with no colors
|
||||
}
|
||||
if (hint.isBorderBottom || hint.isBorderTop) && t.style.Color.Border != nil {
|
||||
return t.style.Color.Border
|
||||
}
|
||||
if hint.isSeparatorRow && t.style.Color.Separator != nil {
|
||||
return t.style.Color.Separator
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) string {
|
||||
separator := t.style.Box.MiddleVertical
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isBorderTop {
|
||||
if t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint) {
|
||||
separator = t.style.Box.MiddleHorizontal
|
||||
} else {
|
||||
separator = t.style.Box.TopSeparator
|
||||
}
|
||||
} else if hint.isBorderBottom {
|
||||
if t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint) {
|
||||
separator = t.style.Box.MiddleHorizontal
|
||||
} else {
|
||||
separator = t.style.Box.BottomSeparator
|
||||
}
|
||||
} else {
|
||||
separator = t.getColumnSeparatorNonBorder(
|
||||
t.shouldMergeCellsHorizontallyAbove(row, colIdx, hint),
|
||||
t.shouldMergeCellsHorizontallyBelow(row, colIdx, hint),
|
||||
colIdx,
|
||||
hint,
|
||||
)
|
||||
}
|
||||
}
|
||||
return separator
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparatorNonBorder(mergeCellsAbove bool, mergeCellsBelow bool, colIdx int, hint renderHint) string {
|
||||
mergeNextCol := t.shouldMergeCellsVertically(colIdx, hint)
|
||||
if hint.isAutoIndexColumn {
|
||||
return t.getColumnSeparatorNonBorderAutoIndex(mergeNextCol, hint)
|
||||
}
|
||||
|
||||
mergeCurrCol := t.shouldMergeCellsVertically(colIdx-1, hint)
|
||||
return t.getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove, mergeCellsBelow, mergeCurrCol, mergeNextCol)
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparatorNonBorderAutoIndex(mergeNextCol bool, hint renderHint) string {
|
||||
if hint.isHeaderOrFooterSeparator() {
|
||||
if mergeNextCol {
|
||||
return t.style.Box.MiddleVertical
|
||||
}
|
||||
return t.style.Box.LeftSeparator
|
||||
} else if mergeNextCol {
|
||||
return t.style.Box.RightSeparator
|
||||
}
|
||||
return t.style.Box.MiddleSeparator
|
||||
}
|
||||
|
||||
func (t *Table) getColumnSeparatorNonBorderNonAutoIndex(mergeCellsAbove bool, mergeCellsBelow bool, mergeCurrCol bool, mergeNextCol bool) string {
|
||||
if mergeCellsAbove && mergeCellsBelow && mergeCurrCol && mergeNextCol {
|
||||
return t.style.Box.EmptySeparator
|
||||
} else if mergeCellsAbove && mergeCellsBelow {
|
||||
return t.style.Box.MiddleHorizontal
|
||||
} else if mergeCellsAbove {
|
||||
return t.style.Box.TopSeparator
|
||||
} else if mergeCellsBelow {
|
||||
return t.style.Box.BottomSeparator
|
||||
} else if mergeCurrCol && mergeNextCol {
|
||||
return t.style.Box.MiddleVertical
|
||||
} else if mergeCurrCol {
|
||||
return t.style.Box.LeftSeparator
|
||||
} else if mergeNextCol {
|
||||
return t.style.Box.RightSeparator
|
||||
}
|
||||
return t.style.Box.MiddleSeparator
|
||||
}
|
||||
|
||||
func (t *Table) getColumnTransformer(colIdx int, hint renderHint) text.Transformer {
|
||||
var transformer text.Transformer
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isHeaderRow {
|
||||
transformer = cfg.TransformerHeader
|
||||
} else if hint.isFooterRow {
|
||||
transformer = cfg.TransformerFooter
|
||||
} else {
|
||||
transformer = cfg.Transformer
|
||||
}
|
||||
}
|
||||
return transformer
|
||||
}
|
||||
|
||||
func (t *Table) getColumnWidthMax(colIdx int) int {
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
return cfg.WidthMax
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *Table) getColumnWidthMin(colIdx int) int {
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
return cfg.WidthMin
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *Table) getFormat(hint renderHint) text.Format {
|
||||
if hint.isSeparatorRow {
|
||||
return text.FormatDefault
|
||||
} else if hint.isHeaderRow {
|
||||
return t.style.Format.Header
|
||||
} else if hint.isFooterRow {
|
||||
return t.style.Format.Footer
|
||||
}
|
||||
return t.style.Format.Row
|
||||
}
|
||||
|
||||
func (t *Table) getMaxColumnLengthForMerging(colIdx int) int {
|
||||
maxColumnLength := t.maxColumnLengths[colIdx]
|
||||
maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.PaddingRight + t.style.Box.PaddingLeft)
|
||||
if t.style.Options.SeparateColumns {
|
||||
maxColumnLength += text.RuneWidthWithoutEscSequences(t.style.Box.EmptySeparator)
|
||||
}
|
||||
return maxColumnLength
|
||||
}
|
||||
|
||||
// getMergedColumnIndices returns a map of colIdx values to all the other colIdx
|
||||
// values (that are being merged) and their lengths.
|
||||
func (t *Table) getMergedColumnIndices(row rowStr, hint renderHint) mergedColumnIndices {
|
||||
if !t.getRowConfig(hint).AutoMerge {
|
||||
return nil
|
||||
}
|
||||
|
||||
mci := make(mergedColumnIndices)
|
||||
for colIdx := 0; colIdx < t.numColumns-1; colIdx++ {
|
||||
// look backward
|
||||
for otherColIdx := colIdx - 1; colIdx >= 0 && otherColIdx >= 0; otherColIdx-- {
|
||||
if row[colIdx] != row[otherColIdx] {
|
||||
break
|
||||
}
|
||||
mci.safeAppend(colIdx, otherColIdx)
|
||||
}
|
||||
// look forward
|
||||
for otherColIdx := colIdx + 1; colIdx < len(row) && otherColIdx < len(row); otherColIdx++ {
|
||||
if row[colIdx] != row[otherColIdx] {
|
||||
break
|
||||
}
|
||||
mci.safeAppend(colIdx, otherColIdx)
|
||||
}
|
||||
}
|
||||
return mci
|
||||
}
|
||||
|
||||
func (t *Table) getRow(rowIdx int, hint renderHint) rowStr {
|
||||
switch {
|
||||
case hint.isHeaderRow:
|
||||
if rowIdx >= 0 && rowIdx < len(t.rowsHeader) {
|
||||
return t.rowsHeader[rowIdx]
|
||||
}
|
||||
case hint.isFooterRow:
|
||||
if rowIdx >= 0 && rowIdx < len(t.rowsFooter) {
|
||||
return t.rowsFooter[rowIdx]
|
||||
}
|
||||
default:
|
||||
if rowIdx >= 0 && rowIdx < len(t.rows) {
|
||||
return t.rows[rowIdx]
|
||||
}
|
||||
}
|
||||
return rowStr{}
|
||||
}
|
||||
|
||||
func (t *Table) getRowConfig(hint renderHint) RowConfig {
|
||||
rowIdx := hint.rowNumber - 1
|
||||
if rowIdx < 0 {
|
||||
rowIdx = 0
|
||||
}
|
||||
|
||||
switch {
|
||||
case hint.isHeaderRow:
|
||||
return t.rowsHeaderConfigMap[rowIdx]
|
||||
case hint.isFooterRow:
|
||||
return t.rowsFooterConfigMap[rowIdx]
|
||||
default:
|
||||
return t.rowsConfigMap[rowIdx]
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) getSeparatorColors(hint renderHint) text.Colors {
|
||||
if t.style.Options.DoNotColorBordersAndSeparators {
|
||||
return nil
|
||||
} else if (hint.isBorderBottom || hint.isBorderTop) && t.style.Color.Border != nil {
|
||||
return t.style.Color.Border
|
||||
} else if t.style.Color.Separator != nil {
|
||||
return t.style.Color.Separator
|
||||
} else if hint.isHeaderRow {
|
||||
return t.style.Color.Header
|
||||
} else if hint.isFooterRow {
|
||||
return t.style.Color.Footer
|
||||
} else if hint.isAutoIndexColumn {
|
||||
return t.style.Color.IndexColumn
|
||||
} else if hint.rowNumber > 0 && hint.rowNumber%2 == 0 {
|
||||
return t.style.Color.RowAlternate
|
||||
}
|
||||
return t.style.Color.Row
|
||||
}
|
||||
|
||||
func (t *Table) getVAlign(colIdx int, hint renderHint) text.VAlign {
|
||||
vAlign := text.VAlignDefault
|
||||
if cfg, ok := t.columnConfigMap[colIdx]; ok {
|
||||
if hint.isHeaderRow {
|
||||
vAlign = cfg.VAlignHeader
|
||||
} else if hint.isFooterRow {
|
||||
vAlign = cfg.VAlignFooter
|
||||
} else {
|
||||
vAlign = cfg.VAlign
|
||||
}
|
||||
}
|
||||
return vAlign
|
||||
}
|
||||
|
||||
func (t *Table) hasHiddenColumns() bool {
|
||||
for _, cc := range t.columnConfigMap {
|
||||
if cc.Hidden {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) hideColumns() map[int]int {
|
||||
colIdxMap := make(map[int]int)
|
||||
numColumns := 0
|
||||
hideColumnsInRows := func(rows []rowStr) []rowStr {
|
||||
var rsp []rowStr
|
||||
for _, row := range rows {
|
||||
var rowNew rowStr
|
||||
for colIdx, col := range row {
|
||||
cc := t.columnConfigMap[colIdx]
|
||||
if !cc.Hidden {
|
||||
rowNew = append(rowNew, col)
|
||||
colIdxMap[colIdx] = len(rowNew) - 1
|
||||
}
|
||||
}
|
||||
if len(rowNew) > numColumns {
|
||||
numColumns = len(rowNew)
|
||||
}
|
||||
rsp = append(rsp, rowNew)
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
// hide columns as directed
|
||||
t.rows = hideColumnsInRows(t.rows)
|
||||
t.rowsFooter = hideColumnsInRows(t.rowsFooter)
|
||||
t.rowsHeader = hideColumnsInRows(t.rowsHeader)
|
||||
|
||||
// reset numColumns to the new number of columns
|
||||
t.numColumns = numColumns
|
||||
|
||||
return colIdxMap
|
||||
}
|
||||
|
||||
func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
|
||||
return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn
|
||||
}
|
||||
|
||||
func (t *Table) render(out *strings.Builder) string {
|
||||
outStr := out.String()
|
||||
if t.outputMirror != nil && len(outStr) > 0 {
|
||||
_, _ = t.outputMirror.Write([]byte(outStr))
|
||||
_, _ = t.outputMirror.Write([]byte("\n"))
|
||||
}
|
||||
return outStr
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsHorizontallyAbove(row rowStr, colIdx int, hint renderHint) bool {
|
||||
if hint.isAutoIndexColumn || hint.isAutoIndexRow {
|
||||
return false
|
||||
}
|
||||
|
||||
rowConfig := t.getRowConfig(hint)
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isHeaderRow && hint.rowNumber == 1 {
|
||||
rowConfig = t.getRowConfig(hint)
|
||||
row = t.getRow(hint.rowNumber-1, hint)
|
||||
} else if hint.isFooterRow && hint.isFirstRow {
|
||||
rowConfig = t.getRowConfig(renderHint{isLastRow: true, rowNumber: len(t.rows)})
|
||||
row = t.getRow(len(t.rows)-1, renderHint{})
|
||||
} else if hint.isFooterRow && hint.isBorderBottom {
|
||||
row = t.getRow(len(t.rowsFooter)-1, renderHint{isFooterRow: true})
|
||||
} else {
|
||||
row = t.getRow(hint.rowNumber-1, hint)
|
||||
}
|
||||
}
|
||||
|
||||
if rowConfig.AutoMerge {
|
||||
return row.areEqual(colIdx-1, colIdx)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsHorizontallyBelow(row rowStr, colIdx int, hint renderHint) bool {
|
||||
if hint.isAutoIndexColumn || hint.isAutoIndexRow {
|
||||
return false
|
||||
}
|
||||
|
||||
var rowConfig RowConfig
|
||||
if hint.isSeparatorRow {
|
||||
if hint.isHeaderRow && hint.rowNumber == 0 {
|
||||
rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: 1})
|
||||
row = t.getRow(0, hint)
|
||||
} else if hint.isHeaderRow && hint.isLastRow {
|
||||
rowConfig = t.getRowConfig(renderHint{rowNumber: 1})
|
||||
row = t.getRow(0, renderHint{})
|
||||
} else if hint.isHeaderRow {
|
||||
rowConfig = t.getRowConfig(renderHint{isHeaderRow: true, rowNumber: hint.rowNumber + 1})
|
||||
row = t.getRow(hint.rowNumber, hint)
|
||||
} else if hint.isFooterRow && hint.rowNumber >= 0 {
|
||||
rowConfig = t.getRowConfig(renderHint{isFooterRow: true, rowNumber: 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{isFooterRow: true})
|
||||
} else if hint.isRegularRow() {
|
||||
rowConfig = t.getRowConfig(renderHint{rowNumber: hint.rowNumber + 1})
|
||||
row = t.getRow(hint.rowNumber, renderHint{})
|
||||
}
|
||||
}
|
||||
|
||||
if rowConfig.AutoMerge {
|
||||
return row.areEqual(colIdx-1, colIdx)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool {
|
||||
if t.columnConfigMap[colIdx].AutoMerge && colIdx < t.numColumns {
|
||||
if hint.isSeparatorRow {
|
||||
rowPrev := t.getRow(hint.rowNumber-1, hint)
|
||||
rowNext := t.getRow(hint.rowNumber, hint)
|
||||
if colIdx < len(rowPrev) && colIdx < len(rowNext) {
|
||||
return rowPrev[colIdx] == rowNext[colIdx] || "" == rowNext[colIdx]
|
||||
}
|
||||
} else {
|
||||
rowPrev := t.getRow(hint.rowNumber-2, hint)
|
||||
rowCurr := t.getRow(hint.rowNumber-1, hint)
|
||||
if colIdx < len(rowPrev) && colIdx < len(rowCurr) {
|
||||
return rowPrev[colIdx] == rowCurr[colIdx] || "" == rowCurr[colIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Table) wrapRow(row rowStr) (int, rowStr) {
|
||||
colMaxLines := 0
|
||||
rowWrapped := make(rowStr, len(row))
|
||||
for colIdx, colStr := range row {
|
||||
widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer()
|
||||
maxWidth := t.getColumnWidthMax(colIdx)
|
||||
if maxWidth == 0 {
|
||||
maxWidth = t.maxColumnLengths[colIdx]
|
||||
}
|
||||
rowWrapped[colIdx] = widthEnforcer(colStr, maxWidth)
|
||||
colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1
|
||||
if colNumLines > colMaxLines {
|
||||
colMaxLines = colNumLines
|
||||
}
|
||||
}
|
||||
return colMaxLines, rowWrapped
|
||||
}
|
@ -1,375 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/text"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testCaption = "A Song of Ice and Fire"
|
||||
testColor = text.Colors{text.FgGreen}
|
||||
testColorHiRedBold = text.Colors{text.FgHiRed, text.Bold}
|
||||
testColorHiBlueBold = text.Colors{text.FgHiBlue, text.Bold}
|
||||
testCSSClass = "test-css-class"
|
||||
testFooter = Row{"", "", "Total", 10000}
|
||||
testFooterMultiLine = Row{"", "", "Total\nSalary", 10000}
|
||||
testHeader = Row{"#", "First Name", "Last Name", "Salary"}
|
||||
testHeaderMultiLine = Row{"#", "First\nName", "Last\nName", "Salary"}
|
||||
testRows = []Row{
|
||||
{1, "Arya", "Stark", 3000},
|
||||
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
{300, "Tyrion", "Lannister", 5000},
|
||||
}
|
||||
testRowMultiLine = Row{0, "Winter", "Is", 0, "Coming.\r\nThe North Remembers!\nThis is known."}
|
||||
testRowNewLines = Row{0, "Valar", "Morghulis", 0, "Faceless\nMen"}
|
||||
testRowPipes = Row{0, "Valar", "Morghulis", 0, "Faceless|Men"}
|
||||
testRowTabs = Row{0, "Valar", "Morghulis", 0, "Faceless\tMen"}
|
||||
testTitle1 = "Game of Thrones"
|
||||
testTitle2 = "When you play the Game of Thrones, you win or you die. There is no middle ground."
|
||||
)
|
||||
|
||||
func init() {
|
||||
text.EnableColors()
|
||||
}
|
||||
|
||||
type myMockOutputMirror struct {
|
||||
mirroredOutput string
|
||||
}
|
||||
|
||||
func (t *myMockOutputMirror) Write(p []byte) (n int, err error) {
|
||||
t.mirroredOutput += string(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestNewWriter(t *testing.T) {
|
||||
tw := NewWriter()
|
||||
assert.NotNil(t, tw.Style())
|
||||
assert.Equal(t, StyleDefault, *tw.Style())
|
||||
|
||||
tw.SetStyle(StyleBold)
|
||||
assert.NotNil(t, tw.Style())
|
||||
assert.Equal(t, StyleBold, *tw.Style())
|
||||
}
|
||||
|
||||
func TestTable_AppendFooter(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||
|
||||
table.AppendFooter([]interface{}{})
|
||||
assert.Equal(t, 0, table.Length())
|
||||
assert.Equal(t, 1, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||
|
||||
table.AppendFooter([]interface{}{})
|
||||
assert.Equal(t, 0, table.Length())
|
||||
assert.Equal(t, 2, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||
|
||||
table.AppendFooter([]interface{}{}, RowConfig{AutoMerge: true})
|
||||
assert.Equal(t, 0, table.Length())
|
||||
assert.Equal(t, 3, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||
assert.False(t, table.rowsFooterConfigMap[0].AutoMerge)
|
||||
assert.False(t, table.rowsFooterConfigMap[1].AutoMerge)
|
||||
assert.True(t, table.rowsFooterConfigMap[2].AutoMerge)
|
||||
}
|
||||
|
||||
func TestTable_AppendHeader(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||
|
||||
table.AppendHeader([]interface{}{})
|
||||
assert.Equal(t, 0, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 1, len(table.rowsHeaderRaw))
|
||||
|
||||
table.AppendHeader([]interface{}{})
|
||||
assert.Equal(t, 0, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 2, len(table.rowsHeaderRaw))
|
||||
|
||||
table.AppendHeader([]interface{}{}, RowConfig{AutoMerge: true})
|
||||
assert.Equal(t, 0, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 3, len(table.rowsHeaderRaw))
|
||||
assert.False(t, table.rowsHeaderConfigMap[0].AutoMerge)
|
||||
assert.False(t, table.rowsHeaderConfigMap[1].AutoMerge)
|
||||
assert.True(t, table.rowsHeaderConfigMap[2].AutoMerge)
|
||||
}
|
||||
|
||||
func TestTable_AppendRow(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Equal(t, 0, table.Length())
|
||||
|
||||
table.AppendRow([]interface{}{})
|
||||
assert.Equal(t, 1, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooter))
|
||||
assert.Equal(t, 0, len(table.rowsHeader))
|
||||
|
||||
table.AppendRow([]interface{}{})
|
||||
assert.Equal(t, 2, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooter))
|
||||
assert.Equal(t, 0, len(table.rowsHeader))
|
||||
|
||||
table.AppendRow([]interface{}{}, RowConfig{AutoMerge: true})
|
||||
assert.Equal(t, 3, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||
assert.False(t, table.rowsConfigMap[0].AutoMerge)
|
||||
assert.False(t, table.rowsConfigMap[1].AutoMerge)
|
||||
assert.True(t, table.rowsConfigMap[2].AutoMerge)
|
||||
}
|
||||
|
||||
func TestTable_AppendRows(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Equal(t, 0, table.Length())
|
||||
|
||||
table.AppendRows([]Row{{}})
|
||||
assert.Equal(t, 1, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooter))
|
||||
assert.Equal(t, 0, len(table.rowsHeader))
|
||||
|
||||
table.AppendRows([]Row{{}})
|
||||
assert.Equal(t, 2, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooter))
|
||||
assert.Equal(t, 0, len(table.rowsHeader))
|
||||
|
||||
table.AppendRows([]Row{{}, {}}, RowConfig{AutoMerge: true})
|
||||
assert.Equal(t, 4, table.Length())
|
||||
assert.Equal(t, 0, len(table.rowsFooterRaw))
|
||||
assert.Equal(t, 0, len(table.rowsHeaderRaw))
|
||||
assert.False(t, table.rowsConfigMap[0].AutoMerge)
|
||||
assert.False(t, table.rowsConfigMap[1].AutoMerge)
|
||||
assert.True(t, table.rowsConfigMap[2].AutoMerge)
|
||||
assert.True(t, table.rowsConfigMap[3].AutoMerge)
|
||||
}
|
||||
|
||||
func TestTable_Length(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Zero(t, table.Length())
|
||||
|
||||
table.AppendRow(testRows[0])
|
||||
assert.Equal(t, 1, table.Length())
|
||||
table.AppendRow(testRows[1])
|
||||
assert.Equal(t, 2, table.Length())
|
||||
|
||||
table.AppendHeader(testHeader)
|
||||
assert.Equal(t, 2, table.Length())
|
||||
}
|
||||
|
||||
func TestTable_ResetFooters(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendFooter(testFooter)
|
||||
assert.NotEmpty(t, table.rowsFooterRaw)
|
||||
|
||||
table.ResetFooters()
|
||||
assert.Empty(t, table.rowsFooterRaw)
|
||||
}
|
||||
|
||||
func TestTable_ResetHeaders(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendHeader(testHeader)
|
||||
assert.NotEmpty(t, table.rowsHeaderRaw)
|
||||
|
||||
table.ResetHeaders()
|
||||
assert.Empty(t, table.rowsHeaderRaw)
|
||||
}
|
||||
|
||||
func TestTable_ResetRows(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRows(testRows)
|
||||
assert.NotEmpty(t, table.rowsRaw)
|
||||
|
||||
table.ResetRows()
|
||||
assert.Empty(t, table.rowsRaw)
|
||||
}
|
||||
|
||||
func TestTable_SetAllowedRowLength(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRows(testRows)
|
||||
table.SetStyle(styleTest)
|
||||
|
||||
expectedOutWithNoRowLimit := `(-----^--------^-----------^------^-----------------------------)
|
||||
[< 1>|<Arya >|<Stark >|<3000>|< >]
|
||||
[< 20>|<Jon >|<Snow >|<2000>|<You know nothing, Jon Snow!>]
|
||||
[<300>|<Tyrion>|<Lannister>|<5000>|< >]
|
||||
\-----v--------v-----------v------v-----------------------------/`
|
||||
assert.Zero(t, table.allowedRowLength)
|
||||
assert.Equal(t, expectedOutWithNoRowLimit, table.Render())
|
||||
|
||||
table.SetAllowedRowLength(utf8.RuneCountInString(table.style.Box.UnfinishedRow))
|
||||
assert.Equal(t, utf8.RuneCountInString(table.style.Box.UnfinishedRow), table.allowedRowLength)
|
||||
assert.Equal(t, "", table.Render())
|
||||
|
||||
table.SetAllowedRowLength(5)
|
||||
expectedOutWithRowLimit := `( ~~~
|
||||
[ ~~~
|
||||
[ ~~~
|
||||
[ ~~~
|
||||
\ ~~~`
|
||||
assert.Equal(t, 5, table.allowedRowLength)
|
||||
assert.Equal(t, expectedOutWithRowLimit, table.Render())
|
||||
|
||||
table.SetAllowedRowLength(30)
|
||||
expectedOutWithRowLimit = `(-----^--------^---------- ~~~
|
||||
[< 1>|<Arya >|<Stark ~~~
|
||||
[< 20>|<Jon >|<Snow ~~~
|
||||
[<300>|<Tyrion>|<Lannister ~~~
|
||||
\-----v--------v---------- ~~~`
|
||||
assert.Equal(t, 30, table.allowedRowLength)
|
||||
assert.Equal(t, expectedOutWithRowLimit, table.Render())
|
||||
|
||||
table.SetAllowedRowLength(300)
|
||||
assert.Equal(t, 300, table.allowedRowLength)
|
||||
assert.Equal(t, expectedOutWithNoRowLimit, table.Render())
|
||||
}
|
||||
|
||||
func TestTable_SetAutoIndex(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRows(testRows)
|
||||
table.SetStyle(styleTest)
|
||||
|
||||
expectedOut := `(-----^--------^-----------^------^-----------------------------)
|
||||
[< 1>|<Arya >|<Stark >|<3000>|< >]
|
||||
[< 20>|<Jon >|<Snow >|<2000>|<You know nothing, Jon Snow!>]
|
||||
[<300>|<Tyrion>|<Lannister>|<5000>|< >]
|
||||
\-----v--------v-----------v------v-----------------------------/`
|
||||
assert.False(t, table.autoIndex)
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
|
||||
table.SetAutoIndex(true)
|
||||
expectedOut = `(---^-----^--------^-----------^------^-----------------------------)
|
||||
[< >|< A>|< B >|< C >|< D>|< E >]
|
||||
{---+-----+--------+-----------+------+-----------------------------}
|
||||
[<1>|< 1>|<Arya >|<Stark >|<3000>|< >]
|
||||
[<2>|< 20>|<Jon >|<Snow >|<2000>|<You know nothing, Jon Snow!>]
|
||||
[<3>|<300>|<Tyrion>|<Lannister>|<5000>|< >]
|
||||
\---v-----v--------v-----------v------v-----------------------------/`
|
||||
assert.True(t, table.autoIndex)
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
|
||||
table.AppendHeader(testHeader)
|
||||
expectedOut = `(---^-----^------------^-----------^--------^-----------------------------)
|
||||
[< >|< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||
{---+-----+------------+-----------+--------+-----------------------------}
|
||||
[<1>|< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||
[<2>|< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||
[<3>|<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||
\---v-----v------------v-----------v--------v-----------------------------/`
|
||||
assert.True(t, table.autoIndex)
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
|
||||
table.AppendRow(testRowMultiLine)
|
||||
expectedOut = `(---^-----^------------^-----------^--------^-----------------------------)
|
||||
[< >|< #>|<FIRST NAME>|<LAST NAME>|<SALARY>|< >]
|
||||
{---+-----+------------+-----------+--------+-----------------------------}
|
||||
[<1>|< 1>|<Arya >|<Stark >|< 3000>|< >]
|
||||
[<2>|< 20>|<Jon >|<Snow >|< 2000>|<You know nothing, Jon Snow!>]
|
||||
[<3>|<300>|<Tyrion >|<Lannister>|< 5000>|< >]
|
||||
[<4>|< 0>|<Winter >|<Is >|< 0>|<Coming. >]
|
||||
[< >|< >|< >|< >|< >|<The North Remembers! >]
|
||||
[< >|< >|< >|< >|< >|<This is known. >]
|
||||
\---v-----v------------v-----------v--------v-----------------------------/`
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
|
||||
table.SetStyle(StyleLight)
|
||||
expectedOut = `┌───┬─────┬────────────┬───────────┬────────┬─────────────────────────────┐
|
||||
│ │ # │ FIRST NAME │ LAST NAME │ SALARY │ │
|
||||
├───┼─────┼────────────┼───────────┼────────┼─────────────────────────────┤
|
||||
│ 1 │ 1 │ Arya │ Stark │ 3000 │ │
|
||||
│ 2 │ 20 │ Jon │ Snow │ 2000 │ You know nothing, Jon Snow! │
|
||||
│ 3 │ 300 │ Tyrion │ Lannister │ 5000 │ │
|
||||
│ 4 │ 0 │ Winter │ Is │ 0 │ Coming. │
|
||||
│ │ │ │ │ │ The North Remembers! │
|
||||
│ │ │ │ │ │ This is known. │
|
||||
└───┴─────┴────────────┴───────────┴────────┴─────────────────────────────┘`
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
}
|
||||
|
||||
func TestTable_SetCaption(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Empty(t, table.caption)
|
||||
|
||||
table.SetCaption(testCaption)
|
||||
assert.NotEmpty(t, table.caption)
|
||||
assert.Equal(t, testCaption, table.caption)
|
||||
}
|
||||
|
||||
func TestTable_SetColumnConfigs(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Empty(t, table.columnConfigs)
|
||||
|
||||
table.SetColumnConfigs([]ColumnConfig{{}, {}, {}})
|
||||
assert.NotEmpty(t, table.columnConfigs)
|
||||
assert.Equal(t, 3, len(table.columnConfigs))
|
||||
}
|
||||
|
||||
func TestTable_SetHTMLCSSClass(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRow(testRows[0])
|
||||
expectedHTML := `<table class="` + DefaultHTMLCSSClass + `">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="right">1</td>
|
||||
<td>Arya</td>
|
||||
<td>Stark</td>
|
||||
<td align="right">3000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
assert.Equal(t, "", table.htmlCSSClass)
|
||||
assert.Equal(t, expectedHTML, table.RenderHTML())
|
||||
|
||||
table.SetHTMLCSSClass(testCSSClass)
|
||||
assert.Equal(t, testCSSClass, table.htmlCSSClass)
|
||||
assert.Equal(t, strings.Replace(expectedHTML, DefaultHTMLCSSClass, testCSSClass, -1), table.RenderHTML())
|
||||
}
|
||||
|
||||
func TestTable_SetOutputMirror(t *testing.T) {
|
||||
table := Table{}
|
||||
table.AppendRow(testRows[0])
|
||||
expectedOut := `+---+------+-------+------+
|
||||
| 1 | Arya | Stark | 3000 |
|
||||
+---+------+-------+------+`
|
||||
assert.Equal(t, nil, table.outputMirror)
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
|
||||
mockOutputMirror := &myMockOutputMirror{}
|
||||
table.SetOutputMirror(mockOutputMirror)
|
||||
assert.Equal(t, mockOutputMirror, table.outputMirror)
|
||||
assert.Equal(t, expectedOut, table.Render())
|
||||
assert.Equal(t, expectedOut+"\n", mockOutputMirror.mirroredOutput)
|
||||
}
|
||||
|
||||
func TestTable_SePageSize(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Equal(t, 0, table.pageSize)
|
||||
|
||||
table.SetPageSize(13)
|
||||
assert.Equal(t, 13, table.pageSize)
|
||||
}
|
||||
|
||||
func TestTable_SortByColumn(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.Empty(t, table.sortBy)
|
||||
|
||||
table.SortBy([]SortBy{{Name: "#", Mode: Asc}})
|
||||
assert.Equal(t, 1, len(table.sortBy))
|
||||
|
||||
table.SortBy([]SortBy{{Name: "First Name", Mode: Dsc}, {Name: "Last Name", Mode: Asc}})
|
||||
assert.Equal(t, 2, len(table.sortBy))
|
||||
}
|
||||
|
||||
func TestTable_SetStyle(t *testing.T) {
|
||||
table := Table{}
|
||||
assert.NotNil(t, table.Style())
|
||||
assert.Equal(t, StyleDefault, *table.Style())
|
||||
|
||||
table.SetStyle(StyleDefault)
|
||||
assert.NotNil(t, table.Style())
|
||||
assert.Equal(t, StyleDefault, *table.Style())
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// AutoIndexColumnID returns a unique Column ID/Name for the given Column Number.
|
||||
// The functionality is similar to what you get in an Excel spreadsheet w.r.t.
|
||||
// the Column ID/Name.
|
||||
func AutoIndexColumnID(colIdx int) string {
|
||||
charIdx := colIdx % 26
|
||||
out := string(rune(65 + charIdx))
|
||||
colIdx = colIdx / 26
|
||||
if colIdx > 0 {
|
||||
return AutoIndexColumnID(colIdx-1) + out
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// WidthEnforcer is a function that helps enforce a width condition on a string.
|
||||
type WidthEnforcer func(col string, maxLen int) string
|
||||
|
||||
// widthEnforcerNone returns the input string as is without any modifications.
|
||||
func widthEnforcerNone(col string, maxLen int) string {
|
||||
return col
|
||||
}
|
||||
|
||||
// isNumber returns true if the argument is a numeric type; false otherwise.
|
||||
func isNumber(x interface{}) bool {
|
||||
if x == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch reflect.TypeOf(x).Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type mergedColumnIndices map[int]map[int]bool
|
||||
|
||||
func (m mergedColumnIndices) mergedLength(colIdx int, maxColumnLengths []int) int {
|
||||
mergedLength := maxColumnLengths[colIdx]
|
||||
for otherColIdx := range m[colIdx] {
|
||||
mergedLength += maxColumnLengths[otherColIdx]
|
||||
}
|
||||
return mergedLength
|
||||
}
|
||||
|
||||
func (m mergedColumnIndices) len(colIdx int) int {
|
||||
return len(m[colIdx]) + 1
|
||||
}
|
||||
|
||||
func (m mergedColumnIndices) safeAppend(colIdx, otherColIdx int) {
|
||||
// map
|
||||
if m[colIdx] == nil {
|
||||
m[colIdx] = make(map[int]bool)
|
||||
}
|
||||
m[colIdx][otherColIdx] = true
|
||||
|
||||
// reverse map
|
||||
if m[otherColIdx] == nil {
|
||||
m[otherColIdx] = make(map[int]bool)
|
||||
}
|
||||
m[otherColIdx][colIdx] = true
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleAutoIndexColumnID() {
|
||||
fmt.Printf("AutoIndexColumnID( 0): \"%s\"\n", AutoIndexColumnID(0))
|
||||
fmt.Printf("AutoIndexColumnID( 1): \"%s\"\n", AutoIndexColumnID(1))
|
||||
fmt.Printf("AutoIndexColumnID( 2): \"%s\"\n", AutoIndexColumnID(2))
|
||||
fmt.Printf("AutoIndexColumnID( 25): \"%s\"\n", AutoIndexColumnID(25))
|
||||
fmt.Printf("AutoIndexColumnID( 26): \"%s\"\n", AutoIndexColumnID(26))
|
||||
fmt.Printf("AutoIndexColumnID( 702): \"%s\"\n", AutoIndexColumnID(702))
|
||||
fmt.Printf("AutoIndexColumnID(18278): \"%s\"\n", AutoIndexColumnID(18278))
|
||||
|
||||
// Output: AutoIndexColumnID( 0): "A"
|
||||
// AutoIndexColumnID( 1): "B"
|
||||
// AutoIndexColumnID( 2): "C"
|
||||
// AutoIndexColumnID( 25): "Z"
|
||||
// AutoIndexColumnID( 26): "AA"
|
||||
// AutoIndexColumnID( 702): "AAA"
|
||||
// AutoIndexColumnID(18278): "AAAA"
|
||||
}
|
||||
|
||||
func TestAutoIndexColumnID(t *testing.T) {
|
||||
assert.Equal(t, "A", AutoIndexColumnID(0))
|
||||
assert.Equal(t, "Z", AutoIndexColumnID(25))
|
||||
assert.Equal(t, "AA", AutoIndexColumnID(26))
|
||||
assert.Equal(t, "ZZ", AutoIndexColumnID(701))
|
||||
assert.Equal(t, "AAA", AutoIndexColumnID(702))
|
||||
assert.Equal(t, "ZZZ", AutoIndexColumnID(18277))
|
||||
assert.Equal(t, "AAAA", AutoIndexColumnID(18278))
|
||||
}
|
||||
|
||||
func TestIsNumber(t *testing.T) {
|
||||
assert.True(t, isNumber(int(1)))
|
||||
assert.True(t, isNumber(int8(1)))
|
||||
assert.True(t, isNumber(int16(1)))
|
||||
assert.True(t, isNumber(int32(1)))
|
||||
assert.True(t, isNumber(int64(1)))
|
||||
assert.True(t, isNumber(uint(1)))
|
||||
assert.True(t, isNumber(uint8(1)))
|
||||
assert.True(t, isNumber(uint16(1)))
|
||||
assert.True(t, isNumber(uint32(1)))
|
||||
assert.True(t, isNumber(uint64(1)))
|
||||
assert.True(t, isNumber(float32(1)))
|
||||
assert.True(t, isNumber(float64(1)))
|
||||
assert.False(t, isNumber("1"))
|
||||
assert.False(t, isNumber(nil))
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// Writer declares the interfaces that can be used to setup and render a table.
|
||||
type Writer interface {
|
||||
AppendFooter(row Row, configs ...RowConfig)
|
||||
AppendHeader(row Row, configs ...RowConfig)
|
||||
AppendRow(row Row, configs ...RowConfig)
|
||||
AppendRows(rows []Row, configs ...RowConfig)
|
||||
AppendSeparator()
|
||||
Length() int
|
||||
Render() string
|
||||
RenderCSV() string
|
||||
RenderHTML() string
|
||||
RenderMarkdown() string
|
||||
ResetFooters()
|
||||
ResetHeaders()
|
||||
ResetRows()
|
||||
SetAllowedRowLength(length int)
|
||||
SetAutoIndex(autoIndex bool)
|
||||
SetCaption(format string, a ...interface{})
|
||||
SetColumnConfigs(configs []ColumnConfig)
|
||||
SetIndexColumn(colNum int)
|
||||
SetOutputMirror(mirror io.Writer)
|
||||
SetPageSize(numLines int)
|
||||
SetRowPainter(painter RowPainter)
|
||||
SetStyle(style Style)
|
||||
SetTitle(format string, a ...interface{})
|
||||
SortBy(sortBy []SortBy)
|
||||
Style() *Style
|
||||
SuppressEmptyColumns()
|
||||
|
||||
// deprecated; in favor of Style().HTML.CSSClass
|
||||
SetHTMLCSSClass(cssClass string)
|
||||
}
|
||||
|
||||
// NewWriter initializes and returns a Writer.
|
||||
func NewWriter() Writer {
|
||||
return &Table{}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.hexq.cn/tiglog/golib/helper/text"
|
||||
)
|
||||
|
||||
func Example_simple() {
|
||||
// simple table with zero customizations
|
||||
tw := NewWriter()
|
||||
// append a header row
|
||||
tw.AppendHeader(Row{"#", "First Name", "Last Name", "Salary"})
|
||||
// append some data rows
|
||||
tw.AppendRows([]Row{
|
||||
{1, "Arya", "Stark", 3000},
|
||||
{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
{300, "Tyrion", "Lannister", 5000},
|
||||
})
|
||||
// append a footer row
|
||||
tw.AppendFooter(Row{"", "", "Total", 10000})
|
||||
// render it
|
||||
fmt.Printf("Table without any customizations:\n%s", tw.Render())
|
||||
|
||||
// Output: Table without any customizations:
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | # | FIRST NAME | LAST NAME | SALARY | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | 1 | Arya | Stark | 3000 | |
|
||||
// | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! |
|
||||
// | 300 | Tyrion | Lannister | 5000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
// | | | TOTAL | 10000 | |
|
||||
// +-----+------------+-----------+--------+-----------------------------+
|
||||
}
|
||||
|
||||
func Example_styled() {
|
||||
// table with some amount of customization
|
||||
tw := NewWriter()
|
||||
// append a header row
|
||||
tw.AppendHeader(Row{"First Name", "Last Name", "Salary"})
|
||||
// append some data rows
|
||||
tw.AppendRows([]Row{
|
||||
{"Jaime", "Lannister", 5000},
|
||||
{"Arya", "Stark", 3000, "A girl has no name."},
|
||||
{"Sansa", "Stark", 4000},
|
||||
{"Jon", "Snow", 2000, "You know nothing, Jon Snow!"},
|
||||
{"Tyrion", "Lannister", 5000, "A Lannister always pays his debts."},
|
||||
})
|
||||
// append a footer row
|
||||
tw.AppendFooter(Row{"", "Total", 10000})
|
||||
// auto-index rows
|
||||
tw.SetAutoIndex(true)
|
||||
// sort by last name and then by salary
|
||||
tw.SortBy([]SortBy{{Name: "Last Name", Mode: Dsc}, {Name: "Salary", Mode: AscNumeric}})
|
||||
// use a ready-to-use style
|
||||
tw.SetStyle(StyleLight)
|
||||
// customize the style and change some stuff
|
||||
tw.Style().Format.Header = text.FormatLower
|
||||
tw.Style().Format.Row = text.FormatLower
|
||||
tw.Style().Format.Footer = text.FormatLower
|
||||
tw.Style().Options.SeparateColumns = false
|
||||
// render it
|
||||
fmt.Printf("Table with customizations:\n%s", tw.Render())
|
||||
|
||||
// Output: Table with customizations:
|
||||
// ┌──────────────────────────────────────────────────────────────────────┐
|
||||
// │ first name last name salary │
|
||||
// ├──────────────────────────────────────────────────────────────────────┤
|
||||
// │ 1 arya stark 3000 a girl has no name. │
|
||||
// │ 2 sansa stark 4000 │
|
||||
// │ 3 jon snow 2000 you know nothing, jon snow! │
|
||||
// │ 4 jaime lannister 5000 │
|
||||
// │ 5 tyrion lannister 5000 a lannister always pays his debts. │
|
||||
// ├──────────────────────────────────────────────────────────────────────┤
|
||||
// │ total 10000 │
|
||||
// └──────────────────────────────────────────────────────────────────────┘
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Align denotes how text is to be aligned horizontally.
|
||||
type Align int
|
||||
|
||||
// Align enumerations
|
||||
const (
|
||||
AlignDefault Align = iota // same as AlignLeft
|
||||
AlignLeft // "left "
|
||||
AlignCenter // " center "
|
||||
AlignJustify // "justify it"
|
||||
AlignRight // " right"
|
||||
)
|
||||
|
||||
// Apply aligns the text as directed. For ex.:
|
||||
// * AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// * AlignLeft.Apply("Jon Snow", 12) returns "Jon Snow "
|
||||
// * AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow "
|
||||
// * AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow"
|
||||
// * AlignRight.Apply("Jon Snow", 12) returns " Jon Snow"
|
||||
func (a Align) Apply(text string, maxLength int) string {
|
||||
text = a.trimString(text)
|
||||
sLen := utf8.RuneCountInString(text)
|
||||
sLenWoE := RuneWidthWithoutEscSequences(text)
|
||||
numEscChars := sLen - sLenWoE
|
||||
|
||||
// now, align the text
|
||||
switch a {
|
||||
case AlignDefault, AlignLeft:
|
||||
return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||
case AlignCenter:
|
||||
if sLenWoE < maxLength {
|
||||
// left pad with half the number of spaces needed before using %text
|
||||
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s",
|
||||
text+strings.Repeat(" ", int((maxLength-sLenWoE)/2)))
|
||||
}
|
||||
case AlignJustify:
|
||||
return a.justifyText(text, sLenWoE, maxLength)
|
||||
}
|
||||
return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text)
|
||||
}
|
||||
|
||||
// HTMLProperty returns the equivalent HTML horizontal-align tag property.
|
||||
func (a Align) HTMLProperty() string {
|
||||
switch a {
|
||||
case AlignLeft:
|
||||
return "align=\"left\""
|
||||
case AlignCenter:
|
||||
return "align=\"center\""
|
||||
case AlignJustify:
|
||||
return "align=\"justify\""
|
||||
case AlignRight:
|
||||
return "align=\"right\""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// MarkdownProperty returns the equivalent Markdown horizontal-align separator.
|
||||
func (a Align) MarkdownProperty() string {
|
||||
switch a {
|
||||
case AlignLeft:
|
||||
return ":--- "
|
||||
case AlignCenter:
|
||||
return ":---:"
|
||||
case AlignRight:
|
||||
return " ---:"
|
||||
default:
|
||||
return " --- "
|
||||
}
|
||||
}
|
||||
|
||||
func (a Align) justifyText(text string, textLength int, maxLength int) string {
|
||||
// split the text into individual words
|
||||
wordsUnfiltered := strings.Split(text, " ")
|
||||
words := Filter(wordsUnfiltered, func(item string) bool {
|
||||
return item != ""
|
||||
})
|
||||
// empty string implies spaces for maxLength
|
||||
if len(words) == 0 {
|
||||
return strings.Repeat(" ", maxLength)
|
||||
}
|
||||
// get the number of spaces to insert into the text
|
||||
numSpacesNeeded := maxLength - textLength + strings.Count(text, " ")
|
||||
numSpacesNeededBetweenWords := 0
|
||||
if len(words) > 1 {
|
||||
numSpacesNeededBetweenWords = numSpacesNeeded / (len(words) - 1)
|
||||
}
|
||||
// create the output string word by word with spaces in between
|
||||
var outText strings.Builder
|
||||
outText.Grow(maxLength)
|
||||
for idx, word := range words {
|
||||
if idx > 0 {
|
||||
// insert spaces only after the first word
|
||||
if idx == len(words)-1 {
|
||||
// insert all the remaining space before the last word
|
||||
outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
|
||||
numSpacesNeeded = 0
|
||||
} else {
|
||||
// insert the determined number of spaces between each word
|
||||
outText.WriteString(strings.Repeat(" ", numSpacesNeededBetweenWords))
|
||||
// and reduce the number of spaces needed after this
|
||||
numSpacesNeeded -= numSpacesNeededBetweenWords
|
||||
}
|
||||
}
|
||||
outText.WriteString(word)
|
||||
if idx == len(words)-1 && numSpacesNeeded > 0 {
|
||||
outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
|
||||
}
|
||||
}
|
||||
return outText.String()
|
||||
}
|
||||
|
||||
func (a Align) trimString(text string) string {
|
||||
switch a {
|
||||
case AlignDefault, AlignLeft:
|
||||
if strings.HasSuffix(text, " ") {
|
||||
return strings.TrimRight(text, " ")
|
||||
}
|
||||
case AlignRight:
|
||||
if strings.HasPrefix(text, " ") {
|
||||
return strings.TrimLeft(text, " ")
|
||||
}
|
||||
default:
|
||||
if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
|
||||
return strings.Trim(text, " ")
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleAlign_Apply() {
|
||||
fmt.Printf("AlignDefault: '%s'\n", AlignDefault.Apply("Jon Snow", 12))
|
||||
fmt.Printf("AlignLeft : '%s'\n", AlignLeft.Apply("Jon Snow", 12))
|
||||
fmt.Printf("AlignCenter : '%s'\n", AlignCenter.Apply("Jon Snow", 12))
|
||||
fmt.Printf("AlignJustify: '%s'\n", AlignJustify.Apply("Jon Snow", 12))
|
||||
fmt.Printf("AlignRight : '%s'\n", AlignRight.Apply("Jon Snow", 12))
|
||||
|
||||
// Output: AlignDefault: 'Jon Snow '
|
||||
// AlignLeft : 'Jon Snow '
|
||||
// AlignCenter : ' Jon Snow '
|
||||
// AlignJustify: 'Jon Snow'
|
||||
// AlignRight : ' Jon Snow'
|
||||
}
|
||||
|
||||
func TestAlign_Apply(t *testing.T) {
|
||||
// AlignDefault & AlignLeft are the same
|
||||
assert.Equal(t, "Jon Snow ", AlignDefault.Apply("Jon Snow", 12))
|
||||
assert.Equal(t, " Jon Snow ", AlignDefault.Apply(" Jon Snow", 12))
|
||||
assert.Equal(t, " ", AlignDefault.Apply("", 12))
|
||||
assert.Equal(t, "Jon Snow ", AlignLeft.Apply("Jon Snow ", 12))
|
||||
assert.Equal(t, " Jon Snow ", AlignLeft.Apply(" Jon Snow ", 12))
|
||||
assert.Equal(t, " ", AlignLeft.Apply("", 12))
|
||||
|
||||
// AlignCenter
|
||||
assert.Equal(t, " Jon Snow ", AlignCenter.Apply("Jon Snow ", 12))
|
||||
assert.Equal(t, " Jon Snow ", AlignCenter.Apply(" Jon Snow", 12))
|
||||
assert.Equal(t, " Jon Snow ", AlignCenter.Apply(" Jon Snow ", 12))
|
||||
assert.Equal(t, " ", AlignCenter.Apply("", 12))
|
||||
|
||||
// AlignJustify
|
||||
assert.Equal(t, "Jon Snow", AlignJustify.Apply("Jon Snow", 12))
|
||||
assert.Equal(t, "JS vs. DT", AlignJustify.Apply("JS vs. DT", 12))
|
||||
assert.Equal(t, "JS is AT", AlignJustify.Apply("JS is AT", 12))
|
||||
assert.Equal(t, "JS is AT", AlignJustify.Apply("JS is AT", 12))
|
||||
assert.Equal(t, "JonSnow ", AlignJustify.Apply("JonSnow", 12))
|
||||
assert.Equal(t, "JonSnow ", AlignJustify.Apply(" JonSnow", 12))
|
||||
assert.Equal(t, " ", AlignJustify.Apply("", 12))
|
||||
|
||||
// Align Right
|
||||
assert.Equal(t, " Jon Snow", AlignRight.Apply("Jon Snow", 12))
|
||||
assert.Equal(t, " Jon Snow ", AlignRight.Apply("Jon Snow ", 12))
|
||||
assert.Equal(t, " Jon Snow ", AlignRight.Apply(" Jon Snow ", 12))
|
||||
assert.Equal(t, " ", AlignRight.Apply("", 12))
|
||||
}
|
||||
|
||||
func TestAlign_Apply_ColoredText(t *testing.T) {
|
||||
// AlignDefault & AlignLeft are the same
|
||||
assert.Equal(t, "\x1b[33mJon Snow\x1b[0m ", AlignDefault.Apply("\x1b[33mJon Snow\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33m Jon Snow\x1b[0m ", AlignDefault.Apply("\x1b[33m Jon Snow\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33m\x1b[0m ", AlignDefault.Apply("\x1b[33m\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33mJon Snow \x1b[0m ", AlignLeft.Apply("\x1b[33mJon Snow \x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33m Jon Snow \x1b[0m ", AlignLeft.Apply("\x1b[33m Jon Snow \x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33m\x1b[0m ", AlignLeft.Apply("\x1b[33m\x1b[0m", 12))
|
||||
|
||||
// AlignCenter
|
||||
assert.Equal(t, " \x1b[33mJon Snow \x1b[0m ", AlignCenter.Apply("\x1b[33mJon Snow \x1b[0m", 12))
|
||||
assert.Equal(t, " \x1b[33m Jon Snow\x1b[0m ", AlignCenter.Apply("\x1b[33m Jon Snow\x1b[0m", 12))
|
||||
assert.Equal(t, " \x1b[33m Jon Snow \x1b[0m", AlignCenter.Apply("\x1b[33m Jon Snow \x1b[0m", 12))
|
||||
assert.Equal(t, " \x1b[33m\x1b[0m ", AlignCenter.Apply("\x1b[33m\x1b[0m", 12))
|
||||
|
||||
// AlignJustify
|
||||
assert.Equal(t, "\x1b[33mJon Snow\x1b[0m", AlignJustify.Apply("\x1b[33mJon Snow\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33mJS vs. DT\x1b[0m", AlignJustify.Apply("\x1b[33mJS vs. DT\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33mJS is AT\x1b[0m", AlignJustify.Apply("\x1b[33mJS is AT\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33mJS is AT\x1b[0m", AlignJustify.Apply("\x1b[33mJS is AT\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33mJonSnow\x1b[0m ", AlignJustify.Apply("\x1b[33mJonSnow\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33m JonSnow\x1b[0m", AlignJustify.Apply("\x1b[33m JonSnow\x1b[0m", 12))
|
||||
assert.Equal(t, "\x1b[33m\x1b[0m ", AlignJustify.Apply("\x1b[33m\x1b[0m", 12))
|
||||
|
||||
// Align Right
|
||||
assert.Equal(t, " \x1b[33mJon Snow\x1b[0m", AlignRight.Apply("\x1b[33mJon Snow\x1b[0m", 12))
|
||||
assert.Equal(t, " \x1b[33mJon Snow \x1b[0m", AlignRight.Apply("\x1b[33mJon Snow \x1b[0m", 12))
|
||||
assert.Equal(t, " \x1b[33m Jon Snow \x1b[0m", AlignRight.Apply("\x1b[33m Jon Snow \x1b[0m", 12))
|
||||
assert.Equal(t, " \x1b[33m\x1b[0m", AlignRight.Apply("\x1b[33m\x1b[0m", 12))
|
||||
}
|
||||
|
||||
func ExampleAlign_HTMLProperty() {
|
||||
fmt.Printf("AlignDefault: '%s'\n", AlignDefault.HTMLProperty())
|
||||
fmt.Printf("AlignLeft : '%s'\n", AlignLeft.HTMLProperty())
|
||||
fmt.Printf("AlignCenter : '%s'\n", AlignCenter.HTMLProperty())
|
||||
fmt.Printf("AlignJustify: '%s'\n", AlignJustify.HTMLProperty())
|
||||
fmt.Printf("AlignRight : '%s'\n", AlignRight.HTMLProperty())
|
||||
|
||||
// Output: AlignDefault: ''
|
||||
// AlignLeft : 'align="left"'
|
||||
// AlignCenter : 'align="center"'
|
||||
// AlignJustify: 'align="justify"'
|
||||
// AlignRight : 'align="right"'
|
||||
}
|
||||
|
||||
func TestAlign_HTMLProperty(t *testing.T) {
|
||||
aligns := map[Align]string{
|
||||
AlignDefault: "",
|
||||
AlignLeft: "left",
|
||||
AlignCenter: "center",
|
||||
AlignJustify: "justify",
|
||||
AlignRight: "right",
|
||||
}
|
||||
for align, htmlStyle := range aligns {
|
||||
assert.Contains(t, align.HTMLProperty(), htmlStyle)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleAlign_MarkdownProperty() {
|
||||
fmt.Printf("AlignDefault: '%s'\n", AlignDefault.MarkdownProperty())
|
||||
fmt.Printf("AlignLeft : '%s'\n", AlignLeft.MarkdownProperty())
|
||||
fmt.Printf("AlignCenter : '%s'\n", AlignCenter.MarkdownProperty())
|
||||
fmt.Printf("AlignJustify: '%s'\n", AlignJustify.MarkdownProperty())
|
||||
fmt.Printf("AlignRight : '%s'\n", AlignRight.MarkdownProperty())
|
||||
|
||||
// Output: AlignDefault: ' --- '
|
||||
// AlignLeft : ':--- '
|
||||
// AlignCenter : ':---:'
|
||||
// AlignJustify: ' --- '
|
||||
// AlignRight : ' ---:'
|
||||
}
|
||||
|
||||
func TestAlign_MarkdownProperty(t *testing.T) {
|
||||
aligns := map[Align]string{
|
||||
AlignDefault: " --- ",
|
||||
AlignLeft: ":--- ",
|
||||
AlignCenter: ":---:",
|
||||
AlignJustify: " --- ",
|
||||
AlignRight: " ---:",
|
||||
}
|
||||
for align, markdownSeparator := range aligns {
|
||||
assert.Contains(t, align.MarkdownProperty(), markdownSeparator)
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package text
|
||||
|
||||
import "strings"
|
||||
|
||||
// ANSICodesSupported will be true on consoles where ANSI Escape Codes/Sequences
|
||||
// are supported.
|
||||
var ANSICodesSupported = areANSICodesSupported()
|
||||
|
||||
// Escape encodes the string with the ANSI Escape Sequence.
|
||||
// For ex.:
|
||||
// Escape("Ghost", "") == "Ghost"
|
||||
// Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m"
|
||||
// Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
|
||||
func Escape(str string, escapeSeq string) string {
|
||||
out := ""
|
||||
if !strings.HasPrefix(str, EscapeStart) {
|
||||
out += escapeSeq
|
||||
}
|
||||
out += strings.Replace(str, EscapeReset, EscapeReset+escapeSeq, -1)
|
||||
if !strings.HasSuffix(out, EscapeReset) {
|
||||
out += EscapeReset
|
||||
}
|
||||
if strings.Contains(out, escapeSeq+EscapeReset) {
|
||||
out = strings.Replace(out, escapeSeq+EscapeReset, "", -1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StripEscape strips all ANSI Escape Sequence from the string.
|
||||
// For ex.:
|
||||
// StripEscape("Ghost") == "Ghost"
|
||||
// StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost"
|
||||
// StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady"
|
||||
// StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady"
|
||||
// StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady"
|
||||
func StripEscape(str string) string {
|
||||
var out strings.Builder
|
||||
out.Grow(RuneWidthWithoutEscSequences(str))
|
||||
|
||||
isEscSeq := false
|
||||
for _, sChr := range str {
|
||||
if sChr == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
}
|
||||
if !isEscSeq {
|
||||
out.WriteRune(sChr)
|
||||
}
|
||||
if isEscSeq && sChr == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEscape(t *testing.T) {
|
||||
assert.Equal(t, "\x1b[91mGhost\x1b[0m", Escape("Ghost", FgHiRed.EscapeSeq()))
|
||||
assert.Equal(t, "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", Escape(FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||
assert.Equal(t, "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", Escape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||
assert.Equal(t, "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m", Escape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq()))
|
||||
}
|
||||
|
||||
func ExampleEscape() {
|
||||
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Ghost", "", Escape("Ghost", ""))
|
||||
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Ghost", FgHiRed.EscapeSeq(), Escape("Ghost", FgHiRed.EscapeSeq()))
|
||||
fmt.Printf("Escape(%#v, %#v) == %#v\n", FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq(), Escape(FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq(), Escape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady", FgHiRed.EscapeSeq()))
|
||||
fmt.Printf("Escape(%#v, %#v) == %#v\n", "Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq(), Escape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady", FgHiRed.EscapeSeq()))
|
||||
|
||||
// Output: Escape("Ghost", "") == "Ghost"
|
||||
// Escape("Ghost", "\x1b[91m") == "\x1b[91mGhost\x1b[0m"
|
||||
// Escape("\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
|
||||
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
|
||||
}
|
||||
|
||||
func TestStripEscape(t *testing.T) {
|
||||
assert.Equal(t, "Ghost", StripEscape(FgHiRed.Sprint("Ghost")))
|
||||
assert.Equal(t, "GhostLady", StripEscape(FgHiBlue.Sprint("Ghost")+"Lady"))
|
||||
assert.Equal(t, "NymeriaGhostLady", StripEscape("Nymeria"+FgHiBlue.Sprint("Ghost")+"Lady"))
|
||||
assert.Equal(t, "Nymeria Ghost Lady", StripEscape("Nymeria "+FgHiBlue.Sprint("Ghost")+" Lady"))
|
||||
}
|
||||
|
||||
func ExampleStripEscape() {
|
||||
fmt.Printf("StripEscape(%#v) == %#v\n", "Ghost", StripEscape("Ghost"))
|
||||
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mGhost\x1b[0m", StripEscape("\x1b[91mGhost\x1b[0m"))
|
||||
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"))
|
||||
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m", StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"))
|
||||
fmt.Printf("StripEscape(%#v) == %#v\n", "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m", StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"))
|
||||
|
||||
// Output: StripEscape("Ghost") == "Ghost"
|
||||
// StripEscape("\x1b[91mGhost\x1b[0m") == "Ghost"
|
||||
// StripEscape("\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "GhostLady"
|
||||
// StripEscape("\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m") == "NymeriaGhostLady"
|
||||
// StripEscape("\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m") == "Nymeria Ghost Lady"
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package text
|
||||
|
||||
func areANSICodesSupported() bool {
|
||||
return true
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
enableVTPMutex = sync.Mutex{}
|
||||
)
|
||||
|
||||
func areANSICodesSupported() bool {
|
||||
enableVTPMutex.Lock()
|
||||
defer enableVTPMutex.Unlock()
|
||||
|
||||
outHandle := windows.Handle(os.Stdout.Fd())
|
||||
var outMode uint32
|
||||
if err := windows.GetConsoleMode(outHandle, &outMode); err == nil {
|
||||
if outMode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
|
||||
return true
|
||||
}
|
||||
if err := windows.SetConsoleMode(outHandle, outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
colorsEnabled = areANSICodesSupported()
|
||||
)
|
||||
|
||||
// DisableColors (forcefully) disables color coding globally.
|
||||
func DisableColors() {
|
||||
colorsEnabled = false
|
||||
}
|
||||
|
||||
// EnableColors (forcefully) enables color coding globally.
|
||||
func EnableColors() {
|
||||
colorsEnabled = true
|
||||
}
|
||||
|
||||
// The logic here is inspired from github.com/fatih/color; the following is
|
||||
// the the bare minimum logic required to print Colored to the console.
|
||||
// The differences:
|
||||
// * This one caches the escape sequences for cases with multiple colors
|
||||
// * This one handles cases where the incoming already has colors in the
|
||||
// form of escape sequences; in which case, text that does not have any
|
||||
// escape sequences are colored/escaped
|
||||
|
||||
// Color represents a single color to render with.
|
||||
type Color int
|
||||
|
||||
// Base colors -- attributes in reality
|
||||
const (
|
||||
Reset Color = iota
|
||||
Bold
|
||||
Faint
|
||||
Italic
|
||||
Underline
|
||||
BlinkSlow
|
||||
BlinkRapid
|
||||
ReverseVideo
|
||||
Concealed
|
||||
CrossedOut
|
||||
)
|
||||
|
||||
// Foreground colors
|
||||
const (
|
||||
FgBlack Color = iota + 30
|
||||
FgRed
|
||||
FgGreen
|
||||
FgYellow
|
||||
FgBlue
|
||||
FgMagenta
|
||||
FgCyan
|
||||
FgWhite
|
||||
)
|
||||
|
||||
// Foreground Hi-Intensity colors
|
||||
const (
|
||||
FgHiBlack Color = iota + 90
|
||||
FgHiRed
|
||||
FgHiGreen
|
||||
FgHiYellow
|
||||
FgHiBlue
|
||||
FgHiMagenta
|
||||
FgHiCyan
|
||||
FgHiWhite
|
||||
)
|
||||
|
||||
// Background colors
|
||||
const (
|
||||
BgBlack Color = iota + 40
|
||||
BgRed
|
||||
BgGreen
|
||||
BgYellow
|
||||
BgBlue
|
||||
BgMagenta
|
||||
BgCyan
|
||||
BgWhite
|
||||
)
|
||||
|
||||
// Background Hi-Intensity colors
|
||||
const (
|
||||
BgHiBlack Color = iota + 100
|
||||
BgHiRed
|
||||
BgHiGreen
|
||||
BgHiYellow
|
||||
BgHiBlue
|
||||
BgHiMagenta
|
||||
BgHiCyan
|
||||
BgHiWhite
|
||||
)
|
||||
|
||||
// EscapeSeq returns the ANSI escape sequence for the color.
|
||||
func (c Color) EscapeSeq() string {
|
||||
return EscapeStart + strconv.Itoa(int(c)) + EscapeStop
|
||||
}
|
||||
|
||||
// HTMLProperty returns the "class" attribute for the color.
|
||||
func (c Color) HTMLProperty() string {
|
||||
out := ""
|
||||
if class, ok := colorCSSClassMap[c]; ok {
|
||||
out = fmt.Sprintf("class=\"%s\"", class)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Sprint colorizes and prints the given string(s).
|
||||
func (c Color) Sprint(a ...interface{}) string {
|
||||
return colorize(fmt.Sprint(a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
// Sprintf formats and colorizes and prints the given string(s).
|
||||
func (c Color) Sprintf(format string, a ...interface{}) string {
|
||||
return colorize(fmt.Sprintf(format, a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
// Colors represents an array of Color objects to render with.
|
||||
// Example: Colors{FgCyan, BgBlack}
|
||||
type Colors []Color
|
||||
|
||||
var (
|
||||
// colorsSeqMap caches the escape sequence for a set of colors
|
||||
colorsSeqMap = sync.Map{}
|
||||
)
|
||||
|
||||
// EscapeSeq returns the ANSI escape sequence for the colors set.
|
||||
func (c Colors) EscapeSeq() string {
|
||||
if len(c) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
colorsKey := fmt.Sprintf("%#v", c)
|
||||
escapeSeq, ok := colorsSeqMap.Load(colorsKey)
|
||||
if !ok || escapeSeq == "" {
|
||||
colorNums := make([]string, len(c))
|
||||
for idx, color := range c {
|
||||
colorNums[idx] = strconv.Itoa(int(color))
|
||||
}
|
||||
escapeSeq = EscapeStart + strings.Join(colorNums, ";") + EscapeStop
|
||||
colorsSeqMap.Store(colorsKey, escapeSeq)
|
||||
}
|
||||
return escapeSeq.(string)
|
||||
}
|
||||
|
||||
// HTMLProperty returns the "class" attribute for the colors.
|
||||
func (c Colors) HTMLProperty() string {
|
||||
if len(c) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var classes []string
|
||||
for _, color := range c {
|
||||
if class, ok := colorCSSClassMap[color]; ok {
|
||||
classes = append(classes, class)
|
||||
}
|
||||
}
|
||||
if len(classes) > 1 {
|
||||
sort.Strings(classes)
|
||||
}
|
||||
return fmt.Sprintf("class=\"%s\"", strings.Join(classes, " "))
|
||||
}
|
||||
|
||||
// Sprint colorizes and prints the given string(s).
|
||||
func (c Colors) Sprint(a ...interface{}) string {
|
||||
return colorize(fmt.Sprint(a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
// Sprintf formats and colorizes and prints the given string(s).
|
||||
func (c Colors) Sprintf(format string, a ...interface{}) string {
|
||||
return colorize(fmt.Sprintf(format, a...), c.EscapeSeq())
|
||||
}
|
||||
|
||||
func colorize(s string, escapeSeq string) string {
|
||||
if !colorsEnabled || escapeSeq == "" {
|
||||
return s
|
||||
}
|
||||
return Escape(s, escapeSeq)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package text
|
||||
|
||||
var (
|
||||
// colorCSSClassMap contains the equivalent CSS-class for all colors
|
||||
colorCSSClassMap = map[Color]string{
|
||||
Bold: "bold",
|
||||
Faint: "faint",
|
||||
Italic: "italic",
|
||||
Underline: "underline",
|
||||
BlinkSlow: "blink-slow",
|
||||
BlinkRapid: "blink-rapid",
|
||||
ReverseVideo: "reverse-video",
|
||||
Concealed: "concealed",
|
||||
CrossedOut: "crossed-out",
|
||||
FgBlack: "fg-black",
|
||||
FgRed: "fg-red",
|
||||
FgGreen: "fg-green",
|
||||
FgYellow: "fg-yellow",
|
||||
FgBlue: "fg-blue",
|
||||
FgMagenta: "fg-magenta",
|
||||
FgCyan: "fg-cyan",
|
||||
FgWhite: "fg-white",
|
||||
FgHiBlack: "fg-hi-black",
|
||||
FgHiRed: "fg-hi-red",
|
||||
FgHiGreen: "fg-hi-green",
|
||||
FgHiYellow: "fg-hi-yellow",
|
||||
FgHiBlue: "fg-hi-blue",
|
||||
FgHiMagenta: "fg-hi-magenta",
|
||||
FgHiCyan: "fg-hi-cyan",
|
||||
FgHiWhite: "fg-hi-white",
|
||||
BgBlack: "bg-black",
|
||||
BgRed: "bg-red",
|
||||
BgGreen: "bg-green",
|
||||
BgYellow: "bg-yellow",
|
||||
BgBlue: "bg-blue",
|
||||
BgMagenta: "bg-magenta",
|
||||
BgCyan: "bg-cyan",
|
||||
BgWhite: "bg-white",
|
||||
BgHiBlack: "bg-hi-black",
|
||||
BgHiRed: "bg-hi-red",
|
||||
BgHiGreen: "bg-hi-green",
|
||||
BgHiYellow: "bg-hi-yellow",
|
||||
BgHiBlue: "bg-hi-blue",
|
||||
BgHiMagenta: "bg-hi-magenta",
|
||||
BgHiCyan: "bg-hi-cyan",
|
||||
BgHiWhite: "bg-hi-white",
|
||||
}
|
||||
)
|
@ -1,158 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
EnableColors()
|
||||
}
|
||||
|
||||
func TestColor_EnableAndDisable(t *testing.T) {
|
||||
defer EnableColors()
|
||||
|
||||
EnableColors()
|
||||
assert.Equal(t, "\x1b[31mtest\x1b[0m", FgRed.Sprint("test"))
|
||||
|
||||
DisableColors()
|
||||
assert.Equal(t, "test", FgRed.Sprint("test"))
|
||||
|
||||
EnableColors()
|
||||
assert.Equal(t, "\x1b[31mtest\x1b[0m", FgRed.Sprint("test"))
|
||||
}
|
||||
|
||||
func ExampleColor_EscapeSeq() {
|
||||
fmt.Printf("Black Background: %#v\n", BgBlack.EscapeSeq())
|
||||
fmt.Printf("Black Foreground: %#v\n", FgBlack.EscapeSeq())
|
||||
|
||||
// Output: Black Background: "\x1b[40m"
|
||||
// Black Foreground: "\x1b[30m"
|
||||
}
|
||||
|
||||
func TestColor_EscapeSeq(t *testing.T) {
|
||||
assert.Equal(t, "\x1b[40m", BgBlack.EscapeSeq())
|
||||
}
|
||||
|
||||
func ExampleColor_HTMLProperty() {
|
||||
fmt.Printf("Bold: %#v\n", Bold.HTMLProperty())
|
||||
fmt.Printf("Black Background: %#v\n", BgBlack.HTMLProperty())
|
||||
fmt.Printf("Black Foreground: %#v\n", FgBlack.HTMLProperty())
|
||||
|
||||
// Output: Bold: "class=\"bold\""
|
||||
// Black Background: "class=\"bg-black\""
|
||||
// Black Foreground: "class=\"fg-black\""
|
||||
}
|
||||
|
||||
func TestColor_HTMLProperty(t *testing.T) {
|
||||
assert.Equal(t, "class=\"bold\"", Bold.HTMLProperty())
|
||||
assert.Equal(t, "class=\"bg-black\"", BgBlack.HTMLProperty())
|
||||
assert.Equal(t, "class=\"fg-black\"", FgBlack.HTMLProperty())
|
||||
}
|
||||
|
||||
func ExampleColor_Sprint() {
|
||||
fmt.Printf("%#v\n", BgBlack.Sprint("Black Background"))
|
||||
fmt.Printf("%#v\n", FgBlack.Sprint("Black Foreground"))
|
||||
|
||||
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||
}
|
||||
|
||||
func TestColor_Sprint(t *testing.T) {
|
||||
assert.Equal(t, "\x1b[31mtest true\x1b[0m", FgRed.Sprint("test ", true))
|
||||
|
||||
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31mtrue\x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m", true))
|
||||
assert.Equal(t, "\x1b[32mtest true\x1b[0m", FgRed.Sprint("\x1b[32mtest ", true))
|
||||
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31m \x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m "))
|
||||
assert.Equal(t, "\x1b[32mtest\x1b[0m", FgRed.Sprint("\x1b[32mtest\x1b[0m"))
|
||||
}
|
||||
|
||||
func ExampleColor_Sprintf() {
|
||||
fmt.Printf("%#v\n", BgBlack.Sprintf("%s %s", "Black", "Background"))
|
||||
fmt.Printf("%#v\n", FgBlack.Sprintf("%s %s", "Black", "Foreground"))
|
||||
|
||||
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||
}
|
||||
|
||||
func TestColor_Sprintf(t *testing.T) {
|
||||
assert.Equal(t, "\x1b[31mtest true\x1b[0m", FgRed.Sprintf("test %s", "true"))
|
||||
}
|
||||
|
||||
func ExampleColors_EscapeSeq() {
|
||||
fmt.Printf("Black Background: %#v\n", Colors{BgBlack}.EscapeSeq())
|
||||
fmt.Printf("Black Foreground: %#v\n", Colors{FgBlack}.EscapeSeq())
|
||||
fmt.Printf("Black Background, White Foreground: %#v\n", Colors{BgBlack, FgWhite}.EscapeSeq())
|
||||
fmt.Printf("Black Foreground, White Background: %#v\n", Colors{FgBlack, BgWhite}.EscapeSeq())
|
||||
|
||||
// Output: Black Background: "\x1b[40m"
|
||||
// Black Foreground: "\x1b[30m"
|
||||
// Black Background, White Foreground: "\x1b[40;37m"
|
||||
// Black Foreground, White Background: "\x1b[30;47m"
|
||||
}
|
||||
|
||||
func TestColors_EscapeSeq(t *testing.T) {
|
||||
assert.Equal(t, "", Colors{}.EscapeSeq())
|
||||
assert.Equal(t, "\x1b[40;37m", Colors{BgBlack, FgWhite}.EscapeSeq())
|
||||
}
|
||||
|
||||
func ExampleColors_HTMLProperty() {
|
||||
fmt.Printf("Black Background: %#v\n", Colors{BgBlack}.HTMLProperty())
|
||||
fmt.Printf("Black Foreground: %#v\n", Colors{FgBlack}.HTMLProperty())
|
||||
fmt.Printf("Black Background, White Foreground: %#v\n", Colors{BgBlack, FgWhite}.HTMLProperty())
|
||||
fmt.Printf("Black Foreground, White Background: %#v\n", Colors{FgBlack, BgWhite}.HTMLProperty())
|
||||
fmt.Printf("Bold Italic Underline Red Text: %#v\n", Colors{Bold, Italic, Underline, FgRed}.HTMLProperty())
|
||||
|
||||
// Output: Black Background: "class=\"bg-black\""
|
||||
// Black Foreground: "class=\"fg-black\""
|
||||
// Black Background, White Foreground: "class=\"bg-black fg-white\""
|
||||
// Black Foreground, White Background: "class=\"bg-white fg-black\""
|
||||
// Bold Italic Underline Red Text: "class=\"bold fg-red italic underline\""
|
||||
}
|
||||
|
||||
func TestColors_HTMLProperty(t *testing.T) {
|
||||
assert.Equal(t, "", Colors{}.HTMLProperty())
|
||||
assert.Equal(t, "class=\"bg-black fg-white\"", Colors{BgBlack, FgWhite}.HTMLProperty())
|
||||
assert.Equal(t, "class=\"bold fg-red\"", Colors{Bold, FgRed}.HTMLProperty())
|
||||
}
|
||||
|
||||
func ExampleColors_Sprint() {
|
||||
fmt.Printf("%#v\n", Colors{BgBlack}.Sprint("Black Background"))
|
||||
fmt.Printf("%#v\n", Colors{BgBlack, FgWhite}.Sprint("Black Background, White Foreground"))
|
||||
fmt.Printf("%#v\n", Colors{FgBlack}.Sprint("Black Foreground"))
|
||||
fmt.Printf("%#v\n", Colors{FgBlack, BgWhite}.Sprint("Black Foreground, White Background"))
|
||||
|
||||
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||
// "\x1b[40;37mBlack Background, White Foreground\x1b[0m"
|
||||
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||
// "\x1b[30;47mBlack Foreground, White Background\x1b[0m"
|
||||
}
|
||||
|
||||
func TestColors_Sprint(t *testing.T) {
|
||||
assert.Equal(t, "test true", Colors{}.Sprint("test ", true))
|
||||
assert.Equal(t, "\x1b[31mtest true\x1b[0m", Colors{FgRed}.Sprint("test ", true))
|
||||
|
||||
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31mtrue\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m", true))
|
||||
assert.Equal(t, "\x1b[32mtest true\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest ", true))
|
||||
assert.Equal(t, "\x1b[32mtest\x1b[0m\x1b[31m \x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m "))
|
||||
assert.Equal(t, "\x1b[32mtest\x1b[0m", Colors{FgRed}.Sprint("\x1b[32mtest\x1b[0m"))
|
||||
}
|
||||
|
||||
func ExampleColors_Sprintf() {
|
||||
fmt.Printf("%#v\n", Colors{BgBlack}.Sprintf("%s %s", "Black", "Background"))
|
||||
fmt.Printf("%#v\n", Colors{BgBlack, FgWhite}.Sprintf("%s, %s", "Black Background", "White Foreground"))
|
||||
fmt.Printf("%#v\n", Colors{FgBlack}.Sprintf("%s %s", "Black", "Foreground"))
|
||||
fmt.Printf("%#v\n", Colors{FgBlack, BgWhite}.Sprintf("%s, %s", "Black Foreground", "White Background"))
|
||||
|
||||
// Output: "\x1b[40mBlack Background\x1b[0m"
|
||||
// "\x1b[40;37mBlack Background, White Foreground\x1b[0m"
|
||||
// "\x1b[30mBlack Foreground\x1b[0m"
|
||||
// "\x1b[30;47mBlack Foreground, White Background\x1b[0m"
|
||||
}
|
||||
|
||||
func TestColors_Sprintf(t *testing.T) {
|
||||
assert.Equal(t, "test true", Colors{}.Sprintf("test %s", "true"))
|
||||
assert.Equal(t, "\x1b[31mtest true\x1b[0m", Colors{FgRed}.Sprintf("test %s", "true"))
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Cursor helps move the cursor on the console in multiple directions.
|
||||
type Cursor rune
|
||||
|
||||
const (
|
||||
// CursorDown helps move the Cursor Down X lines
|
||||
CursorDown Cursor = 'B'
|
||||
|
||||
// CursorLeft helps move the Cursor Left X characters
|
||||
CursorLeft Cursor = 'D'
|
||||
|
||||
// CursorRight helps move the Cursor Right X characters
|
||||
CursorRight Cursor = 'C'
|
||||
|
||||
// CursorUp helps move the Cursor Up X lines
|
||||
CursorUp Cursor = 'A'
|
||||
|
||||
// EraseLine helps erase all characters to the Right of the Cursor in the
|
||||
// current line
|
||||
EraseLine Cursor = 'K'
|
||||
)
|
||||
|
||||
// Sprint prints the Escape Sequence to move the Cursor once.
|
||||
func (c Cursor) Sprint() string {
|
||||
return fmt.Sprintf("%s%c", EscapeStart, c)
|
||||
}
|
||||
|
||||
// Sprintn prints the Escape Sequence to move the Cursor "n" times.
|
||||
func (c Cursor) Sprintn(n int) string {
|
||||
if c == EraseLine {
|
||||
return c.Sprint()
|
||||
}
|
||||
return fmt.Sprintf("%s%d%c", EscapeStart, n, c)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleCursor_Sprint() {
|
||||
fmt.Printf("CursorDown : %#v\n", CursorDown.Sprint())
|
||||
fmt.Printf("CursorLeft : %#v\n", CursorLeft.Sprint())
|
||||
fmt.Printf("CursorRight: %#v\n", CursorRight.Sprint())
|
||||
fmt.Printf("CursorUp : %#v\n", CursorUp.Sprint())
|
||||
fmt.Printf("EraseLine : %#v\n", EraseLine.Sprint())
|
||||
|
||||
// Output: CursorDown : "\x1b[B"
|
||||
// CursorLeft : "\x1b[D"
|
||||
// CursorRight: "\x1b[C"
|
||||
// CursorUp : "\x1b[A"
|
||||
// EraseLine : "\x1b[K"
|
||||
}
|
||||
|
||||
func TestCursor_Sprint(t *testing.T) {
|
||||
assert.Equal(t, "\x1b[B", CursorDown.Sprint())
|
||||
assert.Equal(t, "\x1b[D", CursorLeft.Sprint())
|
||||
assert.Equal(t, "\x1b[C", CursorRight.Sprint())
|
||||
assert.Equal(t, "\x1b[A", CursorUp.Sprint())
|
||||
assert.Equal(t, "\x1b[K", EraseLine.Sprint())
|
||||
}
|
||||
|
||||
func ExampleCursor_Sprintn() {
|
||||
fmt.Printf("CursorDown : %#v\n", CursorDown.Sprintn(5))
|
||||
fmt.Printf("CursorLeft : %#v\n", CursorLeft.Sprintn(5))
|
||||
fmt.Printf("CursorRight: %#v\n", CursorRight.Sprintn(5))
|
||||
fmt.Printf("CursorUp : %#v\n", CursorUp.Sprintn(5))
|
||||
fmt.Printf("EraseLine : %#v\n", EraseLine.Sprintn(5))
|
||||
|
||||
// Output: CursorDown : "\x1b[5B"
|
||||
// CursorLeft : "\x1b[5D"
|
||||
// CursorRight: "\x1b[5C"
|
||||
// CursorUp : "\x1b[5A"
|
||||
// EraseLine : "\x1b[K"
|
||||
}
|
||||
|
||||
func TestCursor_Sprintn(t *testing.T) {
|
||||
assert.Equal(t, "\x1b[5B", CursorDown.Sprintn(5))
|
||||
assert.Equal(t, "\x1b[5D", CursorLeft.Sprintn(5))
|
||||
assert.Equal(t, "\x1b[5C", CursorRight.Sprintn(5))
|
||||
assert.Equal(t, "\x1b[5A", CursorUp.Sprintn(5))
|
||||
assert.Equal(t, "\x1b[K", EraseLine.Sprintn(5))
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package text
|
||||
|
||||
// Direction defines the overall flow of text. Similar to bidi.Direction, but
|
||||
// simplified and specific to this package.
|
||||
type Direction int
|
||||
|
||||
// Available Directions.
|
||||
const (
|
||||
Default Direction = iota
|
||||
LeftToRight
|
||||
RightToLeft
|
||||
)
|
||||
|
||||
// Modifier returns a character to force the given direction for the text that
|
||||
// follows the modifier.
|
||||
func (d Direction) Modifier() string {
|
||||
switch d {
|
||||
case LeftToRight:
|
||||
return "\u202a"
|
||||
case RightToLeft:
|
||||
return "\u202b"
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDirection_Modifier(t *testing.T) {
|
||||
assert.Equal(t, "", Default.Modifier())
|
||||
assert.Equal(t, "\u202a", LeftToRight.Modifier())
|
||||
assert.Equal(t, "\u202b", RightToLeft.Modifier())
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package text
|
||||
|
||||
import "strings"
|
||||
|
||||
// Constants
|
||||
const (
|
||||
CSIStartRune = rune(91) // [
|
||||
CSIStopRune = 'm'
|
||||
EscapeReset = EscapeStart + "0" + EscapeStop
|
||||
EscapeStart = "\x1b["
|
||||
EscapeStartRune = rune(27) // \x1b
|
||||
EscapeStop = "m"
|
||||
EscapeStopRune = 'm'
|
||||
OSIStartRune = rune(93) // ]
|
||||
OSIStopRune = '\\'
|
||||
)
|
||||
|
||||
type escKind int
|
||||
|
||||
const (
|
||||
escKindUnknown escKind = iota
|
||||
escKindCSI
|
||||
escKindOSI
|
||||
)
|
||||
|
||||
type escSeq struct {
|
||||
isIn bool
|
||||
content strings.Builder
|
||||
kind escKind
|
||||
}
|
||||
|
||||
func (e *escSeq) InspectRune(r rune) {
|
||||
if !e.isIn && r == EscapeStartRune {
|
||||
e.isIn = true
|
||||
e.kind = escKindUnknown
|
||||
e.content.Reset()
|
||||
e.content.WriteRune(r)
|
||||
} else if e.isIn {
|
||||
switch {
|
||||
case e.kind == escKindUnknown && r == CSIStartRune:
|
||||
e.kind = escKindCSI
|
||||
case e.kind == escKindUnknown && r == OSIStartRune:
|
||||
e.kind = escKindOSI
|
||||
case e.kind == escKindCSI && r == CSIStopRune || e.kind == escKindOSI && r == OSIStopRune:
|
||||
e.isIn = false
|
||||
e.kind = escKindUnknown
|
||||
}
|
||||
e.content.WriteRune(r)
|
||||
}
|
||||
return
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package text
|
||||
|
||||
// Filter filters the slice 's' to items which return truth when passed to 'f'.
|
||||
func Filter(s []string, f func(string) bool) []string {
|
||||
var out []string
|
||||
for _, item := range s {
|
||||
if f(item) {
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleFilter() {
|
||||
slice := []string{"Arya Stark", "Bran Stark", "Jon Snow", "Sansa Stark"}
|
||||
filter := func(item string) bool {
|
||||
return strings.HasSuffix(item, "Stark")
|
||||
}
|
||||
fmt.Printf("%#v\n", Filter(slice, filter))
|
||||
|
||||
// Output: []string{"Arya Stark", "Bran Stark", "Sansa Stark"}
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
slice := []string{"Arya Stark", "Bran Stark", "Jon Snow", "Sansa Stark"}
|
||||
filter := func(item string) bool {
|
||||
return strings.HasSuffix(item, "Stark")
|
||||
}
|
||||
|
||||
filteredSlice := Filter(slice, filter)
|
||||
assert.Equal(t, 3, len(filteredSlice))
|
||||
assert.NotContains(t, filteredSlice, "Jon Snow")
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Format lets you transform the text in supported methods while keeping escape
|
||||
// sequences in the string intact and untouched.
|
||||
type Format int
|
||||
|
||||
// Format enumerations
|
||||
const (
|
||||
FormatDefault Format = iota // default_Case
|
||||
FormatLower // lower
|
||||
FormatTitle // Title
|
||||
FormatUpper // UPPER
|
||||
)
|
||||
|
||||
// Apply converts the text as directed.
|
||||
func (tc Format) Apply(text string) string {
|
||||
switch tc {
|
||||
case FormatLower:
|
||||
return strings.ToLower(text)
|
||||
case FormatTitle:
|
||||
return toTitle(text)
|
||||
case FormatUpper:
|
||||
return toUpper(text)
|
||||
default:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
func toTitle(text string) string {
|
||||
prev, inEscSeq := ' ', false
|
||||
return strings.Map(
|
||||
func(r rune) rune {
|
||||
if r == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
}
|
||||
if !inEscSeq {
|
||||
if isSeparator(prev) {
|
||||
prev = r
|
||||
r = unicode.ToUpper(r)
|
||||
} else {
|
||||
prev = r
|
||||
}
|
||||
}
|
||||
if inEscSeq && r == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
return r
|
||||
},
|
||||
text,
|
||||
)
|
||||
}
|
||||
|
||||
func toUpper(text string) string {
|
||||
inEscSeq := false
|
||||
return strings.Map(
|
||||
func(r rune) rune {
|
||||
if r == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
}
|
||||
if !inEscSeq {
|
||||
r = unicode.ToUpper(r)
|
||||
}
|
||||
if inEscSeq && r == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
return r
|
||||
},
|
||||
text,
|
||||
)
|
||||
}
|
||||
|
||||
// isSeparator returns true if the given rune is a separator. This function is
|
||||
// lifted straight out of the standard library @ strings/strings.go.
|
||||
func isSeparator(r rune) bool {
|
||||
// ASCII alphanumerics and underscore are not separators
|
||||
if r <= 0x7F {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
return false
|
||||
case 'a' <= r && r <= 'z':
|
||||
return false
|
||||
case 'A' <= r && r <= 'Z':
|
||||
return false
|
||||
case r == '_':
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// Letters and digits are not separators
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
return false
|
||||
}
|
||||
// Otherwise, all we can do for now is treat spaces as separators.
|
||||
return unicode.IsSpace(r)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleFormat_Apply() {
|
||||
fmt.Printf("FormatDefault: %#v\n", FormatDefault.Apply("jon Snow"))
|
||||
fmt.Printf("FormatLower : %#v\n", FormatLower.Apply("jon Snow"))
|
||||
fmt.Printf("FormatTitle : %#v\n", FormatTitle.Apply("jon Snow"))
|
||||
fmt.Printf("FormatUpper : %#v\n", FormatUpper.Apply("jon Snow"))
|
||||
fmt.Println()
|
||||
fmt.Printf("FormatDefault (w/EscSeq): %#v\n", FormatDefault.Apply(Bold.Sprint("jon Snow")))
|
||||
fmt.Printf("FormatLower (w/EscSeq): %#v\n", FormatLower.Apply(Bold.Sprint("jon Snow")))
|
||||
fmt.Printf("FormatTitle (w/EscSeq): %#v\n", FormatTitle.Apply(Bold.Sprint("jon Snow")))
|
||||
fmt.Printf("FormatUpper (w/EscSeq): %#v\n", FormatUpper.Apply(Bold.Sprint("jon Snow")))
|
||||
|
||||
// Output: FormatDefault: "jon Snow"
|
||||
// FormatLower : "jon snow"
|
||||
// FormatTitle : "Jon Snow"
|
||||
// FormatUpper : "JON SNOW"
|
||||
//
|
||||
// FormatDefault (w/EscSeq): "\x1b[1mjon Snow\x1b[0m"
|
||||
// FormatLower (w/EscSeq): "\x1b[1mjon snow\x1b[0m"
|
||||
// FormatTitle (w/EscSeq): "\x1b[1mJon Snow\x1b[0m"
|
||||
// FormatUpper (w/EscSeq): "\x1b[1mJON SNOW\x1b[0m"
|
||||
}
|
||||
|
||||
func TestFormat_Apply(t *testing.T) {
|
||||
text := "A big croc0dile; Died - Empty_fanged ツ \u2008."
|
||||
assert.Equal(t, text, FormatDefault.Apply(text))
|
||||
assert.Equal(t, "a big croc0dile; died - empty_fanged ツ \u2008.", FormatLower.Apply(text))
|
||||
assert.Equal(t, "A Big Croc0dile; Died - Empty_fanged ツ \u2008.", FormatTitle.Apply(text))
|
||||
assert.Equal(t, "A BIG CROC0DILE; DIED - EMPTY_FANGED ツ \u2008.", FormatUpper.Apply(text))
|
||||
|
||||
// test with escape sequences
|
||||
text = Colors{Bold}.Sprint(text)
|
||||
assert.Equal(t, "\x1b[1mA big croc0dile; Died - Empty_fanged ツ \u2008.\x1b[0m", FormatDefault.Apply(text))
|
||||
assert.Equal(t, "\x1b[1ma big croc0dile; died - empty_fanged ツ \u2008.\x1b[0m", FormatLower.Apply(text))
|
||||
assert.Equal(t, "\x1b[1mA Big Croc0dile; Died - Empty_fanged ツ \u2008.\x1b[0m", FormatTitle.Apply(text))
|
||||
assert.Equal(t, "\x1b[1mA BIG CROC0DILE; DIED - EMPTY_FANGED ツ \u2008.\x1b[0m", FormatUpper.Apply(text))
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package text
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Hyperlink(url, text string) string {
|
||||
if url == "" {
|
||||
return text
|
||||
}
|
||||
if text == "" {
|
||||
return url
|
||||
}
|
||||
// source https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHyperlink(t *testing.T) {
|
||||
assert.Equal(t, "Ghost", Hyperlink("", "Ghost"))
|
||||
assert.Equal(t, "https://example.com", Hyperlink("https://example.com", ""))
|
||||
assert.Equal(t, "\x1b]8;;https://example.com\x1b\\Ghost\x1b]8;;\x1b\\", Hyperlink("https://example.com", "Ghost"))
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// RuneWidth stuff
|
||||
var (
|
||||
rwCondition = runewidth.NewCondition()
|
||||
)
|
||||
|
||||
// InsertEveryN inserts the rune every N characters in the string. For ex.:
|
||||
// InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t"
|
||||
// InsertEveryN("Ghost", '-', 2) == "Gh-os-t"
|
||||
// InsertEveryN("Ghost", '-', 3) == "Gho-st"
|
||||
// InsertEveryN("Ghost", '-', 4) == "Ghos-t"
|
||||
// InsertEveryN("Ghost", '-', 5) == "Ghost"
|
||||
func InsertEveryN(str string, runeToInsert rune, n int) string {
|
||||
if n <= 0 {
|
||||
return str
|
||||
}
|
||||
|
||||
sLen := RuneWidthWithoutEscSequences(str)
|
||||
var out strings.Builder
|
||||
out.Grow(sLen + (sLen / n))
|
||||
outLen, eSeq := 0, escSeq{}
|
||||
for idx, c := range str {
|
||||
if eSeq.isIn {
|
||||
eSeq.InspectRune(c)
|
||||
out.WriteRune(c)
|
||||
continue
|
||||
}
|
||||
eSeq.InspectRune(c)
|
||||
if !eSeq.isIn && outLen > 0 && (outLen%n) == 0 && idx != sLen {
|
||||
out.WriteRune(runeToInsert)
|
||||
}
|
||||
out.WriteRune(c)
|
||||
if !eSeq.isIn {
|
||||
outLen += RuneWidth(c)
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// LongestLineLen returns the length of the longest "line" within the
|
||||
// argument string. For ex.:
|
||||
// LongestLineLen("Ghost!\nCome back here!\nRight now!") == 15
|
||||
func LongestLineLen(str string) int {
|
||||
maxLength, currLength, eSeq := 0, 0, escSeq{}
|
||||
for _, c := range str {
|
||||
if eSeq.isIn {
|
||||
eSeq.InspectRune(c)
|
||||
continue
|
||||
}
|
||||
eSeq.InspectRune(c)
|
||||
if c == '\n' {
|
||||
if currLength > maxLength {
|
||||
maxLength = currLength
|
||||
}
|
||||
currLength = 0
|
||||
} else if !eSeq.isIn {
|
||||
currLength += RuneWidth(c)
|
||||
}
|
||||
}
|
||||
if currLength > maxLength {
|
||||
maxLength = currLength
|
||||
}
|
||||
return maxLength
|
||||
}
|
||||
|
||||
// OverrideRuneWidthEastAsianWidth can *probably* help with alignment, and
|
||||
// length calculation issues when dealing with Unicode character-set and a
|
||||
// non-English language set in the LANG variable.
|
||||
//
|
||||
// Set this to 'false' to force the "runewidth" library to pretend to deal with
|
||||
// English character-set. Be warned that if the text/content you are dealing
|
||||
// with contains East Asian character-set, this may result in unexpected
|
||||
// behavior.
|
||||
//
|
||||
// References:
|
||||
// * https://github.com/mattn/go-runewidth/issues/64#issuecomment-1221642154
|
||||
// * https://github.com/jedib0t/go-pretty/issues/220
|
||||
// * https://github.com/jedib0t/go-pretty/issues/204
|
||||
func OverrideRuneWidthEastAsianWidth(val bool) {
|
||||
rwCondition.EastAsianWidth = val
|
||||
}
|
||||
|
||||
// Pad pads the given string with as many characters as needed to make it as
|
||||
// long as specified (maxLen). This function does not count escape sequences
|
||||
// while calculating length of the string. Ex.:
|
||||
// Pad("Ghost", 0, ' ') == "Ghost"
|
||||
// Pad("Ghost", 3, ' ') == "Ghost"
|
||||
// Pad("Ghost", 5, ' ') == "Ghost"
|
||||
// Pad("Ghost", 7, ' ') == "Ghost "
|
||||
// Pad("Ghost", 10, '.') == "Ghost....."
|
||||
func Pad(str string, maxLen int, paddingChar rune) string {
|
||||
strLen := RuneWidthWithoutEscSequences(str)
|
||||
if strLen < maxLen {
|
||||
str += strings.Repeat(string(paddingChar), maxLen-strLen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// RepeatAndTrim repeats the given string until it is as long as maxRunes.
|
||||
// For ex.:
|
||||
// RepeatAndTrim("", 5) == ""
|
||||
// RepeatAndTrim("Ghost", 0) == ""
|
||||
// RepeatAndTrim("Ghost", 5) == "Ghost"
|
||||
// RepeatAndTrim("Ghost", 7) == "GhostGh"
|
||||
// RepeatAndTrim("Ghost", 10) == "GhostGhost"
|
||||
func RepeatAndTrim(str string, maxRunes int) string {
|
||||
if str == "" || maxRunes == 0 {
|
||||
return ""
|
||||
} else if maxRunes == utf8.RuneCountInString(str) {
|
||||
return str
|
||||
}
|
||||
repeatedS := strings.Repeat(str, int(maxRunes/utf8.RuneCountInString(str))+1)
|
||||
return Trim(repeatedS, maxRunes)
|
||||
}
|
||||
|
||||
// RuneCount is similar to utf8.RuneCountInString, except for the fact that it
|
||||
// ignores escape sequences while counting. For ex.:
|
||||
// RuneCount("") == 0
|
||||
// RuneCount("Ghost") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0") == 5
|
||||
// Deprecated: in favor of RuneWidthWithoutEscSequences
|
||||
func RuneCount(str string) int {
|
||||
return RuneWidthWithoutEscSequences(str)
|
||||
}
|
||||
|
||||
// RuneWidth returns the mostly accurate character-width of the rune. This is
|
||||
// not 100% accurate as the character width is usually dependent on the
|
||||
// typeface (font) used in the console/terminal. For ex.:
|
||||
// RuneWidth('A') == 1
|
||||
// RuneWidth('ツ') == 2
|
||||
// RuneWidth('⊙') == 1
|
||||
// RuneWidth('︿') == 2
|
||||
// RuneWidth(0x27) == 0
|
||||
func RuneWidth(r rune) int {
|
||||
return rwCondition.RuneWidth(r)
|
||||
}
|
||||
|
||||
// RuneWidthWithoutEscSequences is similar to RuneWidth, except for the fact
|
||||
// that it ignores escape sequences while counting. For ex.:
|
||||
// RuneWidthWithoutEscSequences("") == 0
|
||||
// RuneWidthWithoutEscSequences("Ghost") == 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m") == 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0") == 5
|
||||
func RuneWidthWithoutEscSequences(str string) int {
|
||||
count, eSeq := 0, escSeq{}
|
||||
for _, c := range str {
|
||||
if eSeq.isIn {
|
||||
eSeq.InspectRune(c)
|
||||
continue
|
||||
}
|
||||
eSeq.InspectRune(c)
|
||||
if !eSeq.isIn {
|
||||
count += RuneWidth(c)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Snip returns the given string with a fixed length. For ex.:
|
||||
// Snip("Ghost", 0, "~") == "Ghost"
|
||||
// Snip("Ghost", 1, "~") == "~"
|
||||
// Snip("Ghost", 3, "~") == "Gh~"
|
||||
// Snip("Ghost", 5, "~") == "Ghost"
|
||||
// Snip("Ghost", 7, "~") == "Ghost "
|
||||
// Snip("\x1b[33mGhost\x1b[0m", 7, "~") == "\x1b[33mGhost\x1b[0m "
|
||||
func Snip(str string, length int, snipIndicator string) string {
|
||||
if length > 0 {
|
||||
lenStr := RuneWidthWithoutEscSequences(str)
|
||||
if lenStr > length {
|
||||
lenStrFinal := length - RuneWidthWithoutEscSequences(snipIndicator)
|
||||
return Trim(str, lenStrFinal) + snipIndicator
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Trim trims a string to the given length while ignoring escape sequences. For
|
||||
// ex.:
|
||||
// Trim("Ghost", 3) == "Gho"
|
||||
// Trim("Ghost", 6) == "Ghost"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 3) == "\x1b[33mGho\x1b[0m"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 6) == "\x1b[33mGhost\x1b[0m"
|
||||
func Trim(str string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(maxLen)
|
||||
|
||||
outLen, eSeq := 0, escSeq{}
|
||||
for _, sChr := range str {
|
||||
if eSeq.isIn {
|
||||
eSeq.InspectRune(sChr)
|
||||
out.WriteRune(sChr)
|
||||
continue
|
||||
}
|
||||
eSeq.InspectRune(sChr)
|
||||
if eSeq.isIn {
|
||||
out.WriteRune(sChr)
|
||||
continue
|
||||
}
|
||||
if outLen < maxLen {
|
||||
outLen++
|
||||
out.WriteRune(sChr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
@ -1,282 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleInsertEveryN() {
|
||||
fmt.Printf("InsertEveryN(\"Ghost\", '-', 0): %#v\n", InsertEveryN("Ghost", '-', 0))
|
||||
fmt.Printf("InsertEveryN(\"Ghost\", '-', 1): %#v\n", InsertEveryN("Ghost", '-', 1))
|
||||
fmt.Printf("InsertEveryN(\"Ghost\", '-', 2): %#v\n", InsertEveryN("Ghost", '-', 2))
|
||||
fmt.Printf("InsertEveryN(\"Ghost\", '-', 3): %#v\n", InsertEveryN("Ghost", '-', 3))
|
||||
fmt.Printf("InsertEveryN(\"Ghost\", '-', 4): %#v\n", InsertEveryN("Ghost", '-', 4))
|
||||
fmt.Printf("InsertEveryN(\"Ghost\", '-', 5): %#v\n", InsertEveryN("Ghost", '-', 5))
|
||||
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 0): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0))
|
||||
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 1): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1))
|
||||
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 2): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2))
|
||||
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 3): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3))
|
||||
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 4): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4))
|
||||
fmt.Printf("InsertEveryN(\"\\x1b[33mGhost\\x1b[0m\", '-', 5): %#v\n", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5))
|
||||
|
||||
// Output: InsertEveryN("Ghost", '-', 0): "Ghost"
|
||||
// InsertEveryN("Ghost", '-', 1): "G-h-o-s-t"
|
||||
// InsertEveryN("Ghost", '-', 2): "Gh-os-t"
|
||||
// InsertEveryN("Ghost", '-', 3): "Gho-st"
|
||||
// InsertEveryN("Ghost", '-', 4): "Ghos-t"
|
||||
// InsertEveryN("Ghost", '-', 5): "Ghost"
|
||||
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0): "\x1b[33mGhost\x1b[0m"
|
||||
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1): "\x1b[33mG-h-o-s-t\x1b[0m"
|
||||
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2): "\x1b[33mGh-os-t\x1b[0m"
|
||||
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3): "\x1b[33mGho-st\x1b[0m"
|
||||
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4): "\x1b[33mGhos-t\x1b[0m"
|
||||
// InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5): "\x1b[33mGhost\x1b[0m"
|
||||
}
|
||||
|
||||
func TestInsertEveryN(t *testing.T) {
|
||||
assert.Equal(t, "Ghost", InsertEveryN("Ghost", '-', 0))
|
||||
assert.Equal(t, "Gツhツoツsツt", InsertEveryN("Ghost", 'ツ', 1))
|
||||
assert.Equal(t, "G-h-o-s-t", InsertEveryN("Ghost", '-', 1))
|
||||
assert.Equal(t, "Gh-os-t", InsertEveryN("Ghost", '-', 2))
|
||||
assert.Equal(t, "Gho-st", InsertEveryN("Ghost", '-', 3))
|
||||
assert.Equal(t, "Ghos-t", InsertEveryN("Ghost", '-', 4))
|
||||
assert.Equal(t, "Ghost", InsertEveryN("Ghost", '-', 5))
|
||||
assert.Equal(t, "\x1b[33mGhost\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 0))
|
||||
assert.Equal(t, "\x1b[33mGツhツoツsツt\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", 'ツ', 1))
|
||||
assert.Equal(t, "\x1b[33mG-h-o-s-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 1))
|
||||
assert.Equal(t, "\x1b[33mGh-os-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 2))
|
||||
assert.Equal(t, "\x1b[33mGho-st\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 3))
|
||||
assert.Equal(t, "\x1b[33mGhos-t\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 4))
|
||||
assert.Equal(t, "\x1b[33mGhost\x1b[0m", InsertEveryN("\x1b[33mGhost\x1b[0m", '-', 5))
|
||||
assert.Equal(t, "G\x1b]8;;http://example.com\x1b\\-h-o-s-t\x1b]8;;\x1b\\", InsertEveryN("G\x1b]8;;http://example.com\x1b\\host\x1b]8;;\x1b\\", '-', 1))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\G-h-o-s-t\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", '-', 1))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\G-h-o-s\x1b]8;;\x1b\\-t", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghos\x1b]8;;\x1b\\t", '-', 1))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Gツhツoツsツt\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 'ツ', 1))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghツosツt\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 'ツ', 2))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", InsertEveryN("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", '-', 5))
|
||||
}
|
||||
|
||||
func ExampleLongestLineLen() {
|
||||
fmt.Printf("LongestLineLen(\"\"): %d\n", LongestLineLen(""))
|
||||
fmt.Printf("LongestLineLen(\"\\n\\n\"): %d\n", LongestLineLen("\n\n"))
|
||||
fmt.Printf("LongestLineLen(\"Ghost\"): %d\n", LongestLineLen("Ghost"))
|
||||
fmt.Printf("LongestLineLen(\"Ghostツ\"): %d\n", LongestLineLen("Ghostツ"))
|
||||
fmt.Printf("LongestLineLen(\"Winter\\nIs\\nComing\"): %d\n", LongestLineLen("Winter\nIs\nComing"))
|
||||
fmt.Printf("LongestLineLen(\"Mother\\nOf\\nDragons\"): %d\n", LongestLineLen("Mother\nOf\nDragons"))
|
||||
fmt.Printf("LongestLineLen(\"\\x1b[33mMother\\x1b[0m\\nOf\\nDragons\"): %d\n", LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"))
|
||||
|
||||
// Output: LongestLineLen(""): 0
|
||||
// LongestLineLen("\n\n"): 0
|
||||
// LongestLineLen("Ghost"): 5
|
||||
// LongestLineLen("Ghostツ"): 7
|
||||
// LongestLineLen("Winter\nIs\nComing"): 6
|
||||
// LongestLineLen("Mother\nOf\nDragons"): 7
|
||||
// LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"): 7
|
||||
}
|
||||
|
||||
func TestLongestLineLen(t *testing.T) {
|
||||
assert.Equal(t, 0, LongestLineLen(""))
|
||||
assert.Equal(t, 0, LongestLineLen("\n\n"))
|
||||
assert.Equal(t, 5, LongestLineLen("Ghost"))
|
||||
assert.Equal(t, 7, LongestLineLen("Ghostツ"))
|
||||
assert.Equal(t, 6, LongestLineLen("Winter\nIs\nComing"))
|
||||
assert.Equal(t, 7, LongestLineLen("Mother\nOf\nDragons"))
|
||||
assert.Equal(t, 7, LongestLineLen("\x1b[33mMother\x1b[0m\nOf\nDragons"))
|
||||
assert.Equal(t, 7, LongestLineLen("Mother\nOf\n\x1b]8;;http://example.com\x1b\\Dragons\x1b]8;;\x1b\\"))
|
||||
}
|
||||
|
||||
func TestOverrideRuneWidthEastAsianWidth(t *testing.T) {
|
||||
originalValue := rwCondition.EastAsianWidth
|
||||
defer func() {
|
||||
rwCondition.EastAsianWidth = originalValue
|
||||
}()
|
||||
|
||||
OverrideRuneWidthEastAsianWidth(true)
|
||||
assert.Equal(t, 2, RuneWidthWithoutEscSequences("╋"))
|
||||
OverrideRuneWidthEastAsianWidth(false)
|
||||
assert.Equal(t, 1, RuneWidthWithoutEscSequences("╋"))
|
||||
|
||||
// Note for posterity. We want the length of the box drawing character to
|
||||
// be reported as 1. However, with an environment where LANG is set to
|
||||
// something like 'zh_CN.UTF-8', the value being returned is 2, which breaks
|
||||
// text alignment/padding logic in this library.
|
||||
//
|
||||
// If a future version of runewidth is able to address this internally and
|
||||
// return 1 for the above, the function being tested can be marked for
|
||||
// deprecation.
|
||||
}
|
||||
|
||||
func ExamplePad() {
|
||||
fmt.Printf("%#v\n", Pad("Ghost", 0, ' '))
|
||||
fmt.Printf("%#v\n", Pad("Ghost", 3, ' '))
|
||||
fmt.Printf("%#v\n", Pad("Ghost", 5, ' '))
|
||||
fmt.Printf("%#v\n", Pad("\x1b[33mGhost\x1b[0m", 7, '-'))
|
||||
fmt.Printf("%#v\n", Pad("\x1b[33mGhost\x1b[0m", 10, '.'))
|
||||
|
||||
// Output: "Ghost"
|
||||
// "Ghost"
|
||||
// "Ghost"
|
||||
// "\x1b[33mGhost\x1b[0m--"
|
||||
// "\x1b[33mGhost\x1b[0m....."
|
||||
}
|
||||
|
||||
func TestPad(t *testing.T) {
|
||||
assert.Equal(t, "Ghost", Pad("Ghost", 0, ' '))
|
||||
assert.Equal(t, "Ghost", Pad("Ghost", 3, ' '))
|
||||
assert.Equal(t, "Ghost", Pad("Ghost", 5, ' '))
|
||||
assert.Equal(t, "Ghost ", Pad("Ghost", 7, ' '))
|
||||
assert.Equal(t, "Ghost.....", Pad("Ghost", 10, '.'))
|
||||
assert.Equal(t, "\x1b[33mGhost\x1b[0 ", Pad("\x1b[33mGhost\x1b[0", 7, ' '))
|
||||
assert.Equal(t, "\x1b[33mGhost\x1b[0.....", Pad("\x1b[33mGhost\x1b[0", 10, '.'))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\ ", Pad("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 7, ' '))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\.....", Pad("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 10, '.'))
|
||||
}
|
||||
|
||||
func ExampleRepeatAndTrim() {
|
||||
fmt.Printf("RepeatAndTrim(\"\", 5): %#v\n", RepeatAndTrim("", 5))
|
||||
fmt.Printf("RepeatAndTrim(\"Ghost\", 0): %#v\n", RepeatAndTrim("Ghost", 0))
|
||||
fmt.Printf("RepeatAndTrim(\"Ghost\", 3): %#v\n", RepeatAndTrim("Ghost", 3))
|
||||
fmt.Printf("RepeatAndTrim(\"Ghost\", 5): %#v\n", RepeatAndTrim("Ghost", 5))
|
||||
fmt.Printf("RepeatAndTrim(\"Ghost\", 7): %#v\n", RepeatAndTrim("Ghost", 7))
|
||||
fmt.Printf("RepeatAndTrim(\"Ghost\", 10): %#v\n", RepeatAndTrim("Ghost", 10))
|
||||
|
||||
// Output: RepeatAndTrim("", 5): ""
|
||||
// RepeatAndTrim("Ghost", 0): ""
|
||||
// RepeatAndTrim("Ghost", 3): "Gho"
|
||||
// RepeatAndTrim("Ghost", 5): "Ghost"
|
||||
// RepeatAndTrim("Ghost", 7): "GhostGh"
|
||||
// RepeatAndTrim("Ghost", 10): "GhostGhost"
|
||||
}
|
||||
|
||||
func TestRepeatAndTrim(t *testing.T) {
|
||||
assert.Equal(t, "", RepeatAndTrim("", 5))
|
||||
assert.Equal(t, "", RepeatAndTrim("Ghost", 0))
|
||||
assert.Equal(t, "Gho", RepeatAndTrim("Ghost", 3))
|
||||
assert.Equal(t, "Ghost", RepeatAndTrim("Ghost", 5))
|
||||
assert.Equal(t, "GhostGh", RepeatAndTrim("Ghost", 7))
|
||||
assert.Equal(t, "GhostGhost", RepeatAndTrim("Ghost", 10))
|
||||
assert.Equal(t, "───", RepeatAndTrim("─", 3))
|
||||
}
|
||||
|
||||
func ExampleRuneCount() {
|
||||
fmt.Printf("RuneCount(\"\"): %d\n", RuneCount(""))
|
||||
fmt.Printf("RuneCount(\"Ghost\"): %d\n", RuneCount("Ghost"))
|
||||
fmt.Printf("RuneCount(\"Ghostツ\"): %d\n", RuneCount("Ghostツ"))
|
||||
fmt.Printf("RuneCount(\"\\x1b[33mGhost\\x1b[0m\"): %d\n", RuneCount("\x1b[33mGhost\x1b[0m"))
|
||||
fmt.Printf("RuneCount(\"\\x1b[33mGhost\\x1b[0\"): %d\n", RuneCount("\x1b[33mGhost\x1b[0"))
|
||||
|
||||
// Output: RuneCount(""): 0
|
||||
// RuneCount("Ghost"): 5
|
||||
// RuneCount("Ghostツ"): 7
|
||||
// RuneCount("\x1b[33mGhost\x1b[0m"): 5
|
||||
// RuneCount("\x1b[33mGhost\x1b[0"): 5
|
||||
}
|
||||
|
||||
func TestRuneCount(t *testing.T) {
|
||||
assert.Equal(t, 0, RuneCount(""))
|
||||
assert.Equal(t, 5, RuneCount("Ghost"))
|
||||
assert.Equal(t, 7, RuneCount("Ghostツ"))
|
||||
assert.Equal(t, 5, RuneCount("\x1b[33mGhost\x1b[0m"))
|
||||
assert.Equal(t, 5, RuneCount("\x1b[33mGhost\x1b[0"))
|
||||
assert.Equal(t, 5, RuneCount("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\"))
|
||||
}
|
||||
|
||||
func ExampleRuneWidth() {
|
||||
fmt.Printf("RuneWidth('A'): %d\n", RuneWidth('A'))
|
||||
fmt.Printf("RuneWidth('ツ'): %d\n", RuneWidth('ツ'))
|
||||
fmt.Printf("RuneWidth('⊙'): %d\n", RuneWidth('⊙'))
|
||||
fmt.Printf("RuneWidth('︿'): %d\n", RuneWidth('︿'))
|
||||
fmt.Printf("RuneWidth(rune(27)): %d\n", RuneWidth(rune(27))) // ANSI escape sequence
|
||||
|
||||
// Output: RuneWidth('A'): 1
|
||||
// RuneWidth('ツ'): 2
|
||||
// RuneWidth('⊙'): 1
|
||||
// RuneWidth('︿'): 2
|
||||
// RuneWidth(rune(27)): 0
|
||||
}
|
||||
|
||||
func TestRuneWidth(t *testing.T) {
|
||||
assert.Equal(t, 1, RuneWidth('A'))
|
||||
assert.Equal(t, 2, RuneWidth('ツ'))
|
||||
assert.Equal(t, 1, RuneWidth('⊙'))
|
||||
assert.Equal(t, 2, RuneWidth('︿'))
|
||||
assert.Equal(t, 0, RuneWidth(rune(27))) // ANSI escape sequence
|
||||
}
|
||||
|
||||
func ExampleRuneWidthWithoutEscSequences() {
|
||||
fmt.Printf("RuneWidthWithoutEscSequences(\"\"): %d\n", RuneWidthWithoutEscSequences(""))
|
||||
fmt.Printf("RuneWidthWithoutEscSequences(\"Ghost\"): %d\n", RuneWidthWithoutEscSequences("Ghost"))
|
||||
fmt.Printf("RuneWidthWithoutEscSequences(\"Ghostツ\"): %d\n", RuneWidthWithoutEscSequences("Ghostツ"))
|
||||
fmt.Printf("RuneWidthWithoutEscSequences(\"\\x1b[33mGhost\\x1b[0m\"): %d\n", RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"))
|
||||
fmt.Printf("RuneWidthWithoutEscSequences(\"\\x1b[33mGhost\\x1b[0\"): %d\n", RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"))
|
||||
|
||||
// Output: RuneWidthWithoutEscSequences(""): 0
|
||||
// RuneWidthWithoutEscSequences("Ghost"): 5
|
||||
// RuneWidthWithoutEscSequences("Ghostツ"): 7
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"): 5
|
||||
// RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"): 5
|
||||
}
|
||||
|
||||
func TestRuneWidthWithoutEscSequences(t *testing.T) {
|
||||
assert.Equal(t, 0, RuneWidthWithoutEscSequences(""))
|
||||
assert.Equal(t, 5, RuneWidthWithoutEscSequences("Ghost"))
|
||||
assert.Equal(t, 7, RuneWidthWithoutEscSequences("Ghostツ"))
|
||||
assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0m"))
|
||||
assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b[33mGhost\x1b[0"))
|
||||
assert.Equal(t, 5, RuneWidthWithoutEscSequences("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\"))
|
||||
}
|
||||
|
||||
func ExampleSnip() {
|
||||
fmt.Printf("Snip(\"Ghost\", 0, \"~\"): %#v\n", Snip("Ghost", 0, "~"))
|
||||
fmt.Printf("Snip(\"Ghost\", 1, \"~\"): %#v\n", Snip("Ghost", 1, "~"))
|
||||
fmt.Printf("Snip(\"Ghost\", 3, \"~\"): %#v\n", Snip("Ghost", 3, "~"))
|
||||
fmt.Printf("Snip(\"Ghost\", 5, \"~\"): %#v\n", Snip("Ghost", 5, "~"))
|
||||
fmt.Printf("Snip(\"Ghost\", 7, \"~\"): %#v\n", Snip("Ghost", 7, "~"))
|
||||
fmt.Printf("Snip(\"\\x1b[33mGhost\\x1b[0m\", 7, \"~\"): %#v\n", Snip("\x1b[33mGhost\x1b[0m", 7, "~"))
|
||||
|
||||
// Output: Snip("Ghost", 0, "~"): "Ghost"
|
||||
// Snip("Ghost", 1, "~"): "~"
|
||||
// Snip("Ghost", 3, "~"): "Gh~"
|
||||
// Snip("Ghost", 5, "~"): "Ghost"
|
||||
// Snip("Ghost", 7, "~"): "Ghost"
|
||||
// Snip("\x1b[33mGhost\x1b[0m", 7, "~"): "\x1b[33mGhost\x1b[0m"
|
||||
}
|
||||
|
||||
func TestSnip(t *testing.T) {
|
||||
assert.Equal(t, "Ghost", Snip("Ghost", 0, "~"))
|
||||
assert.Equal(t, "~", Snip("Ghost", 1, "~"))
|
||||
assert.Equal(t, "Gh~", Snip("Ghost", 3, "~"))
|
||||
assert.Equal(t, "Ghost", Snip("Ghost", 5, "~"))
|
||||
assert.Equal(t, "Ghost", Snip("Ghost", 7, "~"))
|
||||
assert.Equal(t, "\x1b[33mGhost\x1b[0m", Snip("\x1b[33mGhost\x1b[0m", 7, "~"))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", Snip("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 7, "~"))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Gh\x1b]8;;\x1b\\~", Snip("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 3, "~"))
|
||||
assert.Equal(t, "\x1b[33m\x1b]8;;http://example.com\x1b\\Gh\x1b]8;;\x1b\\\x1b[0m~", Snip("\x1b[33m\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\\x1b[0m", 3, "~"))
|
||||
}
|
||||
|
||||
func ExampleTrim() {
|
||||
fmt.Printf("Trim(\"Ghost\", 0): %#v\n", Trim("Ghost", 0))
|
||||
fmt.Printf("Trim(\"Ghost\", 3): %#v\n", Trim("Ghost", 3))
|
||||
fmt.Printf("Trim(\"Ghost\", 6): %#v\n", Trim("Ghost", 6))
|
||||
fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 0): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 0))
|
||||
fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 3): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 3))
|
||||
fmt.Printf("Trim(\"\\x1b[33mGhost\\x1b[0m\", 6): %#v\n", Trim("\x1b[33mGhost\x1b[0m", 6))
|
||||
|
||||
// Output: Trim("Ghost", 0): ""
|
||||
// Trim("Ghost", 3): "Gho"
|
||||
// Trim("Ghost", 6): "Ghost"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 0): ""
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 3): "\x1b[33mGho\x1b[0m"
|
||||
// Trim("\x1b[33mGhost\x1b[0m", 6): "\x1b[33mGhost\x1b[0m"
|
||||
}
|
||||
|
||||
func TestTrim(t *testing.T) {
|
||||
assert.Equal(t, "", Trim("Ghost", 0))
|
||||
assert.Equal(t, "Gho", Trim("Ghost", 3))
|
||||
assert.Equal(t, "Ghost", Trim("Ghost", 6))
|
||||
assert.Equal(t, "\x1b[33mGho\x1b[0m", Trim("\x1b[33mGhost\x1b[0m", 3))
|
||||
assert.Equal(t, "\x1b[33mGhost\x1b[0m", Trim("\x1b[33mGhost\x1b[0m", 6))
|
||||
assert.Equal(t, "\x1b]8;;http://example.com\x1b\\Gho\x1b]8;;\x1b\\", Trim("\x1b]8;;http://example.com\x1b\\Ghost\x1b]8;;\x1b\\", 3))
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Transformer related constants
|
||||
const (
|
||||
unixTimeMinMilliseconds = int64(10000000000)
|
||||
unixTimeMinMicroseconds = unixTimeMinMilliseconds * 1000
|
||||
unixTimeMinNanoSeconds = unixTimeMinMicroseconds * 1000
|
||||
)
|
||||
|
||||
// Transformer related variables
|
||||
var (
|
||||
colorsNumberPositive = Colors{FgHiGreen}
|
||||
colorsNumberNegative = Colors{FgHiRed}
|
||||
colorsNumberZero = Colors{}
|
||||
colorsURL = Colors{Underline, FgBlue}
|
||||
rfc3339Milli = "2006-01-02T15:04:05.000Z07:00"
|
||||
rfc3339Micro = "2006-01-02T15:04:05.000000Z07:00"
|
||||
|
||||
possibleTimeLayouts = []string{
|
||||
time.RFC3339,
|
||||
rfc3339Milli, // strfmt.DateTime.String()'s default layout
|
||||
rfc3339Micro,
|
||||
time.RFC3339Nano,
|
||||
}
|
||||
)
|
||||
|
||||
// Transformer helps format the contents of an object to the user's liking.
|
||||
type Transformer func(val interface{}) string
|
||||
|
||||
// NewNumberTransformer returns a number Transformer that:
|
||||
// * transforms the number as directed by 'format' (ex.: %.2f)
|
||||
// * colors negative values Red
|
||||
// * colors positive values Green
|
||||
func NewNumberTransformer(format string) Transformer {
|
||||
return func(val interface{}) string {
|
||||
if valStr := transformInt(format, val); valStr != "" {
|
||||
return valStr
|
||||
}
|
||||
if valStr := transformUint(format, val); valStr != "" {
|
||||
return valStr
|
||||
}
|
||||
if valStr := transformFloat(format, val); valStr != "" {
|
||||
return valStr
|
||||
}
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
func transformInt(format string, val interface{}) string {
|
||||
transform := func(val int64) string {
|
||||
if val < 0 {
|
||||
return colorsNumberNegative.Sprintf("-"+format, -val)
|
||||
}
|
||||
if val > 0 {
|
||||
return colorsNumberPositive.Sprintf(format, val)
|
||||
}
|
||||
return colorsNumberZero.Sprintf(format, val)
|
||||
}
|
||||
|
||||
if number, ok := val.(int); ok {
|
||||
return transform(int64(number))
|
||||
}
|
||||
if number, ok := val.(int8); ok {
|
||||
return transform(int64(number))
|
||||
}
|
||||
if number, ok := val.(int16); ok {
|
||||
return transform(int64(number))
|
||||
}
|
||||
if number, ok := val.(int32); ok {
|
||||
return transform(int64(number))
|
||||
}
|
||||
if number, ok := val.(int64); ok {
|
||||
return transform(int64(number))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func transformUint(format string, val interface{}) string {
|
||||
transform := func(val uint64) string {
|
||||
if val > 0 {
|
||||
return colorsNumberPositive.Sprintf(format, val)
|
||||
}
|
||||
return colorsNumberZero.Sprintf(format, val)
|
||||
}
|
||||
|
||||
if number, ok := val.(uint); ok {
|
||||
return transform(uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint8); ok {
|
||||
return transform(uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint16); ok {
|
||||
return transform(uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint32); ok {
|
||||
return transform(uint64(number))
|
||||
}
|
||||
if number, ok := val.(uint64); ok {
|
||||
return transform(uint64(number))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func transformFloat(format string, val interface{}) string {
|
||||
transform := func(val float64) string {
|
||||
if val < 0 {
|
||||
return colorsNumberNegative.Sprintf("-"+format, -val)
|
||||
}
|
||||
if val > 0 {
|
||||
return colorsNumberPositive.Sprintf(format, val)
|
||||
}
|
||||
return colorsNumberZero.Sprintf(format, val)
|
||||
}
|
||||
|
||||
if number, ok := val.(float32); ok {
|
||||
return transform(float64(number))
|
||||
}
|
||||
if number, ok := val.(float64); ok {
|
||||
return transform(float64(number))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NewJSONTransformer returns a Transformer that can format a JSON string or an
|
||||
// object into pretty-indented JSON-strings.
|
||||
func NewJSONTransformer(prefix string, indent string) Transformer {
|
||||
return func(val interface{}) string {
|
||||
if valStr, ok := val.(string); ok {
|
||||
var b bytes.Buffer
|
||||
if err := json.Indent(&b, []byte(strings.TrimSpace(valStr)), prefix, indent); err == nil {
|
||||
return string(b.Bytes())
|
||||
}
|
||||
} else if b, err := json.MarshalIndent(val, prefix, indent); err == nil {
|
||||
return string(b)
|
||||
}
|
||||
return fmt.Sprintf("%#v", val)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTimeTransformer returns a Transformer that can format a timestamp (a
|
||||
// time.Time) into a well-defined time format defined using the provided layout
|
||||
// (ex.: time.RFC3339).
|
||||
//
|
||||
// If a non-nil location value is provided, the time will be localized to that
|
||||
// location (use time.Local to get localized timestamps).
|
||||
func NewTimeTransformer(layout string, location *time.Location) Transformer {
|
||||
return func(val interface{}) string {
|
||||
rsp := fmt.Sprint(val)
|
||||
if valTime, ok := val.(time.Time); ok {
|
||||
rsp = formatTime(valTime, layout, location)
|
||||
} else {
|
||||
// cycle through some supported layouts to see if the string form
|
||||
// of the object matches any of these layouts
|
||||
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||
if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil {
|
||||
rsp = formatTime(valTime, layout, location)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnixTimeTransformer returns a Transformer that can format a unix-timestamp
|
||||
// into a well-defined time format as defined by 'layout'. This can handle
|
||||
// unix-time in Seconds, MilliSeconds, Microseconds and Nanoseconds.
|
||||
//
|
||||
// If a non-nil location value is provided, the time will be localized to that
|
||||
// location (use time.Local to get localized timestamps).
|
||||
func NewUnixTimeTransformer(layout string, location *time.Location) Transformer {
|
||||
transformer := NewTimeTransformer(layout, location)
|
||||
|
||||
return func(val interface{}) string {
|
||||
if unixTime, ok := val.(int64); ok {
|
||||
return formatTimeUnix(unixTime, transformer)
|
||||
} else if unixTimeStr, ok := val.(string); ok {
|
||||
if unixTime, err := strconv.ParseInt(unixTimeStr, 10, 64); err == nil {
|
||||
return formatTimeUnix(unixTime, transformer)
|
||||
}
|
||||
}
|
||||
return fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
// NewURLTransformer returns a Transformer that can format and pretty print a string
|
||||
// that contains a URL (the text is underlined and colored Blue).
|
||||
func NewURLTransformer(colors ...Color) Transformer {
|
||||
colorsToUse := colorsURL
|
||||
if len(colors) > 0 {
|
||||
colorsToUse = colors
|
||||
}
|
||||
|
||||
return func(val interface{}) string {
|
||||
return colorsToUse.Sprint(val)
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(t time.Time, layout string, location *time.Location) string {
|
||||
rsp := ""
|
||||
if t.Unix() > 0 {
|
||||
if location != nil {
|
||||
t = t.In(location)
|
||||
}
|
||||
rsp = t.Format(layout)
|
||||
}
|
||||
return rsp
|
||||
}
|
||||
|
||||
func formatTimeUnix(unixTime int64, timeTransformer Transformer) string {
|
||||
if unixTime >= unixTimeMinNanoSeconds {
|
||||
unixTime = unixTime / time.Second.Nanoseconds()
|
||||
} else if unixTime >= unixTimeMinMicroseconds {
|
||||
unixTime = unixTime / (time.Second.Nanoseconds() / 1000)
|
||||
} else if unixTime >= unixTimeMinMilliseconds {
|
||||
unixTime = unixTime / (time.Second.Nanoseconds() / 1000000)
|
||||
}
|
||||
return timeTransformer(time.Unix(unixTime, 0))
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewNumberTransformer(t *testing.T) {
|
||||
signColorsMap := map[string]Colors{
|
||||
"negative": colorsNumberNegative,
|
||||
"positive": colorsNumberPositive,
|
||||
"zero": nil,
|
||||
"nil": nil,
|
||||
}
|
||||
colorValuesMap := map[string]map[interface{}]string{
|
||||
"negative": {
|
||||
int(-5): "%05d",
|
||||
int8(-5): "%05d",
|
||||
int16(-5): "%05d",
|
||||
int32(-5): "%05d",
|
||||
int64(-5): "%05d",
|
||||
float32(-5.55555): "%08.2f",
|
||||
float64(-5.55555): "%08.2f",
|
||||
},
|
||||
"positive": {
|
||||
int(5): "%05d",
|
||||
int8(5): "%05d",
|
||||
int16(5): "%05d",
|
||||
int32(5): "%05d",
|
||||
int64(5): "%05d",
|
||||
uint(5): "%05d",
|
||||
uint8(5): "%05d",
|
||||
uint16(5): "%05d",
|
||||
uint32(5): "%05d",
|
||||
uint64(5): "%05d",
|
||||
float32(5.55555): "%08.2f",
|
||||
float64(5.55555): "%08.2f",
|
||||
},
|
||||
"zero": {
|
||||
int(0): "%05d",
|
||||
int8(0): "%05d",
|
||||
int16(0): "%05d",
|
||||
int32(0): "%05d",
|
||||
int64(0): "%05d",
|
||||
uint(0): "%05d",
|
||||
uint8(0): "%05d",
|
||||
uint16(0): "%05d",
|
||||
uint32(0): "%05d",
|
||||
uint64(0): "%05d",
|
||||
float32(0.00000): "%08.2f",
|
||||
float64(0.00000): "%08.2f",
|
||||
},
|
||||
"nil": {
|
||||
nil: "%v",
|
||||
},
|
||||
}
|
||||
|
||||
for sign, valuesFormatMap := range colorValuesMap {
|
||||
for value, format := range valuesFormatMap {
|
||||
transformer := NewNumberTransformer(format)
|
||||
expected := signColorsMap[sign].Sprintf(format, value)
|
||||
if sign == "negative" {
|
||||
expected = strings.Replace(expected, "-0", "-00", 1)
|
||||
}
|
||||
actual := transformer(value)
|
||||
var kind reflect.Kind
|
||||
if value != nil {
|
||||
kind = reflect.TypeOf(value).Kind()
|
||||
}
|
||||
message := fmt.Sprintf("%s.%s: expected=%v, actual=%v; format=%#v",
|
||||
sign, kind, expected, actual, format)
|
||||
|
||||
assert.Equal(t, expected, actual, message)
|
||||
}
|
||||
}
|
||||
|
||||
// invalid input
|
||||
assert.Equal(t, "foo", NewNumberTransformer("%05d")("foo"))
|
||||
}
|
||||
|
||||
type jsonTest struct {
|
||||
Foo string `json:"foo"`
|
||||
Bar int32 `json:"bar"`
|
||||
Baz float64 `json:"baz"`
|
||||
Nan jsonNestTest `json:"nan"`
|
||||
}
|
||||
|
||||
type jsonNestTest struct {
|
||||
A string
|
||||
B int32
|
||||
C float64
|
||||
}
|
||||
|
||||
func TestNewJSONTransformer(t *testing.T) {
|
||||
transformer := NewJSONTransformer("", " ")
|
||||
|
||||
// instance of a struct
|
||||
inputObj := jsonTest{
|
||||
Foo: "fooooooo",
|
||||
Bar: 13,
|
||||
Baz: 3.14,
|
||||
Nan: jsonNestTest{
|
||||
A: "a",
|
||||
B: 2,
|
||||
C: 3.0,
|
||||
},
|
||||
}
|
||||
expectedOutput := `{
|
||||
"foo": "fooooooo",
|
||||
"bar": 13,
|
||||
"baz": 3.14,
|
||||
"nan": {
|
||||
"A": "a",
|
||||
"B": 2,
|
||||
"C": 3
|
||||
}
|
||||
}`
|
||||
assert.Equal(t, expectedOutput, transformer(inputObj))
|
||||
|
||||
// numbers
|
||||
assert.Equal(t, "1", transformer(int(1)))
|
||||
assert.Equal(t, "1.2345", transformer(float32(1.2345)))
|
||||
|
||||
// slices
|
||||
assert.Equal(t, "[\n 1,\n 2,\n 3\n]", transformer([]uint{1, 2, 3}))
|
||||
|
||||
// strings
|
||||
assert.Equal(t, "\"foo\"", transformer("foo"))
|
||||
assert.Equal(t, "\"{foo...\"", transformer("{foo...")) // malformed JSON
|
||||
|
||||
// strings with valid JSON
|
||||
input := "{\"foo\":\"bar\",\"baz\":[1,2,3]}"
|
||||
expectedOutput = `{
|
||||
"foo": "bar",
|
||||
"baz": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}`
|
||||
assert.Equal(t, expectedOutput, transformer(input))
|
||||
}
|
||||
|
||||
func TestNewTimeTransformer(t *testing.T) {
|
||||
inStr := "2010-11-12T13:14:15-07:00"
|
||||
inTime, err := time.Parse(time.RFC3339, inStr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
location, err := time.LoadLocation("America/Los_Angeles")
|
||||
assert.Nil(t, err)
|
||||
transformer := NewTimeTransformer(time.RFC3339, location)
|
||||
expected := "2010-11-12T12:14:15-08:00"
|
||||
assert.Equal(t, expected, transformer(inStr))
|
||||
assert.Equal(t, expected, transformer(inTime))
|
||||
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||
assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout)
|
||||
}
|
||||
|
||||
location, err = time.LoadLocation("Asia/Singapore")
|
||||
assert.Nil(t, err)
|
||||
transformer = NewTimeTransformer(time.UnixDate, location)
|
||||
expected = "Sat Nov 13 04:14:15 +08 2010"
|
||||
assert.Equal(t, expected, transformer(inStr))
|
||||
assert.Equal(t, expected, transformer(inTime))
|
||||
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||
assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout)
|
||||
}
|
||||
|
||||
location, err = time.LoadLocation("Europe/London")
|
||||
assert.Nil(t, err)
|
||||
transformer = NewTimeTransformer(time.RFC3339, location)
|
||||
expected = "2010-11-12T20:14:15Z"
|
||||
assert.Equal(t, expected, transformer(inStr))
|
||||
assert.Equal(t, expected, transformer(inTime))
|
||||
for _, possibleTimeLayout := range possibleTimeLayouts {
|
||||
assert.Equal(t, expected, transformer(inTime.Format(possibleTimeLayout)), possibleTimeLayout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUnixTimeTransformer(t *testing.T) {
|
||||
inStr := "2010-11-12T13:14:15-07:00"
|
||||
inTime, err := time.Parse(time.RFC3339, inStr)
|
||||
assert.Nil(t, err)
|
||||
inUnixTime := inTime.Unix()
|
||||
|
||||
location, err := time.LoadLocation("America/Los_Angeles")
|
||||
assert.Nil(t, err)
|
||||
transformer := NewUnixTimeTransformer(time.RFC3339, location)
|
||||
expected := "2010-11-12T12:14:15-08:00"
|
||||
assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string")
|
||||
assert.Equal(t, expected, transformer(inUnixTime), "seconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds")
|
||||
|
||||
location, err = time.LoadLocation("Asia/Singapore")
|
||||
assert.Nil(t, err)
|
||||
transformer = NewUnixTimeTransformer(time.UnixDate, location)
|
||||
expected = "Sat Nov 13 04:14:15 +08 2010"
|
||||
assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string")
|
||||
assert.Equal(t, expected, transformer(inUnixTime), "seconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds")
|
||||
|
||||
location, err = time.LoadLocation("Europe/London")
|
||||
assert.Nil(t, err)
|
||||
transformer = NewUnixTimeTransformer(time.RFC3339, location)
|
||||
expected = "2010-11-12T20:14:15Z"
|
||||
assert.Equal(t, expected, transformer(fmt.Sprint(inUnixTime)), "seconds in string")
|
||||
assert.Equal(t, expected, transformer(inUnixTime), "seconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000), "milliseconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000000), "microseconds")
|
||||
assert.Equal(t, expected, transformer(inUnixTime*1000000000), "nanoseconds")
|
||||
|
||||
assert.Equal(t, "0.123456", transformer(float32(0.123456)))
|
||||
}
|
||||
|
||||
func TestNewURLTransformer(t *testing.T) {
|
||||
url := "https://winter.is.coming"
|
||||
|
||||
transformer := NewURLTransformer()
|
||||
assert.Equal(t, colorsURL.Sprint(url), transformer(url))
|
||||
|
||||
transformer2 := NewURLTransformer(FgRed, BgWhite, Bold)
|
||||
assert.Equal(t, Colors{FgRed, BgWhite, Bold}.Sprint(url), transformer2(url))
|
||||
assert.Equal(t, colorsURL.Sprint(url), transformer(url))
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package text
|
||||
|
||||
import "strings"
|
||||
|
||||
// VAlign denotes how text is to be aligned vertically.
|
||||
type VAlign int
|
||||
|
||||
// VAlign enumerations
|
||||
const (
|
||||
VAlignDefault VAlign = iota // same as VAlignTop
|
||||
VAlignTop // "top\n\n"
|
||||
VAlignMiddle // "\nmiddle\n"
|
||||
VAlignBottom // "\n\nbottom"
|
||||
)
|
||||
|
||||
// Apply aligns the lines vertically. For ex.:
|
||||
// * VAlignTop.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// * VAlignMiddle.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// * VAlignBottom.Apply({"Game", "Of", "Thrones"}, 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
func (va VAlign) Apply(lines []string, maxLines int) []string {
|
||||
if len(lines) == maxLines {
|
||||
return lines
|
||||
} else if len(lines) > maxLines {
|
||||
maxLines = len(lines)
|
||||
}
|
||||
|
||||
insertIdx := 0
|
||||
if va == VAlignMiddle {
|
||||
insertIdx = int(maxLines-len(lines)) / 2
|
||||
} else if va == VAlignBottom {
|
||||
insertIdx = maxLines - len(lines)
|
||||
}
|
||||
|
||||
linesOut := strings.Split(strings.Repeat("\n", maxLines-1), "\n")
|
||||
for idx, line := range lines {
|
||||
linesOut[idx+insertIdx] = line
|
||||
}
|
||||
return linesOut
|
||||
}
|
||||
|
||||
// ApplyStr aligns the string (of 1 or more lines) vertically. For ex.:
|
||||
// * VAlignTop.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"Game", "Of", "Thrones", "", ""}
|
||||
// * VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "Game", "Of", "Thrones", ""}
|
||||
// * VAlignBottom.ApplyStr("Game\nOf\nThrones", 5)
|
||||
// returns {"", "", "Game", "Of", "Thrones"}
|
||||
func (va VAlign) ApplyStr(text string, maxLines int) []string {
|
||||
return va.Apply(strings.Split(text, "\n"), maxLines)
|
||||
}
|
||||
|
||||
// HTMLProperty returns the equivalent HTML vertical-align tag property.
|
||||
func (va VAlign) HTMLProperty() string {
|
||||
switch va {
|
||||
case VAlignTop:
|
||||
return "valign=\"top\""
|
||||
case VAlignMiddle:
|
||||
return "valign=\"middle\""
|
||||
case VAlignBottom:
|
||||
return "valign=\"bottom\""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleVAlign_Apply() {
|
||||
lines := []string{"Game", "Of", "Thrones"}
|
||||
maxLines := 5
|
||||
fmt.Printf("VAlignDefault: %#v\n", VAlignDefault.Apply(lines, maxLines))
|
||||
fmt.Printf("VAlignTop : %#v\n", VAlignTop.Apply(lines, maxLines))
|
||||
fmt.Printf("VAlignMiddle : %#v\n", VAlignMiddle.Apply(lines, maxLines))
|
||||
fmt.Printf("VAlignBottom : %#v\n", VAlignBottom.Apply(lines, maxLines))
|
||||
|
||||
// Output: VAlignDefault: []string{"Game", "Of", "Thrones", "", ""}
|
||||
// VAlignTop : []string{"Game", "Of", "Thrones", "", ""}
|
||||
// VAlignMiddle : []string{"", "Game", "Of", "Thrones", ""}
|
||||
// VAlignBottom : []string{"", "", "Game", "Of", "Thrones"}
|
||||
}
|
||||
|
||||
func TestVAlign_Apply(t *testing.T) {
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||
VAlignDefault.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||
VAlignTop.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||
assert.Equal(t, []string{"", "Game", "Of", "Thrones", ""},
|
||||
VAlignMiddle.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 1))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones"},
|
||||
VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 3))
|
||||
assert.Equal(t, []string{"", "", "Game", "Of", "Thrones"},
|
||||
VAlignBottom.Apply([]string{"Game", "Of", "Thrones"}, 5))
|
||||
}
|
||||
|
||||
func ExampleVAlign_ApplyStr() {
|
||||
str := "Game\nOf\nThrones"
|
||||
maxLines := 5
|
||||
fmt.Printf("VAlignDefault: %#v\n", VAlignDefault.ApplyStr(str, maxLines))
|
||||
fmt.Printf("VAlignTop : %#v\n", VAlignTop.ApplyStr(str, maxLines))
|
||||
fmt.Printf("VAlignMiddle : %#v\n", VAlignMiddle.ApplyStr(str, maxLines))
|
||||
fmt.Printf("VAlignBottom : %#v\n", VAlignBottom.ApplyStr(str, maxLines))
|
||||
|
||||
// Output: VAlignDefault: []string{"Game", "Of", "Thrones", "", ""}
|
||||
// VAlignTop : []string{"Game", "Of", "Thrones", "", ""}
|
||||
// VAlignMiddle : []string{"", "Game", "Of", "Thrones", ""}
|
||||
// VAlignBottom : []string{"", "", "Game", "Of", "Thrones"}
|
||||
}
|
||||
|
||||
func TestVAlign_ApplyStr(t *testing.T) {
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||
VAlignDefault.ApplyStr("Game\nOf\nThrones", 5))
|
||||
assert.Equal(t, []string{"Game", "Of", "Thrones", "", ""},
|
||||
VAlignTop.ApplyStr("Game\nOf\nThrones", 5))
|
||||
assert.Equal(t, []string{"", "Game", "Of", "Thrones", ""},
|
||||
VAlignMiddle.ApplyStr("Game\nOf\nThrones", 5))
|
||||
assert.Equal(t, []string{"", "", "Game", "Of", "Thrones"},
|
||||
VAlignBottom.ApplyStr("Game\nOf\nThrones", 5))
|
||||
}
|
||||
|
||||
func ExampleVAlign_HTMLProperty() {
|
||||
fmt.Printf("VAlignDefault: '%s'\n", VAlignDefault.HTMLProperty())
|
||||
fmt.Printf("VAlignTop : '%s'\n", VAlignTop.HTMLProperty())
|
||||
fmt.Printf("VAlignMiddle : '%s'\n", VAlignMiddle.HTMLProperty())
|
||||
fmt.Printf("VAlignBottom : '%s'\n", VAlignBottom.HTMLProperty())
|
||||
|
||||
// Output: VAlignDefault: ''
|
||||
// VAlignTop : 'valign="top"'
|
||||
// VAlignMiddle : 'valign="middle"'
|
||||
// VAlignBottom : 'valign="bottom"'
|
||||
}
|
||||
|
||||
func TestVAlign_HTMLProperty(t *testing.T) {
|
||||
vAligns := map[VAlign]string{
|
||||
VAlignDefault: "",
|
||||
VAlignTop: "top",
|
||||
VAlignMiddle: "middle",
|
||||
VAlignBottom: "bottom",
|
||||
}
|
||||
for vAlign, htmlStyle := range vAligns {
|
||||
assert.Contains(t, vAlign.HTMLProperty(), htmlStyle)
|
||||
}
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// WrapHard wraps a string to the given length using a newline. Handles strings
|
||||
// with ANSI escape sequences (such as text color) without breaking the text
|
||||
// formatting. Breaks all words that go beyond the line boundary.
|
||||
//
|
||||
// For examples, refer to the unit-tests or GoDoc examples.
|
||||
func WrapHard(str string, wrapLen int) string {
|
||||
if wrapLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
str = strings.Replace(str, "\t", " ", -1)
|
||||
sLen := utf8.RuneCountInString(str)
|
||||
if sLen <= wrapLen {
|
||||
return str
|
||||
}
|
||||
|
||||
out := &strings.Builder{}
|
||||
out.Grow(sLen + (sLen / wrapLen))
|
||||
for idx, paragraph := range strings.Split(str, "\n\n") {
|
||||
if idx > 0 {
|
||||
out.WriteString("\n\n")
|
||||
}
|
||||
wrapHard(paragraph, wrapLen, out)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// WrapSoft wraps a string to the given length using a newline. Handles strings
|
||||
// with ANSI escape sequences (such as text color) without breaking the text
|
||||
// formatting. Tries to move words that go beyond the line boundary to the next
|
||||
// line.
|
||||
//
|
||||
// For examples, refer to the unit-tests or GoDoc examples.
|
||||
func WrapSoft(str string, wrapLen int) string {
|
||||
if wrapLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
str = strings.Replace(str, "\t", " ", -1)
|
||||
sLen := utf8.RuneCountInString(str)
|
||||
if sLen <= wrapLen {
|
||||
return str
|
||||
}
|
||||
|
||||
out := &strings.Builder{}
|
||||
out.Grow(sLen + (sLen / wrapLen))
|
||||
for idx, paragraph := range strings.Split(str, "\n\n") {
|
||||
if idx > 0 {
|
||||
out.WriteString("\n\n")
|
||||
}
|
||||
wrapSoft(paragraph, wrapLen, out)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// WrapText is very similar to WrapHard except for one minor difference. Unlike
|
||||
// WrapHard which discards line-breaks and respects only paragraph-breaks, this
|
||||
// function respects line-breaks too.
|
||||
//
|
||||
// For examples, refer to the unit-tests or GoDoc examples.
|
||||
func WrapText(str string, wrapLen int) string {
|
||||
if wrapLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
sLen := utf8.RuneCountInString(str)
|
||||
out.Grow(sLen + (sLen / wrapLen))
|
||||
lineIdx, isEscSeq, lastEscSeq := 0, false, ""
|
||||
for _, char := range str {
|
||||
if char == EscapeStartRune {
|
||||
isEscSeq = true
|
||||
lastEscSeq = ""
|
||||
}
|
||||
if isEscSeq {
|
||||
lastEscSeq += string(char)
|
||||
}
|
||||
|
||||
appendChar(char, wrapLen, &lineIdx, isEscSeq, lastEscSeq, &out)
|
||||
|
||||
if isEscSeq && char == EscapeStopRune {
|
||||
isEscSeq = false
|
||||
}
|
||||
if lastEscSeq == EscapeReset {
|
||||
lastEscSeq = ""
|
||||
}
|
||||
}
|
||||
if lastEscSeq != "" && lastEscSeq != EscapeReset {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func appendChar(char rune, wrapLen int, lineLen *int, inEscSeq bool, lastSeenEscSeq string, out *strings.Builder) {
|
||||
// handle reaching the end of the line as dictated by wrapLen or by finding
|
||||
// a newline character
|
||||
if (*lineLen == wrapLen && !inEscSeq && char != '\n') || (char == '\n') {
|
||||
if lastSeenEscSeq != "" {
|
||||
// terminate escape sequence and the line; and restart the escape
|
||||
// sequence in the next line
|
||||
out.WriteString(EscapeReset)
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(lastSeenEscSeq)
|
||||
} else {
|
||||
// just start a new line
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
// reset line index to 0th character
|
||||
*lineLen = 0
|
||||
}
|
||||
|
||||
// if the rune is not a new line, output it
|
||||
if char != '\n' {
|
||||
out.WriteRune(char)
|
||||
|
||||
// increment the line index if not in the middle of an escape sequence
|
||||
if !inEscSeq {
|
||||
*lineLen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendWord(word string, lineIdx *int, lastSeenEscSeq string, wrapLen int, out *strings.Builder) {
|
||||
inEscSeq := false
|
||||
for _, char := range word {
|
||||
if char == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
lastSeenEscSeq = ""
|
||||
}
|
||||
if inEscSeq {
|
||||
lastSeenEscSeq += string(char)
|
||||
}
|
||||
|
||||
appendChar(char, wrapLen, lineIdx, inEscSeq, lastSeenEscSeq, out)
|
||||
|
||||
if inEscSeq && char == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
if lastSeenEscSeq == EscapeReset {
|
||||
lastSeenEscSeq = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractOpenEscapeSeq(str string) string {
|
||||
escapeSeq, inEscSeq := "", false
|
||||
for _, char := range str {
|
||||
if char == EscapeStartRune {
|
||||
inEscSeq = true
|
||||
escapeSeq = ""
|
||||
}
|
||||
if inEscSeq {
|
||||
escapeSeq += string(char)
|
||||
}
|
||||
if char == EscapeStopRune {
|
||||
inEscSeq = false
|
||||
}
|
||||
}
|
||||
if escapeSeq == EscapeReset {
|
||||
escapeSeq = ""
|
||||
}
|
||||
return escapeSeq
|
||||
}
|
||||
|
||||
func terminateLine(wrapLen int, lineLen *int, lastSeenEscSeq string, out *strings.Builder) {
|
||||
if *lineLen < wrapLen {
|
||||
out.WriteString(strings.Repeat(" ", wrapLen-*lineLen))
|
||||
}
|
||||
// something is already on the line; terminate it
|
||||
if lastSeenEscSeq != "" {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
out.WriteRune('\n')
|
||||
out.WriteString(lastSeenEscSeq)
|
||||
*lineLen = 0
|
||||
}
|
||||
|
||||
func terminateOutput(lastSeenEscSeq string, out *strings.Builder) {
|
||||
if lastSeenEscSeq != "" && lastSeenEscSeq != EscapeReset && !strings.HasSuffix(out.String(), EscapeReset) {
|
||||
out.WriteString(EscapeReset)
|
||||
}
|
||||
}
|
||||
|
||||
func wrapHard(paragraph string, wrapLen int, out *strings.Builder) {
|
||||
lineLen, lastSeenEscSeq := 0, ""
|
||||
words := strings.Fields(paragraph)
|
||||
for wordIdx, word := range words {
|
||||
escSeq := extractOpenEscapeSeq(word)
|
||||
if escSeq != "" {
|
||||
lastSeenEscSeq = escSeq
|
||||
}
|
||||
if lineLen > 0 {
|
||||
out.WriteRune(' ')
|
||||
lineLen++
|
||||
}
|
||||
|
||||
wordLen := RuneWidthWithoutEscSequences(word)
|
||||
if lineLen+wordLen <= wrapLen { // word fits within the line
|
||||
out.WriteString(word)
|
||||
lineLen += wordLen
|
||||
} else { // word doesn't fit within the line; hard-wrap
|
||||
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
|
||||
}
|
||||
|
||||
// end of line; but more words incoming
|
||||
if lineLen == wrapLen && wordIdx < len(words)-1 {
|
||||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||
}
|
||||
}
|
||||
terminateOutput(lastSeenEscSeq, out)
|
||||
}
|
||||
|
||||
func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) {
|
||||
lineLen, lastSeenEscSeq := 0, ""
|
||||
words := strings.Fields(paragraph)
|
||||
for wordIdx, word := range words {
|
||||
escSeq := extractOpenEscapeSeq(word)
|
||||
if escSeq != "" {
|
||||
lastSeenEscSeq = escSeq
|
||||
}
|
||||
|
||||
spacing, spacingLen := wrapSoftSpacing(lineLen)
|
||||
wordLen := RuneWidthWithoutEscSequences(word)
|
||||
if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line
|
||||
out.WriteString(spacing)
|
||||
out.WriteString(word)
|
||||
lineLen += spacingLen + wordLen
|
||||
} else { // word doesn't fit within the line
|
||||
lineLen = wrapSoftLastWordInLine(wrapLen, lineLen, lastSeenEscSeq, wordLen, word, out)
|
||||
}
|
||||
|
||||
// end of line; but more words incoming
|
||||
if lineLen == wrapLen && wordIdx < len(words)-1 {
|
||||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||
}
|
||||
}
|
||||
terminateOutput(lastSeenEscSeq, out)
|
||||
}
|
||||
|
||||
func wrapSoftLastWordInLine(wrapLen int, lineLen int, lastSeenEscSeq string, wordLen int, word string, out *strings.Builder) int {
|
||||
if lineLen > 0 { // something is already on the line; terminate it
|
||||
terminateLine(wrapLen, &lineLen, lastSeenEscSeq, out)
|
||||
}
|
||||
if wordLen <= wrapLen { // word fits within a single line
|
||||
out.WriteString(word)
|
||||
lineLen = wordLen
|
||||
} else { // word doesn't fit within a single line; hard-wrap
|
||||
appendWord(word, &lineLen, lastSeenEscSeq, wrapLen, out)
|
||||
}
|
||||
return lineLen
|
||||
}
|
||||
|
||||
func wrapSoftSpacing(lineLen int) (string, int) {
|
||||
spacing, spacingLen := "", 0
|
||||
if lineLen > 0 {
|
||||
spacing, spacingLen = " ", 1
|
||||
}
|
||||
return spacing, spacingLen
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func ExampleWrapHard() {
|
||||
str := `The quick brown fox jumped over the lazy dog.
|
||||
|
||||
A big crocodile died empty-fanged, gulping horribly in jerking kicking little
|
||||
motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with
|
||||
Xavier yelling Zap!`
|
||||
strWrapped := WrapHard(str, 30)
|
||||
for idx, line := range strings.Split(strWrapped, "\n") {
|
||||
fmt.Printf("Line #%02d: '%s'\n", idx+1, line)
|
||||
}
|
||||
|
||||
// Output: Line #01: 'The quick brown fox jumped ove'
|
||||
// Line #02: 'r the lazy dog.'
|
||||
// Line #03: ''
|
||||
// Line #04: 'A big crocodile died empty-fan'
|
||||
// Line #05: 'ged, gulping horribly in jerki'
|
||||
// Line #06: 'ng kicking little motions. Non'
|
||||
// Line #07: 'chalant old Peter Quinn ruthle'
|
||||
// Line #08: 'ssly shot the under-water verm'
|
||||
// Line #09: 'in with Xavier yelling Zap!'
|
||||
}
|
||||
|
||||
func TestWrapHard(t *testing.T) {
|
||||
assert.Equal(t, "", WrapHard("Ghost", 0))
|
||||
assert.Equal(t, "G\nh\no\ns\nt", WrapHard("Ghost", 1))
|
||||
assert.Equal(t, "Gh\nos\nt", WrapHard("Ghost", 2))
|
||||
assert.Equal(t, "Gho\nst", WrapHard("Ghost", 3))
|
||||
assert.Equal(t, "Ghos\nt", WrapHard("Ghost", 4))
|
||||
assert.Equal(t, "Ghost", WrapHard("Ghost", 5))
|
||||
assert.Equal(t, "Ghost", WrapHard("Ghost", 6))
|
||||
assert.Equal(t, "Jo\nn \nSn\now", WrapHard("Jon\nSnow", 2))
|
||||
assert.Equal(t, "Jo\nn \nSn\now", WrapHard("Jon\nSnow\n", 2))
|
||||
assert.Equal(t, "Jon\nSno\nw", WrapHard("Jon\nSnow\n", 3))
|
||||
assert.Equal(t, "Jon i\ns a S\nnow", WrapHard("Jon is a Snow", 5))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapHard("\x1b[33mJon\x1b[0m\nSnow", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapHard("\x1b[33mJon\x1b[0m\nSnow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapHard("\x1b[33mJon Snow\x1b[0m", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapHard("\x1b[33mJon Snow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw \x1b[0m", WrapHard("\x1b[33mJon Snow\n\x1b[0m", 3))
|
||||
|
||||
complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+"
|
||||
assert.Equal(t, complexIn, WrapHard(complexIn, 27))
|
||||
}
|
||||
|
||||
func ExampleWrapSoft() {
|
||||
str := `The quick brown fox jumped over the lazy dog.
|
||||
|
||||
A big crocodile died empty-fanged, gulping horribly in jerking kicking little
|
||||
motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with
|
||||
Xavier yelling Zap!`
|
||||
strWrapped := WrapSoft(str, 30)
|
||||
for idx, line := range strings.Split(strWrapped, "\n") {
|
||||
fmt.Printf("Line #%02d: '%s'\n", idx+1, line)
|
||||
}
|
||||
|
||||
// Output: Line #01: 'The quick brown fox jumped '
|
||||
// Line #02: 'over the lazy dog.'
|
||||
// Line #03: ''
|
||||
// Line #04: 'A big crocodile died '
|
||||
// Line #05: 'empty-fanged, gulping horribly'
|
||||
// Line #06: 'in jerking kicking little '
|
||||
// Line #07: 'motions. Nonchalant old Peter '
|
||||
// Line #08: 'Quinn ruthlessly shot the '
|
||||
// Line #09: 'under-water vermin with Xavier'
|
||||
// Line #10: 'yelling Zap!'
|
||||
}
|
||||
|
||||
func TestWrapSoft(t *testing.T) {
|
||||
assert.Equal(t, "", WrapSoft("Ghost", 0))
|
||||
assert.Equal(t, "G\nh\no\ns\nt", WrapSoft("Ghost", 1))
|
||||
assert.Equal(t, "Gh\nos\nt", WrapSoft("Ghost", 2))
|
||||
assert.Equal(t, "Gho\nst", WrapSoft("Ghost", 3))
|
||||
assert.Equal(t, "Ghos\nt", WrapSoft("Ghost", 4))
|
||||
assert.Equal(t, "Ghost", WrapSoft("Ghost", 5))
|
||||
assert.Equal(t, "Ghost", WrapSoft("Ghost", 6))
|
||||
assert.Equal(t, "Jo\nn \nSn\now", WrapSoft("Jon\nSnow", 2))
|
||||
assert.Equal(t, "Jo\nn \nSn\now", WrapSoft("Jon\nSnow\n", 2))
|
||||
assert.Equal(t, "Jon\nSno\nw", WrapSoft("Jon\nSnow\n", 3))
|
||||
assert.Equal(t, "Jon \nSnow", WrapSoft("Jon\nSnow", 4))
|
||||
assert.Equal(t, "Jon \nSnow", WrapSoft("Jon\nSnow\n", 4))
|
||||
assert.Equal(t, "Jon \nis a \nSnow", WrapSoft("Jon is a Snow", 5))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapSoft("\x1b[33mJon\x1b[0m\nSnow", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapSoft("\x1b[33mJon\x1b[0m\nSnow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapSoft("\x1b[33mJon Snow\x1b[0m", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw\x1b[0m", WrapSoft("\x1b[33mJon Snow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33mSno\x1b[0m\n\x1b[33mw \x1b[0m", WrapSoft("\x1b[33mJon Snow\n\x1b[0m", 3))
|
||||
|
||||
complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+"
|
||||
assert.Equal(t, complexIn, WrapSoft(complexIn, 27))
|
||||
|
||||
assert.Equal(t, "\x1b[33mJon \x1b[0m\n\x1b[33mSnow\x1b[0m", WrapSoft("\x1b[33mJon Snow\x1b[0m", 4))
|
||||
assert.Equal(t, "\x1b[33mJon \x1b[0m\n\x1b[33mSnow\x1b[0m\n\x1b[33m???\x1b[0m", WrapSoft("\x1b[33mJon Snow???\x1b[0m", 4))
|
||||
}
|
||||
|
||||
func ExampleWrapText() {
|
||||
str := `The quick brown fox jumped over the lazy dog.
|
||||
|
||||
A big crocodile died empty-fanged, gulping horribly in jerking kicking little
|
||||
motions. Nonchalant old Peter Quinn ruthlessly shot the under-water vermin with
|
||||
Xavier yelling Zap!`
|
||||
strWrapped := WrapText(str, 30)
|
||||
for idx, line := range strings.Split(strWrapped, "\n") {
|
||||
fmt.Printf("Line #%02d: '%s'\n", idx+1, line)
|
||||
}
|
||||
|
||||
// Output: Line #01: 'The quick brown fox jumped ove'
|
||||
// Line #02: 'r the lazy dog.'
|
||||
// Line #03: ''
|
||||
// Line #04: 'A big crocodile died empty-fan'
|
||||
// Line #05: 'ged, gulping horribly in jerki'
|
||||
// Line #06: 'ng kicking little'
|
||||
// Line #07: 'motions. Nonchalant old Peter '
|
||||
// Line #08: 'Quinn ruthlessly shot the unde'
|
||||
// Line #09: 'r-water vermin with'
|
||||
// Line #10: 'Xavier yelling Zap!'
|
||||
}
|
||||
|
||||
func TestWrapText(t *testing.T) {
|
||||
assert.Equal(t, "", WrapText("Ghost", 0))
|
||||
assert.Equal(t, "G\nh\no\ns\nt", WrapText("Ghost", 1))
|
||||
assert.Equal(t, "Gh\nos\nt", WrapText("Ghost", 2))
|
||||
assert.Equal(t, "Gho\nst", WrapText("Ghost", 3))
|
||||
assert.Equal(t, "Ghos\nt", WrapText("Ghost", 4))
|
||||
assert.Equal(t, "Ghost", WrapText("Ghost", 5))
|
||||
assert.Equal(t, "Ghost", WrapText("Ghost", 6))
|
||||
assert.Equal(t, "Jo\nn\nSn\now", WrapText("Jon\nSnow", 2))
|
||||
assert.Equal(t, "Jo\nn\nSn\now\n", WrapText("Jon\nSnow\n", 2))
|
||||
assert.Equal(t, "Jon\nSno\nw\n", WrapText("Jon\nSnow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw", WrapText("\x1b[33mJon\x1b[0m\nSnow", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\nSno\nw\n", WrapText("\x1b[33mJon\x1b[0m\nSnow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m", WrapText("\x1b[33mJon Snow\x1b[0m", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m\n\x1b[33m\x1b[0m", WrapText("\x1b[33mJon Snow\n", 3))
|
||||
assert.Equal(t, "\x1b[33mJon\x1b[0m\n\x1b[33m Sn\x1b[0m\n\x1b[33mow\x1b[0m\n\x1b[33m\x1b[0m", WrapText("\x1b[33mJon Snow\n\x1b[0m", 3))
|
||||
|
||||
complexIn := "+---+------+-------+------+\n| 1 | Arya | Stark | 3000 |\n+---+------+-------+------+"
|
||||
assert.Equal(t, complexIn, WrapText(complexIn, 27))
|
||||
}
|
Loading…
Reference in New Issue
Block a user