pax_global_header 0000666 0000000 0000000 00000000064 15206753664 0014530 g ustar 00root root 0000000 0000000 52 comment=60aeed6ac0becb64e7c8e5c09049dd36ef35e387
golang-github-ysmood-gop-0.4.1/ 0000775 0000000 0000000 00000000000 15206753664 0016354 5 ustar 00root root 0000000 0000000 golang-github-ysmood-gop-0.4.1/.cspell.json 0000664 0000000 0000000 00000000134 15206753664 0020605 0 ustar 00root root 0000000 0000000 {
"words": [
"tmpl",
"tput",
"Unsets",
"ysmood"
]
}
golang-github-ysmood-gop-0.4.1/.github/ 0000775 0000000 0000000 00000000000 15206753664 0017714 5 ustar 00root root 0000000 0000000 golang-github-ysmood-gop-0.4.1/.github/workflows/ 0000775 0000000 0000000 00000000000 15206753664 0021751 5 ustar 00root root 0000000 0000000 golang-github-ysmood-gop-0.4.1/.github/workflows/test.yml 0000664 0000000 0000000 00000000707 15206753664 0023457 0 ustar 00root root 0000000 0000000 name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: 1.18
- uses: actions/checkout@v2
- name: lint
run: go run github.com/ysmood/golangci-lint@latest
- name: test
env:
TERM: xterm-256color
run: |
go test -race -coverprofile=coverage.out ./...
go run github.com/ysmood/got/cmd/check-cov@latest
golang-github-ysmood-gop-0.4.1/.gitignore 0000664 0000000 0000000 00000000031 15206753664 0020336 0 ustar 00root root 0000000 0000000 *.out
*.test
tmp/
.claude golang-github-ysmood-gop-0.4.1/.golangci.yml 0000664 0000000 0000000 00000000315 15206753664 0020737 0 ustar 00root root 0000000 0000000 run:
skip-dirs-use-default: false
linters:
enable:
- gofmt
- gocyclo
- misspell
- bodyclose
disable:
- govet
gocyclo:
min-complexity: 15
issues:
exclude-use-default: false
golang-github-ysmood-gop-0.4.1/LICENSE 0000664 0000000 0000000 00000002052 15206753664 0017360 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2023 Yad Smood
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
golang-github-ysmood-gop-0.4.1/README.md 0000664 0000000 0000000 00000003473 15206753664 0017642 0 ustar 00root root 0000000 0000000 # Go Pretty Print Value
Make a random Go value human readable. The output format uses valid golang syntax, so you don't have to learn any new knowledge to understand the output.
## Features
- Uses valid golang syntax to print the data
- Make rune, []byte, time, etc. data human readable
- Color output with customizable theme
- Stable map output with sorted by keys
- Auto split multiline large string block
- Prints the path of circular reference
- Auto format inline json string
- Low-level API to extend the lib
## Usage
Usually, you only need to use `gop.P` function:
```go
package main
import (
"time"
"github.com/ysmood/got/lib/gop"
)
func main() {
val := map[string]interface{}{
"bool": true,
"number": 1 + 1i,
"bytes": []byte{97, 98, 99},
"lines": "multiline string\nline two",
"slice": []interface{}{1, 2},
"time": time.Now(),
"chan": make(chan int, 1),
"struct": struct{ test int32 }{
test: 13,
},
"json": `{"a" : 1}`,
"func": func(int) int { return 0 },
}
val["slice"].([]interface{})[1] = val["slice"]
_ = gop.P(val)
}
```
The output will be something like:
```go
// 2023-10-07T18:19:57.517309+08:00 example/main.go:27 (main.main)
map[string]interface {}{
"bool": true,
"bytes": []byte("abc"),
"chan": make(chan int, 1)/* 0x1400008c070 */,
"func": (func(int) int)(nil)/* 0x1025a5460 */,
"json": gop.JSONStr(map[string]interface {}{
"a": 1.0,
}, `{"a" : 1}`),
"lines": `multiline string
line two`,
"number": 1+1i,
"slice": []interface {}{
1,
gop.Circular("slice").([]interface {}),
},
"struct": struct { test int32 }{
test: int32(13),
},
"time": gop.Time("2023-10-07T18:19:57.516984+08:00", 3081584),
}
```
golang-github-ysmood-gop-0.4.1/bench_test.go 0000664 0000000 0000000 00000003665 15206753664 0021033 0 ustar 00root root 0000000 0000000 package gop_test
import (
"testing"
"time"
"unsafe"
"github.com/ysmood/gop"
)
func benchValue() interface{} {
ref := "test"
timeStamp, _ := time.Parse(time.RFC3339Nano, "2021-08-28T08:36:36.807908+08:00")
fn := func(string) int { return 10 }
ch1 := make(chan int)
ch2 := make(chan string, 3)
ch3 := make(chan struct{})
return []interface{}{
nil,
[]int{},
[]interface{}{true, false, uintptr(0x17), float32(100.121111133)},
true, 10, int8(2), int32(100),
float64(100.121111133),
complex64(1 + 2i), complex128(1 + 2i),
[3]int{1, 2},
ch1,
ch2,
ch3,
fn,
map[interface{}]interface{}{
`"test"`: 10,
"a": 1,
&ref: 1,
},
unsafe.Pointer(&ref),
struct {
Int int
str string
M map[int]int
}{10, "ok", map[int]int{1: 0x20}},
[]byte("aa\xe2"),
[]byte("bytes\n\tbytes"),
[]byte("long long long long string"),
byte('a'),
byte(1),
'天',
"long long long long string",
"\ntest",
"\t\n`",
&ref,
(*struct{ Int int })(nil),
&struct{ Int int }{},
&map[int]int{1: 2, 3: 4},
&[]int{1, 2},
&[2]int{1, 2},
&[]byte{1, 2},
timeStamp,
time.Hour,
`{"a": 1}`,
[]byte(`{"a": 1}`),
}
}
func BenchmarkTokenize(b *testing.B) {
v := benchValue()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = gop.Tokenize(v)
}
}
func BenchmarkFormatDefault(b *testing.B) {
v := benchValue()
ts := gop.Tokenize(v)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = gop.Format(ts, gop.ThemeDefault)
}
}
func BenchmarkFormatNone(b *testing.B) {
v := benchValue()
ts := gop.Tokenize(v)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = gop.Format(ts, gop.ThemeNone)
}
}
func BenchmarkF(b *testing.B) {
v := benchValue()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = gop.F(v)
}
}
func BenchmarkPlain(b *testing.B) {
v := benchValue()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = gop.Plain(v)
}
}
golang-github-ysmood-gop-0.4.1/convertors.go 0000664 0000000 0000000 00000003247 15206753664 0021115 0 ustar 00root root 0000000 0000000 package gop
import (
"encoding/base64"
"fmt"
"reflect"
"time"
)
// SymbolPtr for Ptr
const SymbolPtr = "gop.Ptr"
// Ptr returns a pointer to v
func Ptr(v interface{}) interface{} {
val := reflect.ValueOf(v)
ptr := reflect.New(val.Type())
ptr.Elem().Set(val)
return ptr.Interface()
}
// SymbolCircular for Circular
const SymbolCircular = "gop.Circular"
// Circular reference of the path from the root
func Circular(path ...interface{}) interface{} {
return nil
}
// SymbolBase64 for Base64
const SymbolBase64 = "gop.Base64"
// Base64 returns the []byte that s represents
func Base64(s string) []byte {
b, _ := base64.StdEncoding.DecodeString(s)
return b
}
// SymbolRune for Rune
const SymbolRune = "gop.Rune"
func Rune(i int32, r rune) rune {
return r
}
// SymbolTime for Time
const SymbolTime = "gop.Time"
// Time from parsing s
func Time(s string, monotonic int) time.Time {
t, _ := time.Parse(time.RFC3339Nano, s)
return t
}
// SymbolDuration for Duration
const SymbolDuration = "gop.Duration"
// Duration from parsing s
func Duration(s string) time.Duration {
d, _ := time.ParseDuration(s)
return d
}
// SymbolJSONStr for JSONStr
const SymbolJSONStr = "gop.JSONStr"
// JSONStr returns the raw
func JSONStr(v interface{}, raw string) string {
return raw
}
// SymbolJSONBytes for JSONBytes
const SymbolJSONBytes = "gop.JSONBytes"
// JSONBytes returns the raw as []byte
func JSONBytes(v interface{}, raw string) []byte {
return []byte(raw)
}
const SymbolGopError = "gop.GopError"
// GopError returns an error with the given message, it represents an error occurred during tokenization or formatting.
func GopError(msg string) error {
return fmt.Errorf("%s", msg)
}
golang-github-ysmood-gop-0.4.1/example/ 0000775 0000000 0000000 00000000000 15206753664 0020007 5 ustar 00root root 0000000 0000000 golang-github-ysmood-gop-0.4.1/example/main.go 0000664 0000000 0000000 00000000777 15206753664 0021275 0 ustar 00root root 0000000 0000000 // Package main ...
package main
import (
"time"
"github.com/ysmood/gop"
)
func main() {
val := map[string]interface{}{
"bool": true,
"number": 1 + 1i,
"bytes": []byte{97, 98, 99},
"lines": "multiline string\nline two",
"slice": []interface{}{1, 2},
"time": time.Now(),
"chan": make(chan int, 1),
"struct": struct{ test int32 }{
test: 13,
},
"json": `{"a" : 1}`,
"func": func(int) int { return 0 },
}
val["slice"].([]interface{})[1] = val["slice"]
_ = gop.P(val)
}
golang-github-ysmood-gop-0.4.1/fixtures/ 0000775 0000000 0000000 00000000000 15206753664 0020225 5 ustar 00root root 0000000 0000000 golang-github-ysmood-gop-0.4.1/fixtures/compile_check.go.tmpl 0000664 0000000 0000000 00000000125 15206753664 0024312 0 ustar 00root root 0000000 0000000 package main
import (
"unsafe"
"github.com/ysmood/gop"
)
func main() {
_ = %s
} golang-github-ysmood-gop-0.4.1/fixtures/expected.tmpl 0000664 0000000 0000000 00000003015 15206753664 0022723 0 ustar 00root root 0000000 0000000 []interface {}{
nil,
[]int{},
[]interface {}{
true,
false,
uintptr(23),
float32(100.12111),
},
true,
10,
int8(2),
gop.Rune(100, 'd'),
100.121111133,
complex64(1+2i),
1+2i,
[3]int{
1,
2,
0,
},
make(chan int)/* {{.ch1}} */,
make(chan string, 3)/* {{.ch2}} */,
make(chan struct {})/* {{.ch3}} */,
(func(string) int)(nil)/* {{.fn}} */,
map[interface {}]interface {}{
`"test"`: 10,
"a": 1,
(interface {})(nil)/* {{.ref}} */: 1,
},
unsafe.Pointer(uintptr({{.ptr}})),
struct { Int int; str string; M map[int]int }{
Int: 10,
str: "ok",
M: map[int]int{
1: 32,
},
},
gop.Base64("YWHi"),
[]byte(`bytes
bytes`),
[]byte("long long long long string"),
byte('a'),
byte(0x1),
gop.Rune(22825, '天'),
"long long long long string",
`
test`,
"" +
" \n" +
"`",
gop.Ptr("test").(*string),
(*struct { Int int })(nil),
&struct { Int int }{
Int: 0,
},
&map[int]int{
1: 2,
3: 4,
},
&[]int{
1,
2,
},
&[2]int{
1,
2,
},
gop.Ptr([]byte("\x01\x02")).(*[]uint8),
gop.Time("2021-08-28T08:36:36.807908+08:00", 63765707796),
gop.Duration("1h0m0s"),
gop.JSONStr(map[string]interface {}{
"a": 1.0,
}, `{"a": 1}`),
gop.JSONBytes(map[string]interface {}{
"a": 1.0,
}, `{"a": 1}`),
} golang-github-ysmood-gop-0.4.1/fixtures/expected_with_color.tmpl 0000664 0000000 0000000 00000004655 15206753664 0025167 0 ustar 00root root 0000000 0000000 <36>[]interface {}<39>{
<31>nil<39>,
<36>[]int<39>{},
<36>[]interface {}<39>{
<34>true<39>,
<34>false<39>,
<36>uintptr<39>(<32>23<39>),
<36>float32<39>(<32>100.12111<39>),
},
<34>true<39>,
<32>10<39>,
<36>int8<39>(<32>2<39>),
<36>gop.Rune<39>(<32>100<39>, <33>'d'<39>),
<32>100.121111133<39>,
<36>complex64<39>(<32>1+2i<39>),
<32>1+2i<39>,
<36>[3]int<39>{
<32>1<39>,
<32>2<39>,
<32>0<39>,
},
<35>make<39>(<34>chan<39> <36>int<39>)<37>/* {{.ch1}} */<39>,
<35>make<39>(<34>chan<39> <36>string<39>, <32>3<39>)<37>/* {{.ch2}} */<39>,
<35>make<39>(<34>chan<39> <36>struct {}<39>)<37>/* {{.ch3}} */<39>,
(<36>func(string) int<39>)(<31>nil<39>)<37>/* {{.fn}} */<39>,
<36>map[interface {}]interface {}<39>{
<33>`"test"`<39>: <32>10<39>,
<33>"a"<39>: <32>1<39>,
(<36>interface {}<39>)(<31>nil<39>)<37>/* {{.ref}} */<39>: <32>1<39>,
},
<36>unsafe.Pointer<39>(<36>uintptr<39>(<32>{{.ptr}}<39>)),
<36>struct { Int int; str string; M map[int]int }<39>{
Int: <32>10<39>,
str: <33>"ok"<39>,
M: <36>map[int]int<39>{
<32>1<39>: <32>32<39>,
},
},
<35>gop.Base64<39>(<33>"YWHi"<39>),
<36>[]byte<39>(<33>`bytes<39>
<33> bytes`<39>),
<36>[]byte<39>(<33>"long long long long string"<39>),
<36>byte<39>(<33>'a'<39>),
<36>byte<39>(<33>0x1<39>),
<36>gop.Rune<39>(<32>22825<39>, <33>'天'<39>),
<33>"long long long long string"<39>,
<33>`<39>
<33>test`<39>,
<33>"" +<39>
<33> " \n" +<39>
<33> "`"<39>,
<35>gop.Ptr<39>(<33>"test"<39>).(<36>*string<39>),
(<36>*struct { Int int }<39>)(<31>nil<39>),
&<36>struct { Int int }<39>{
Int: <32>0<39>,
},
&<36>map[int]int<39>{
<32>1<39>: <32>2<39>,
<32>3<39>: <32>4<39>,
},
&<36>[]int<39>{
<32>1<39>,
<32>2<39>,
},
&<36>[2]int<39>{
<32>1<39>,
<32>2<39>,
},
<35>gop.Ptr<39>(<36>[]byte<39>(<33>"\x01\x02"<39>)).(<36>*[]uint8<39>),
<35>gop.Time<39>(<33>"2021-08-28T08:36:36.807908+08:00"<39>, <32>63765707796<39>),
<36>gop.Duration<39>(<33>"1h0m0s"<39>),
<35>gop.JSONStr<39>(<36>map[string]interface {}<39>{
<33>"a"<39>: <32>1.0<39>,
}, <33>`{"a": 1}`<39>),
<35>gop.JSONBytes<39>(<36>map[string]interface {}<39>{
<33>"a"<39>: <32>1.0<39>,
}, <33>`{"a": 1}`<39>),
} golang-github-ysmood-gop-0.4.1/format.go 0000664 0000000 0000000 00000013314 15206753664 0020175 0 ustar 00root root 0000000 0000000 // Package gop ...
package gop
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// Stdout is the default stdout for gop.P .
var Stdout io.Writer = os.Stdout
const indentUnit = " "
// Theme to color values
type Theme func(t Type) []Style
// ThemeDefault colors for Sprint
var ThemeDefault = func(t Type) []Style {
switch t {
case TypeName:
return []Style{Cyan}
case Bool, Chan:
return []Style{Blue}
case RuneInt32, Byte, String:
return []Style{Yellow}
case Number:
return []Style{Green}
case Func:
return []Style{Magenta}
case Comment:
return []Style{White}
case Nil:
return []Style{Red}
case Error:
return []Style{Underline, Red}
default:
return []Style{None}
}
}
// ThemeNone colors for Sprint
var ThemeNone = func(t Type) []Style {
return []Style{None}
}
// F is a shortcut for Format with color
func F(v interface{}) string {
return Format(Tokenize(v), ThemeDefault)
}
// P pretty print the values
func P(values ...interface{}) error {
list := []interface{}{}
for _, v := range values {
list = append(list, F(v))
}
pc, file, line, _ := runtime.Caller(1)
fn := runtime.FuncForPC(pc).Name()
cwd, _ := os.Getwd()
file, _ = filepath.Rel(cwd, file)
tpl := Stylize("// %s %s:%d (%s)\n", ThemeDefault(Comment))
_, _ = fmt.Fprintf(Stdout, tpl, time.Now().Format(time.RFC3339Nano), file, line, fn)
_, err := fmt.Fprintln(Stdout, list...)
return err
}
// Plain is a shortcut for Format with plain color
func Plain(v interface{}) string {
return Format(Tokenize(v), ThemeNone)
}
// indentCache holds pre-repeated indent strings to avoid calling
// strings.Repeat for every indented line.
var indentCache = func() []string {
out := make([]string, 33)
for i := range out {
out[i] = strings.Repeat(indentUnit, i)
}
return out
}()
func writeIndent(sb *strings.Builder, depth int) {
if depth < len(indentCache) {
sb.WriteString(indentCache[depth])
return
}
for i := 0; i < depth; i++ {
sb.WriteString(indentUnit)
}
}
// Format a list of tokens.
func Format(ts []Token, theme Theme) string {
var out strings.Builder
depth := 0
for i, t := range ts {
tt := t.Type()
if oneOf(tt, SliceOpen, MapOpen, StructOpen) {
depth++
}
if i < len(ts)-1 && oneOf(ts[i+1].Type(), SliceClose, MapClose, StructClose) {
depth--
}
styles := theme(tt)
switch tt {
case SliceOpen, MapOpen, StructOpen:
buildStyled(&out, t, styles)
out.WriteByte('\n')
case SliceItem, MapKey, StructKey:
writeIndent(&out, depth)
case Colon, InlineComma, Chan:
buildStyled(&out, t, styles)
out.WriteByte(' ')
case Comma:
buildStyled(&out, t, styles)
out.WriteByte('\n')
case SliceClose, MapClose, StructClose:
s := out.String()
if strings.HasSuffix(s, "{\n") {
out.Reset()
out.WriteString(s[:len(s)-1])
buildStyled(&out, t, styles)
} else {
writeIndent(&out, depth)
buildStyled(&out, t, styles)
}
case String:
writeReadableString(&out, t, depth, styles)
default:
buildStyled(&out, t, styles)
}
}
return out.String()
}
// buildStyled renders t into sb, applying styles when any are active.
// Non-Lit tokens always produce single-line output, so we can emit the
// escape sequences around t.Build directly and skip the temp builder.
func buildStyled(sb *strings.Builder, t Token, styles []Style) {
if NoStyle || !hasActiveStyle(styles) {
t.Build(sb)
return
}
if l, ok := t.(*Lit); ok {
Render(sb, l.L, styles)
return
}
for i := len(styles) - 1; i >= 0; i-- {
if styles[i] != None {
sb.WriteString(styles[i].Set)
}
}
t.Build(sb)
for _, s := range styles {
if s != None {
sb.WriteString(s.Unset)
}
}
}
// writeReadableString handles the String-type token path: it materializes
// the raw literal, reshapes it via readableStr, then stylizes the result.
func writeReadableString(sb *strings.Builder, t Token, depth int, styles []Style) {
var raw string
if l, ok := t.(*Lit); ok {
raw = l.L
} else {
var inner strings.Builder
t.Build(&inner)
raw = inner.String()
}
s := readableStr(depth, raw)
if NoStyle || !hasActiveStyle(styles) {
sb.WriteString(s)
return
}
Render(sb, s, styles)
}
func oneOf(t Type, list ...Type) bool {
for _, el := range list {
if t == el {
return true
}
}
return false
}
// To make multi-line string block more human readable.
// Split newline into two strings, convert "\t" into tab.
// Such as format string: "line one \n\t line two" into:
//
// "line one \n" +
// " line two"
func readableStr(depth int, s string) string {
if (strings.Contains(s, "\n") || strings.Contains(s, `"`)) && !strings.Contains(s, "`") {
return "`" + s + "`"
}
s = fmt.Sprintf("%#v", s)
s, _ = replaceEscaped(s, 't', " ")
indent := strings.Repeat(indentUnit, depth+1)
if n, has := replaceEscaped(s, 'n', "\\n\" +\n"+indent+"\""); has {
return "\"\" +\n" + indent + n
}
return s
}
// We use a simple state machine to replace escaped char like "\n"
func replaceEscaped(s string, escaped rune, new string) (string, bool) {
type State int
const (
init State = iota
preMatch
match
)
state := init
var out strings.Builder
var buf strings.Builder
has := false
onInit := func(r rune) {
state = init
out.WriteString(buf.String())
out.WriteRune(r)
buf.Reset()
}
onPreMatch := func() {
state = preMatch
buf.Reset()
buf.WriteString("\\")
}
onEscape := func() {
state = match
out.WriteString(new)
buf.Reset()
has = true
}
for _, r := range s {
switch state {
case preMatch:
switch r {
case escaped:
onEscape()
default:
onInit(r)
}
case match:
switch r {
case '\\':
onPreMatch()
default:
onInit(r)
}
default:
switch r {
case '\\':
onPreMatch()
default:
onInit(r)
}
}
}
return out.String(), has
}
golang-github-ysmood-gop-0.4.1/format_test.go 0000664 0000000 0000000 00000040057 15206753664 0021240 0 ustar 00root root 0000000 0000000 package gop_test
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"text/template"
"time"
"unsafe"
"github.com/ysmood/gop"
)
func eq(t *testing.T, a, b interface{}) {
t.Helper()
if a == b {
return
}
t.Log(a, "[should equal]", b)
t.Fail()
}
// RandStr generates a random string with the specified length
func randStr(l int) string {
b := make([]byte, (l+1)/2)
_, _ = rand.Read(b)
return hex.EncodeToString(b)[:l]
}
func render(value string, data interface{}) string {
out := bytes.NewBuffer(nil)
t := template.New("")
t, err := t.Parse(value)
if err != nil {
panic(err)
}
err = t.Execute(out, data)
if err != nil {
panic(err)
}
return out.String()
}
func assertPanic(t *testing.T, fn func()) (val interface{}) {
t.Helper()
defer func() {
t.Helper()
val = recover()
if val == nil {
t.Error("should panic")
}
}()
fn()
return
}
func TestStyle(t *testing.T) {
s := gop.Style{Set: "", Unset: ""}
eq(t, gop.S("test", s), "test")
eq(t, gop.S("", s), "")
eq(t, gop.S("", gop.None), "")
}
func TestTokenize(t *testing.T) {
ref := "test"
timeStamp, _ := time.Parse(time.RFC3339Nano, "2021-08-28T08:36:36.807908+08:00")
fn := func(string) int { return 10 }
ch1 := make(chan int)
ch2 := make(chan string, 3)
ch3 := make(chan struct{})
v := []interface{}{
nil,
[]int{},
[]interface{}{true, false, uintptr(0x17), float32(100.121111133)},
true, 10, int8(2), int32(100),
float64(100.121111133),
complex64(1 + 2i), complex128(1 + 2i),
[3]int{1, 2},
ch1,
ch2,
ch3,
fn,
map[interface{}]interface{}{
`"test"`: 10,
"a": 1,
&ref: 1,
},
unsafe.Pointer(&ref),
struct {
Int int
str string
M map[int]int
}{10, "ok", map[int]int{1: 0x20}},
[]byte("aa\xe2"),
[]byte("bytes\n\tbytes"),
[]byte("long long long long string"),
byte('a'),
byte(1),
'天',
"long long long long string",
"\ntest",
"\t\n`",
&ref,
(*struct{ Int int })(nil),
&struct{ Int int }{},
&map[int]int{1: 2, 3: 4},
&[]int{1, 2},
&[2]int{1, 2},
&[]byte{1, 2},
timeStamp,
time.Hour,
`{"a": 1}`,
[]byte(`{"a": 1}`),
}
check := func(out string, file string) {
t.Helper()
tpl, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
expected := render(string(tpl), map[string]interface{}{
"ch1": fmt.Sprintf("0x%x", reflect.ValueOf(ch1).Pointer()),
"ch2": fmt.Sprintf("0x%x", reflect.ValueOf(ch2).Pointer()),
"ch3": fmt.Sprintf("0x%x", reflect.ValueOf(ch3).Pointer()),
"fn": fmt.Sprintf("0x%x", reflect.ValueOf(fn).Pointer()),
"ptr": fmt.Sprintf("%v", &ref),
"ref": fmt.Sprintf("0x%x", reflect.ValueOf(&ref).Pointer()),
})
if out != expected {
t.Log("check failed")
writeFile(t, "tmp/expected.txt", expected)
writeFile(t, "tmp/out.txt", out)
t.Fail()
}
}
out := gop.StripANSI(gop.F(v))
{
b, err := os.ReadFile(filepath.Join("fixtures", "compile_check.go.tmpl"))
if err != nil {
t.Fatal(err)
}
code := fmt.Sprintf(string(b), out)
f := filepath.Join("tmp", randStr(8), "main.go")
err = os.MkdirAll(filepath.Dir(f), 0755)
if err != nil {
t.Fatal(err)
}
writeFile(t, f, code)
b, err = exec.Command("go", "run", f).CombinedOutput()
if err != nil {
t.Fatal(string(b))
}
}
check(out, filepath.Join("fixtures", "expected.tmpl"))
out = gop.VisualizeANSI(gop.F(v))
check(out, filepath.Join("fixtures", "expected_with_color.tmpl"))
}
func TestRef(t *testing.T) {
a := [2][]int{{1}}
a[1] = a[0]
eq(t, gop.Plain(a), `[2][]int{
[]int{
1,
},
[]int{
1,
},
}`)
}
type A struct {
Int int
B *B
}
type B struct {
s string
a *A
}
func TestCircularRef(t *testing.T) {
a := A{Int: 10}
b := B{"test", &a}
a.B = &b
eq(t, gop.StripANSI(gop.F(a)), `gop_test.A{
Int: 10,
B: &gop_test.B{
s: "test",
a: &gop_test.A{
Int: 10,
B: gop.Circular("B").(*gop_test.B),
},
},
}`)
}
func TestCircularNilRef(t *testing.T) {
arr := []A{{}, {}}
eq(t, gop.StripANSI(gop.F(arr)), `[]gop_test.A{
gop_test.A{
Int: 0,
B: (*gop_test.B)(nil),
},
gop_test.A{
Int: 0,
B: (*gop_test.B)(nil),
},
}`)
}
func TestCircularMap(t *testing.T) {
a := map[int]interface{}{}
a[0] = a
ts := gop.Tokenize(a)
eq(t, gop.Format(ts, gop.ThemeNone), `map[int]interface {}{
0: gop.Circular().(map[int]interface {}),
}`)
}
func TestCircularSlice(t *testing.T) {
a := [][]interface{}{{nil}, {nil}}
a[0][0] = a[1]
a[1][0] = a[0][0]
ts := gop.Tokenize(a)
eq(t, gop.Format(ts, gop.ThemeNone), `[][]interface {}{
[]interface {}{
[]interface {}{
gop.Circular(0, 0).([]interface {}),
},
},
[]interface {}{
gop.Circular(1).([]interface {}),
},
}`)
}
func TestCircularMapKey(t *testing.T) {
a := map[interface{}]interface{}{}
b := map[interface{}]interface{}{}
a[&b] = b
b[&a] = a
ts := gop.Tokenize(b)
eq(t, gop.Format(ts, gop.ThemeNone), render(`map[interface {}]interface {}{
(interface {})(nil)/* {{.a}} */: map[interface {}]interface {}{
(interface {})(nil)/* {{.b}} */: gop.Circular().(map[interface {}]interface {}),
},
}`, map[string]interface{}{
"a": fmt.Sprintf("0x%x", reflect.ValueOf(&a).Pointer()),
"b": fmt.Sprintf("0x%x", reflect.ValueOf(&b).Pointer()),
}))
}
func TestPlain(t *testing.T) {
eq(t, gop.Plain(10), "10")
}
func TestPlainMinify(t *testing.T) {
a := map[int]interface{}{
1: "a",
}
b := map[int]interface{}{}
pa := gop.Plain(a)
pb := gop.Plain(b)
eq(t, pa, "map[int]interface {}{\n 1: \"a\",\n}")
eq(t, pb, "map[int]interface {}{}")
}
func TestP(t *testing.T) {
gop.Stdout = io.Discard
_ = gop.P("test")
gop.Stdout = os.Stdout
}
func TestConvertors(t *testing.T) {
eq(t, gop.Circular(""), nil)
s := randStr(8)
eq(t, *gop.Ptr(s).(*string), s)
bs := base64.StdEncoding.EncodeToString([]byte(s))
eq(t, string(gop.Base64(bs)), string([]byte(s)))
now := time.Now()
eq(t, gop.Time(now.Format(time.RFC3339Nano), 1234).Unix(), now.Unix())
eq(t, gop.Duration("10m"), 10*time.Minute)
eq(t, gop.JSONStr(nil, "[1, 2]"), "[1, 2]")
eq(t, string(gop.JSONBytes(nil, "[1, 2]")), string([]byte("[1, 2]")))
eq(t, gop.Rune(100, 'd'), 'd')
eq(t, gop.Rune(100, 'd'), int32(100))
eq(t, gop.GopError("test").Error(), "test")
}
func TestGetPrivateFieldErr(t *testing.T) {
assertPanic(t, func() {
gop.GetPrivateField(reflect.ValueOf(1), 0)
})
assertPanic(t, func() {
gop.GetPrivateFieldByName(reflect.ValueOf(1), "test")
})
}
func TestTypeName(t *testing.T) {
type f float64
type i int
type c complex128
type b byte
eq(t, gop.Plain(f(1)), "gop_test.f(1.0)")
eq(t, gop.Plain(i(1)), "gop_test.i(1)")
eq(t, gop.Plain(c(1)), "gop_test.c(1+0i)")
eq(t, gop.Plain(b('a')), "gop_test.b(97)")
}
func TestFixNestedStyle(t *testing.T) {
s := gop.S(" 0 "+gop.S(" 1 "+
gop.S(" 2 "+
gop.S(" 3 ", gop.Cyan)+
" 4 ", gop.Blue)+
" 5 ", gop.Red)+" 6 ", gop.BgRed)
out := gop.VisualizeANSI(gop.FixNestedStyle(s))
eq(t, out, `<41> 0 <31> 1 <39><34> 2 <39><36> 3 <39><34> 4 <39><31> 5 <39> 6 <49>`)
gop.FixNestedStyle("test")
}
func TestStripANSI(t *testing.T) {
eq(t, gop.StripANSI(gop.S("test", gop.Red)), "test")
}
func TestTheme(t *testing.T) {
eq(t, gop.ThemeDefault(gop.Error)[0], gop.Underline)
}
func TestNil(t *testing.T) {
eq(t, gop.Plain(map[string]string(nil)), "map[string]string(nil)")
eq(t, gop.Plain(chan int(nil)), "(chan int)(nil)")
eq(t, gop.Plain([]string(nil)), "[]string(nil)")
eq(t, gop.Plain((func())(nil)), "(func())(nil)")
eq(t, gop.Plain((*struct{})(nil)), "(*struct {})(nil)")
}
func TestJSONArrayBug(t *testing.T) {
// Test for the bug fix where JSON arrays were incorrectly checked as objects
// The bug was: _, isArr := jv.(map[string]interface{}) instead of jv.([]interface{})
// This caused JSON arrays to never be recognized, so they weren't properly formatted
jsonArrayStr := `[1, 2, 3]`
result := gop.Plain(jsonArrayStr)
// With the fix, JSON arrays should be detected and formatted with gop.JSONStr()
// The result should contain "gop.JSONStr(" indicating proper JSON array detection
if !strings.Contains(result, "gop.JSONStr(") {
t.Errorf("JSON array not properly detected as JSON. Got: %s", result)
}
// Test with byte array too
jsonArrayBytes := []byte(`[{"key": "value"}, 123]`)
result2 := gop.Plain(jsonArrayBytes)
// With the fix, JSON byte arrays should be detected and formatted with gop.JSONBytes()
if !strings.Contains(result2, "gop.JSONBytes(") {
t.Errorf("JSON byte array not properly detected as JSON. Got: %s", result2)
}
// Test that the tokenized version contains the actual parsed array structure
if !strings.Contains(result, "[]interface {}") {
t.Errorf("JSON array structure not properly tokenized. Got: %s", result)
}
}
func TestMaxDepth(t *testing.T) {
// Create a deeply nested structure
type Node struct {
Value int
Next *Node
}
// Create a chain of 20 nodes
var root *Node
current := &Node{Value: 0}
root = current
for i := 1; i < 20; i++ {
current.Next = &Node{Value: i}
current = current.Next
}
// Test with default MaxDepth (15)
result := gop.Plain(root)
if !strings.Contains(result, "gop.GopError(") {
t.Errorf("Expected max depth error with default MaxDepth, got: %s", result)
}
if !strings.Contains(result, "max depth exceeded") {
t.Errorf("Expected 'max depth exceeded' message, got: %s", result)
}
// Test with custom MaxDepth of 5
tokens := gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 5})
result = gop.Format(tokens, gop.ThemeNone)
if !strings.Contains(result, "gop.GopError(") {
t.Errorf("Expected max depth error with MaxDepth=5, got: %s", result)
}
if !strings.Contains(result, "max depth exceeded") {
t.Errorf("Expected 'max depth exceeded' message with MaxDepth=5, got: %s", result)
}
// Test with MaxDepth of 0 (no limit)
tokens = gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 0})
result = gop.Format(tokens, gop.ThemeNone)
if strings.Contains(result, "max depth exceeded") {
t.Errorf("Should not have max depth error with MaxDepth=0, got: %s", result)
}
// Verify all 20 values are present when no limit
for i := 0; i < 20; i++ {
if !strings.Contains(result, fmt.Sprintf("Value: %d", i)) {
t.Errorf("Missing Value: %d in unlimited depth output", i)
}
}
// Test with deeply nested maps
deepMap := map[string]interface{}{
"level1": map[string]interface{}{
"level2": map[string]interface{}{
"level3": map[string]interface{}{
"level4": map[string]interface{}{
"level5": map[string]interface{}{
"level6": "deep value",
},
},
},
},
},
}
// Test with MaxDepth of 3
tokens = gop.TokenizeWithOptions(deepMap, gop.Options{MaxDepth: 3})
result = gop.Format(tokens, gop.ThemeNone)
if !strings.Contains(result, "gop.GopError(") {
t.Errorf("Expected max depth error with nested maps at MaxDepth=3, got: %s", result)
}
// Test with MaxDepth of 10 (should be enough for this structure)
tokens = gop.TokenizeWithOptions(deepMap, gop.Options{MaxDepth: 10})
result = gop.Format(tokens, gop.ThemeNone)
if strings.Contains(result, "max depth exceeded") {
t.Errorf("Should not have max depth error with MaxDepth=10 for this structure, got: %s", result)
}
if !strings.Contains(result, "deep value") {
t.Errorf("Should contain 'deep value' with sufficient MaxDepth, got: %s", result)
}
// Test with deeply nested slices
var deepSlice interface{} = []interface{}{
[]interface{}{
[]interface{}{
[]interface{}{
[]interface{}{
[]interface{}{
"deeply nested",
},
},
},
},
},
}
tokens = gop.TokenizeWithOptions(deepSlice, gop.Options{MaxDepth: 4})
result = gop.Format(tokens, gop.ThemeNone)
if !strings.Contains(result, "gop.GopError(") {
t.Errorf("Expected max depth error with nested slices at MaxDepth=4, got: %s", result)
}
}
func writeFile(t *testing.T, f, code string) {
err := os.WriteFile(f, []byte(code), 0644)
if err != nil {
t.Fatal(err)
}
}
// stringTok is a non-Lit Token that reports String type. It exists so
// tests can exercise the writeReadableString non-Lit branch.
type stringTok struct{ s string }
func (stringTok) Type() gop.Type { return gop.String }
func (t stringTok) Build(sb *strings.Builder) { sb.WriteString(t.s) }
func TestStyledToken(t *testing.T) {
var sb strings.Builder
// Nil Inner: Type returns Nil, Build writes nothing.
empty := gop.Styled{}
eq(t, empty.Type(), gop.Nil)
empty.Build(&sb)
eq(t, sb.String(), "")
// Non-nil Inner with no active styles: writes inner verbatim.
sb.Reset()
plain := gop.Styled{Inner: &gop.Lit{T: gop.String, L: "hi"}}
eq(t, plain.Type(), gop.String)
plain.Build(&sb)
eq(t, sb.String(), "hi")
// Lit fast path with active style.
sb.Reset()
litStyled := gop.Styled{
Inner: &gop.Lit{T: gop.String, L: "hi"},
Styles: []gop.Style{gop.Red},
}
litStyled.Build(&sb)
eq(t, sb.String(), gop.Red.Set+"hi"+gop.Red.Unset)
// Non-Lit inner with active style: goes through the temp builder path.
sb.Reset()
nonLitStyled := gop.Styled{
Inner: stringTok{s: "hi"},
Styles: []gop.Style{gop.Red},
}
nonLitStyled.Build(&sb)
eq(t, sb.String(), gop.Red.Set+"hi"+gop.Red.Unset)
}
func TestRenderNoStyle(t *testing.T) {
orig := gop.NoStyle
gop.NoStyle = true
defer func() { gop.NoStyle = orig }()
var sb strings.Builder
gop.Render(&sb, "hi", []gop.Style{gop.Red})
eq(t, sb.String(), "hi")
}
func TestRenderMultilineSkipsNone(t *testing.T) {
var sb strings.Builder
// Multi-line path with a None entry that must be skipped without altering output.
gop.Render(&sb, "a\nb", []gop.Style{gop.None, gop.Red})
eq(t, sb.String(), gop.Red.Set+"a"+gop.Red.Unset+"\n"+gop.Red.Set+"b"+gop.Red.Unset)
}
func TestRenderMultilineCRLF(t *testing.T) {
// CRLF input exercises the \r-trim branch in Render and the \r\n
// return in firstNewline; a leading newline also exercises the idx==0
// segment path.
var sb strings.Builder
gop.Render(&sb, "\r\na\r\nb", []gop.Style{gop.Red})
wrap := func(s string) string { return gop.Red.Set + s + gop.Red.Unset }
eq(t, sb.String(), wrap("")+"\r\n"+wrap("a")+"\r\n"+wrap("b"))
}
func TestWriteIndentDeep(t *testing.T) {
// Build a chain of pointers deeper than the indentCache (33 levels)
// so Format's indent writer falls through to the per-level loop.
type Node struct{ Next *Node }
root := &Node{}
cur := root
for i := 0; i < 40; i++ {
cur.Next = &Node{}
cur = cur.Next
}
tokens := gop.TokenizeWithOptions(root, gop.Options{MaxDepth: 0})
out := gop.Format(tokens, gop.ThemeNone)
// The deepest "Next:" field should be indented well beyond 33 levels.
deepIndent := strings.Repeat(" ", 40) + "Next:"
if !strings.Contains(out, deepIndent) {
t.Errorf("expected deep indent past the cache, got:\n%s", out)
}
}
func TestFormatNonLitStringToken(t *testing.T) {
// writeReadableString has a non-Lit branch reached only when a
// caller passes a Token with Type()==String that isn't a *Lit.
tokens := []gop.Token{stringTok{s: "hi"}}
eq(t, gop.Format(tokens, gop.ThemeNone), `"hi"`)
// With an active theme it also exercises the Render path.
themed := gop.Format(tokens, func(tp gop.Type) []gop.Style {
if tp == gop.String {
return []gop.Style{gop.Red}
}
return []gop.Style{gop.None}
})
eq(t, themed, gop.Red.Set+`"hi"`+gop.Red.Unset)
}
func TestTokenizeNumberAllKinds(t *testing.T) {
type myInt int
type myInt8 int8
type myUint uint
type myFloat64 float64
type myComplex128 complex128
type myUintptr uintptr
cases := []struct {
v interface{}
want string
}{
{int(1), "1"},
{myInt(1), "gop_test.myInt(1)"},
{int8(1), "int8(1)"},
{myInt8(1), "gop_test.myInt8(1)"},
{uint(1), "uint(1)"},
{myUint(1), "gop_test.myUint(1)"},
{uintptr(1), "uintptr(1)"},
{myUintptr(1), "gop_test.myUintptr(1)"},
{float32(1), "float32(1)"},
{float64(1), "1.0"},
{myFloat64(1), "gop_test.myFloat64(1.0)"},
{complex64(1 + 2i), "complex64(1+2i)"},
{complex128(1 + 2i), "1+2i"},
{myComplex128(1 + 2i), "gop_test.myComplex128(1+2i)"},
}
for _, c := range cases {
got := gop.Plain(c.v)
if got != c.want {
t.Errorf("gop.Plain(%v) = %q, want %q", c.v, got, c.want)
}
}
}
golang-github-ysmood-gop-0.4.1/go.mod 0000664 0000000 0000000 00000000047 15206753664 0017463 0 ustar 00root root 0000000 0000000 module github.com/ysmood/gop
go 1.19
golang-github-ysmood-gop-0.4.1/style.go 0000664 0000000 0000000 00000013430 15206753664 0020044 0 ustar 00root root 0000000 0000000 package gop
import (
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)
// Style type
type Style struct {
Set string
Unset string
}
var (
// Bold style
Bold = addStyle(1, 22)
// Faint style
Faint = addStyle(2, 22)
// Italic style
Italic = addStyle(3, 23)
// Underline style
Underline = addStyle(4, 24)
// Blink style
Blink = addStyle(5, 25)
// RapidBlink style
RapidBlink = addStyle(6, 26)
// Invert style
Invert = addStyle(7, 27)
// Hide style
Hide = addStyle(8, 28)
// Strike style
Strike = addStyle(9, 29)
// Black color
Black = addStyle(30, 39)
// Red color
Red = addStyle(31, 39)
// Green color
Green = addStyle(32, 39)
// Yellow color
Yellow = addStyle(33, 39)
// Blue color
Blue = addStyle(34, 39)
// Magenta color
Magenta = addStyle(35, 39)
// Cyan color
Cyan = addStyle(36, 39)
// White color
White = addStyle(37, 39)
// BgBlack color
BgBlack = addStyle(40, 49)
// BgRed color
BgRed = addStyle(41, 49)
// BgGreen color
BgGreen = addStyle(42, 49)
// BgYellow color
BgYellow = addStyle(43, 49)
// BgBlue color
BgBlue = addStyle(44, 49)
// BgMagenta color
BgMagenta = addStyle(45, 49)
// BgCyan color
BgCyan = addStyle(46, 49)
// BgWhite color
BgWhite = addStyle(47, 49)
// None type
None = Style{}
)
// Styled wraps an inner Token and applies the given Styles when built.
// It is the token form of the legacy Stylize helper.
type Styled struct {
Inner Token
Styles []Style
}
// Type returns the inner token type.
func (s Styled) Type() Type {
if s.Inner == nil {
return Nil
}
return s.Inner.Type()
}
// Build writes the styled rendering of the inner token to sb.
func (s Styled) Build(sb *strings.Builder) {
if s.Inner == nil {
return
}
if NoStyle || !hasActiveStyle(s.Styles) {
s.Inner.Build(sb)
return
}
// Fast path: a Lit already holds its string, skip the temp builder.
if l, ok := s.Inner.(*Lit); ok {
Render(sb, l.L, s.Styles)
return
}
var inner strings.Builder
s.Inner.Build(&inner)
Render(sb, inner.String(), s.Styles)
}
func hasActiveStyle(styles []Style) bool {
for _, s := range styles {
if s != None {
return true
}
}
return false
}
// Render writes the stylized form of str to sb without allocating
// intermediate strings: both the single-line and multi-line paths stream
// directly into sb.
func Render(sb *strings.Builder, str string, styles []Style) {
if NoStyle || !hasActiveStyle(styles) {
sb.WriteString(str)
return
}
if !strings.ContainsAny(str, "\r\n") {
writeStyleSets(sb, styles)
sb.WriteString(str)
writeStyleUnsets(sb, styles)
return
}
newline := firstNewline(str)
remaining := str
first := true
for {
idx := strings.IndexByte(remaining, '\n')
if idx < 0 {
if !first {
sb.WriteString(newline)
}
writeStyleSets(sb, styles)
sb.WriteString(remaining)
writeStyleUnsets(sb, styles)
return
}
end := idx
if idx > 0 && remaining[idx-1] == '\r' {
end = idx - 1
}
if !first {
sb.WriteString(newline)
}
first = false
writeStyleSets(sb, styles)
sb.WriteString(remaining[:end])
writeStyleUnsets(sb, styles)
remaining = remaining[idx+1:]
}
}
func writeStyleSets(sb *strings.Builder, styles []Style) {
for i := len(styles) - 1; i >= 0; i-- {
if styles[i] != None {
sb.WriteString(styles[i].Set)
}
}
}
func writeStyleUnsets(sb *strings.Builder, styles []Style) {
for _, s := range styles {
if s != None {
sb.WriteString(s.Unset)
}
}
}
func firstNewline(s string) string {
idx := strings.IndexByte(s, '\n')
if idx > 0 && s[idx-1] == '\r' {
return "\r\n"
}
return "\n"
}
// S is the shortcut for Stylize.
func S(str string, styles ...Style) string {
return Stylize(str, styles)
}
// Stylize wraps str with the given styles.
func Stylize(str string, styles []Style) string {
if NoStyle || !hasActiveStyle(styles) {
return str
}
var sb strings.Builder
Render(&sb, str, styles)
return sb.String()
}
// NoStyle respects https://no-color.org/ and "tput colors"
var NoStyle = func() bool {
_, noColor := os.LookupEnv("NO_COLOR")
b, _ := exec.Command("tput", "colors").CombinedOutput()
n, _ := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 32)
return noColor || n == 0
}()
// RegANSI token
var RegANSI = regexp.MustCompile(`\x1b\[\d+m`)
// StripANSI tokens
func StripANSI(str string) string {
return RegANSI.ReplaceAllString(str, "")
}
var regNum = regexp.MustCompile(`\d+`)
// VisualizeANSI tokens
func VisualizeANSI(str string) string {
return RegANSI.ReplaceAllStringFunc(str, func(s string) string {
return "<" + regNum.FindString(s) + ">"
})
}
// FixNestedStyle like
//
// 123>4>5>
//
// into
//
// 1>2>3>4>5>
func FixNestedStyle(s string) string {
out := ""
stacks := map[string][]string{}
i := 0
l := 0
r := 0
for i < len(s) {
loc := RegANSI.FindStringIndex(s[i:])
if loc == nil {
break
}
l, r = i+loc[0], i+loc[1]
token := s[l:r]
out += s[i:l]
unset := GetStyle(token).Unset
if unset == "" {
unset = token
}
if _, has := stacks[unset]; !has {
stacks[unset] = []string{}
}
stack := stacks[unset]
if len(stack) == 0 {
stack = append(stack, token)
out += token
} else {
if token == GetStyle(last(stack)).Unset {
out += token
stack = stack[:len(stack)-1]
if len(stack) > 0 {
out += last(stack)
}
} else {
out += GetStyle(last(stack)).Unset
stack = append(stack, token)
out += token
}
}
stacks[unset] = stack
i = r
}
return out + s[i:]
}
// GetStyle from available styles
func GetStyle(s string) Style {
return styleSetMap[s]
}
func last(list []string) string {
return list[len(list)-1]
}
var styleSetMap = map[string]Style{}
func addStyle(set, unset int) Style {
s := Style{
fmt.Sprintf("\x1b[%dm", set),
fmt.Sprintf("\x1b[%dm", unset),
}
styleSetMap[s.Set] = s
return s
}
golang-github-ysmood-gop-0.4.1/token.go 0000664 0000000 0000000 00000041661 15206753664 0020033 0 ustar 00root root 0000000 0000000 package gop
import (
"encoding/base64"
"encoding/json"
"reflect"
"sort"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
)
// Type of token
type Type int
const (
// Nil type
Nil Type = iota
// Bool type
Bool
// Number type
Number
// Float type
Float
// Complex type
Complex
// String type
String
// Byte type
Byte
// RuneInt32 type
RuneInt32
// Chan type
Chan
// Func type
Func
// Error type
Error
// Comment type
Comment
// TypeName type
TypeName
// ParenOpen type
ParenOpen
// ParenClose type
ParenClose
// Dot type
Dot
// And type
And
// SliceOpen type
SliceOpen
// SliceItem type
SliceItem
// InlineComma type
InlineComma
// Comma type
Comma
// SliceClose type
SliceClose
// MapOpen type
MapOpen
// MapKey type
MapKey
// Colon type
Colon
// MapClose type
MapClose
// StructOpen type
StructOpen
// StructKey type
StructKey
// StructField type
StructField
// StructClose type
StructClose
)
// Token represents a symbol in value layout. Build appends the
// token's rendered form to sb so the literal can be computed
// lazily and avoid allocating intermediate strings.
type Token interface {
Type() Type
Build(sb *strings.Builder)
}
// Lit is a token with a fixed literal string.
type Lit struct {
T Type
L string
}
// Type returns the token type.
func (l *Lit) Type() Type { return l.T }
// Build writes the literal to sb.
func (l *Lit) Build(sb *strings.Builder) { sb.WriteString(l.L) }
// IntTok lazily renders a signed integer as a Number token.
type IntTok int64
// Type returns Number.
func (IntTok) Type() Type { return Number }
// Build appends the decimal representation to sb.
func (n IntTok) Build(sb *strings.Builder) {
var buf [20]byte
sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
}
// UintTok lazily renders an unsigned integer as a Number token.
type UintTok uint64
// Type returns Number.
func (UintTok) Type() Type { return Number }
// Build appends the decimal representation to sb.
func (n UintTok) Build(sb *strings.Builder) {
var buf [20]byte
sb.Write(strconv.AppendUint(buf[:0], uint64(n), 10))
}
// Float64Tok lazily renders a float64 as a Number token, appending ".0" when the value is integral.
type Float64Tok float64
// Type returns Number.
func (Float64Tok) Type() Type { return Number }
// Build appends the formatted value to sb.
func (f Float64Tok) Build(sb *strings.Builder) {
var buf [32]byte
s := strconv.AppendFloat(buf[:0], float64(f), 'f', -1, 64)
sb.Write(s)
hasDot := false
for _, b := range s {
if b == '.' {
hasDot = true
break
}
}
if !hasDot {
sb.WriteString(".0")
}
}
// FloatTok lazily renders a float at the given bit size as a Number token.
type FloatTok struct {
V float64
Bits int
}
// Type returns Number.
func (FloatTok) Type() Type { return Number }
// Build appends the formatted value to sb.
func (f FloatTok) Build(sb *strings.Builder) {
var buf [32]byte
sb.Write(strconv.AppendFloat(buf[:0], f.V, 'f', -1, f.Bits))
}
// ComplexTok lazily renders a complex value at the given bit size as a Number token,
// stripping the parentheses that strconv.FormatComplex adds.
type ComplexTok struct {
V complex128
Bits int
}
// Type returns Number.
func (ComplexTok) Type() Type { return Number }
// Build appends the formatted value to sb.
func (c ComplexTok) Build(sb *strings.Builder) {
s := strconv.FormatComplex(c.V, 'f', -1, c.Bits)
sb.WriteString(s[1 : len(s)-1])
}
// PtrTok lazily renders a uintptr as a Number token in 0xHEX form.
type PtrTok uintptr
// Type returns Number.
func (PtrTok) Type() Type { return Number }
// Build appends the hex representation to sb.
func (p PtrTok) Build(sb *strings.Builder) {
sb.WriteString("0x")
var buf [20]byte
sb.Write(strconv.AppendUint(buf[:0], uint64(p), 16))
}
// CommentPtrTok lazily renders a uintptr as a Comment token in /* 0xHEX */ form.
type CommentPtrTok uintptr
// Type returns Comment.
func (CommentPtrTok) Type() Type { return Comment }
// Build appends the wrapped hex representation to sb.
func (p CommentPtrTok) Build(sb *strings.Builder) {
sb.WriteString("/* 0x")
var buf [20]byte
sb.Write(strconv.AppendUint(buf[:0], uint64(p), 16))
sb.WriteString(" */")
}
// RuneTok lazily renders a rune as a RuneInt32 token (quoted).
type RuneTok rune
// Type returns RuneInt32.
func (RuneTok) Type() Type { return RuneInt32 }
// Build appends the quoted rune to sb.
func (r RuneTok) Build(sb *strings.Builder) {
sb.WriteString(strconv.QuoteRune(rune(r)))
}
// ByteTok lazily renders a byte as a Byte token, quoted when graphic else as 0xHEX.
type ByteTok byte
// Type returns Byte.
func (ByteTok) Type() Type { return Byte }
// Build appends the rendered byte to sb.
func (b ByteTok) Build(sb *strings.Builder) {
r := rune(b)
if unicode.IsGraphic(r) {
sb.WriteString(strconv.QuoteRune(r))
return
}
sb.WriteString("0x")
var buf [4]byte
sb.Write(strconv.AppendUint(buf[:0], uint64(b), 16))
}
// Pre-allocated singletons for common fixed-literal tokens. Reusing
// these avoids per-token heap allocations during tokenization.
var (
tokParenOpen Token = &Lit{ParenOpen, "("}
tokParenClose Token = &Lit{ParenClose, ")"}
tokDot Token = &Lit{Dot, "."}
tokAnd Token = &Lit{And, "&"}
tokSliceOpen Token = &Lit{SliceOpen, "{"}
tokSliceClose Token = &Lit{SliceClose, "}"}
tokSliceItem Token = &Lit{SliceItem, ""}
tokInlineComma Token = &Lit{InlineComma, ","}
tokComma Token = &Lit{Comma, ","}
tokMapOpen Token = &Lit{MapOpen, "{"}
tokMapClose Token = &Lit{MapClose, "}"}
tokMapKey Token = &Lit{MapKey, ""}
tokColon Token = &Lit{Colon, ":"}
tokStructOpen Token = &Lit{StructOpen, "{"}
tokStructClose Token = &Lit{StructClose, "}"}
tokChan Token = &Lit{Chan, "chan"}
tokNil Token = &Lit{Nil, "nil"}
tokTrue Token = &Lit{Bool, "true"}
tokFalse Token = &Lit{Bool, "false"}
tokFuncMake Token = &Lit{Func, "make"}
tokFuncCircular Token = &Lit{Func, SymbolCircular}
tokFuncGopError Token = &Lit{Func, SymbolGopError}
tokFuncBase64 Token = &Lit{Func, SymbolBase64}
tokFuncTime Token = &Lit{Func, SymbolTime}
tokFuncJSONStr Token = &Lit{Func, SymbolJSONStr}
tokFuncJSONBytes Token = &Lit{Func, SymbolJSONBytes}
tokFuncPtr Token = &Lit{Func, SymbolPtr}
tokStrMaxDepth Token = &Lit{String, "max depth exceeded"}
tokTNGopRune Token = &Lit{TypeName, "gop.Rune"}
tokTNByte Token = &Lit{TypeName, "byte"}
tokTNBytes Token = &Lit{TypeName, "[]byte"}
tokTNUnsafePtr Token = &Lit{TypeName, "unsafe.Pointer"}
tokTNUintptr Token = &Lit{TypeName, "uintptr"}
tokTNDuration Token = &Lit{TypeName, SymbolDuration}
)
// DefaultOptions for Tokenize.
var DefaultOptions = Options{
MaxDepth: 15,
}
// Tokenize a random Go value with [DefaultOptions].
func Tokenize(v interface{}) []Token {
return TokenizeWithOptions(v, DefaultOptions)
}
// Options controls tokenization.
type Options struct {
// MaxDepth limits the depth of tokenization for nested structures.
// If less than 1 there is no limit.
MaxDepth int
}
// TokenizeWithOptions tokenizes v with the given Options.
func TokenizeWithOptions(v interface{}, opts Options) []Token {
tz := tokenizer{Options: opts, global: map[uintptr]path{}, path: path{}}
return tz.tokenize(reflect.ValueOf(v))
}
func tokenize(v reflect.Value) []Token {
tz := tokenizer{global: map[uintptr]path{}, path: path{}}
return tz.tokenize(v)
}
type path []interface{}
func (p path) tokens(opts Options) []Token {
ts := []Token{}
for i, seg := range p {
ts = append(ts, TokenizeWithOptions(seg, opts)...)
if i < len(p)-1 {
ts = append(ts, tokInlineComma)
}
}
return ts
}
type tokenizer struct {
Options
global map[uintptr]path
path path
}
func (tz *tokenizer) push(p interface{}) {
if v := reflect.ValueOf(p); v.Kind() == reflect.Ptr {
p = v.Pointer()
}
tz.path = append(tz.path, p)
}
func (tz *tokenizer) pop() {
tz.path = tz.path[:len(tz.path)-1]
}
func (tz *tokenizer) circular(v reflect.Value) ([]Token, func()) {
cleanup := func() {}
switch v.Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice:
ptr := v.Pointer()
if ptr == 0 {
return nil, cleanup
}
if prev, has := tz.global[ptr]; has {
ts := []Token{tokFuncCircular, tokParenOpen}
ts = append(ts, prev.tokens(tz.Options)...)
return append(ts, tokParenClose, tokDot,
tokParenOpen, typeName(v.Type().String()), tokParenClose), cleanup
}
tz.global[ptr] = tz.path
cleanup = func() {
delete(tz.global, ptr)
}
}
return nil, cleanup
}
func (tz *tokenizer) tokenize(v reflect.Value) []Token {
if tz.MaxDepth > 0 && len(tz.path) >= tz.MaxDepth {
return []Token{tokFuncGopError, tokParenOpen, tokStrMaxDepth, tokParenClose}
}
if ts, has := tz.tokenizeSpecial(v); has {
return ts
}
{
ts, cleanup := tz.circular(v)
defer cleanup()
if ts != nil {
return ts
}
}
switch v.Kind() {
case reflect.Interface:
return tz.tokenize(v.Elem())
case reflect.Bool:
if v.Bool() {
return []Token{tokTrue}
}
return []Token{tokFalse}
case reflect.String:
return tokenizeString(v)
case reflect.Chan:
if v.IsNil() {
return []Token{tokParenOpen,
typeName(v.Type().String()), tokParenClose,
tokParenOpen, tokNil, tokParenClose}
}
if v.Cap() == 0 {
return []Token{tokFuncMake, tokParenOpen,
tokChan, typeName(v.Type().Elem().String()), tokParenClose,
CommentPtrTok(v.Pointer())}
}
return []Token{tokFuncMake, tokParenOpen, tokChan,
typeName(v.Type().Elem().String()), tokInlineComma,
IntTok(v.Cap()), tokParenClose,
CommentPtrTok(v.Pointer())}
case reflect.Func:
if v.IsNil() {
return []Token{tokParenOpen,
typeName(v.Type().String()), tokParenClose,
tokParenOpen, tokNil, tokParenClose}
}
return []Token{tokParenOpen, &Lit{TypeName, v.Type().String()},
tokParenClose, tokParenOpen, tokNil, tokParenClose,
CommentPtrTok(v.Pointer())}
case reflect.Ptr:
return tz.tokenizePtr(v)
case reflect.UnsafePointer:
return []Token{tokTNUnsafePtr, tokParenOpen, tokTNUintptr,
tokParenOpen, PtrTok(v.Pointer()), tokParenClose, tokParenClose}
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,
reflect.Uintptr, reflect.Complex64, reflect.Complex128:
return tokenizeNumber(v)
default:
// Slice, Array, Map, Struct — the remaining kinds reflect can surface here.
return tz.tokenizeCollection(v)
}
}
func (tz *tokenizer) tokenizeSpecial(v reflect.Value) ([]Token, bool) {
if v.Kind() == reflect.Invalid {
return []Token{tokNil}, true
} else if r, ok := v.Interface().(rune); ok && unicode.IsGraphic(r) {
return tokenizeRuneInt32(r), true
} else if b, ok := v.Interface().(byte); ok {
return tokenizeByte(b), true
} else if t, ok := v.Interface().(time.Time); ok {
return tokenizeTime(t), true
} else if d, ok := v.Interface().(time.Duration); ok {
return tokenizeDuration(d), true
}
return tz.tokenizeJSON(v)
}
func (tz *tokenizer) tokenizeCollection(v reflect.Value) []Token {
var ts []Token
switch v.Kind() {
case reflect.Slice, reflect.Array:
if v.Kind() == reflect.Slice && v.IsNil() {
return []Token{typeName(v.Type().String()), tokParenOpen, tokNil, tokParenClose}
}
if data, ok := v.Interface().([]byte); ok {
ts = tokenizeBytes(data)
break
}
ts = make([]Token, 0, v.Len()*4+3)
ts = append(ts, typeName(v.Type().String()))
ts = append(ts, tokSliceOpen)
for i := 0; i < v.Len(); i++ {
el := v.Index(i)
ts = append(ts, tokSliceItem)
tz.push(i)
ts = append(ts, tz.tokenize(el)...)
tz.pop()
ts = append(ts, tokComma)
}
ts = append(ts, tokSliceClose)
case reflect.Map:
if v.IsNil() {
return []Token{typeName(v.Type().String()), tokParenOpen, tokNil, tokParenClose}
}
ts = make([]Token, 0, v.Len()*6+3)
ts = append(ts, typeName(v.Type().String()))
keys := v.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return compare(keys[i].Interface(), keys[j].Interface()) < 0
})
ts = append(ts, tokMapOpen)
for _, k := range keys {
ts = append(ts, tokMapKey)
if k.Kind() == reflect.Interface && k.Elem().Kind() == reflect.Ptr {
ts = append(ts, tokenizeMapKey(k)...)
} else {
ts = append(ts, tokenize(k)...)
}
ts = append(ts, tokColon)
tz.push(k.Interface())
ts = append(ts, tz.tokenize(v.MapIndex(k))...)
tz.pop()
ts = append(ts, tokComma)
}
ts = append(ts, tokMapClose)
case reflect.Struct:
t := v.Type()
ts = make([]Token, 0, v.NumField()*6+3)
ts = append(ts, typeName(t.String()))
ts = append(ts, tokStructOpen)
for i := 0; i < v.NumField(); i++ {
name := t.Field(i).Name
ts = append(ts, tokStructKey)
ts = append(ts, &Lit{StructField, name})
f := v.Field(i)
if !f.CanInterface() {
f = GetPrivateField(v, i)
}
ts = append(ts, tokColon)
tz.push(name)
ts = append(ts, tz.tokenize(f)...)
tz.pop()
ts = append(ts, tokComma)
}
ts = append(ts, tokStructClose)
}
return ts
}
var tokStructKey Token = &Lit{StructKey, ""}
func tokenizeNumber(v reflect.Value) []Token {
tName := v.Type().String()
switch v.Kind() {
case reflect.Int:
if tName != "int" {
return []Token{typeName(tName), tokParenOpen, IntTok(v.Int()), tokParenClose}
}
return []Token{IntTok(v.Int())}
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return []Token{typeName(tName), tokParenOpen, IntTok(v.Int()), tokParenClose}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return []Token{typeName(tName), tokParenOpen, UintTok(v.Uint()), tokParenClose}
case reflect.Float32:
return []Token{typeName(tName), tokParenOpen, FloatTok{V: v.Float(), Bits: 32}, tokParenClose}
case reflect.Float64:
if tName != "float64" {
return []Token{typeName(tName), tokParenOpen, Float64Tok(v.Float()), tokParenClose}
}
return []Token{Float64Tok(v.Float())}
case reflect.Complex64:
return []Token{typeName(tName), tokParenOpen, ComplexTok{V: v.Complex(), Bits: 64}, tokParenClose}
default:
// reflect.Complex128 — callers dispatch only numeric kinds here.
if tName != "complex128" {
return []Token{typeName(tName), tokParenOpen, ComplexTok{V: v.Complex(), Bits: 128}, tokParenClose}
}
return []Token{ComplexTok{V: v.Complex(), Bits: 128}}
}
}
func tokenizeRuneInt32(r rune) []Token {
return []Token{
tokTNGopRune,
tokParenOpen,
IntTok(int64(r)),
tokInlineComma,
RuneTok(r),
tokParenClose,
}
}
func tokenizeByte(b byte) []Token {
return []Token{tokTNByte, tokParenOpen, ByteTok(b), tokParenClose}
}
func tokenizeTime(t time.Time) []Token {
ext := GetPrivateFieldByName(reflect.ValueOf(t), "ext").Int()
return []Token{tokFuncTime, tokParenOpen,
&Lit{String, t.Format(time.RFC3339Nano)},
tokInlineComma, IntTok(ext), tokParenClose}
}
func tokenizeDuration(d time.Duration) []Token {
return []Token{tokTNDuration, tokParenOpen,
&Lit{String, d.String()}, tokParenClose}
}
func tokenizeString(v reflect.Value) []Token {
return []Token{&Lit{String, v.String()}}
}
func tokenizeBytes(data []byte) []Token {
if utf8.Valid(data) {
return []Token{tokTNBytes, tokParenOpen,
&Lit{String, string(data)}, tokParenClose}
}
return []Token{tokFuncBase64, tokParenOpen,
&Lit{String, base64.StdEncoding.EncodeToString(data)}, tokParenClose}
}
func tokenizeMapKey(v reflect.Value) []Token {
return []Token{
tokParenOpen, typeName(v.Type().String()), tokParenClose,
tokParenOpen, tokNil, tokParenClose,
CommentPtrTok(v.Elem().Pointer()),
}
}
func (tz *tokenizer) tokenizePtr(v reflect.Value) []Token {
if v.Elem().Kind() == reflect.Invalid {
return []Token{
tokParenOpen, typeName(v.Type().String()), tokParenClose,
tokParenOpen, tokNil, tokParenClose}
}
needFn := false
switch v.Elem().Kind() {
case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array:
if _, ok := v.Elem().Interface().([]byte); ok {
needFn = true
}
default:
needFn = true
}
if needFn {
ts := []Token{tokFuncPtr, tokParenOpen}
ts = append(ts, tz.tokenize(v.Elem())...)
ts = append(ts, tokParenClose, tokDot, tokParenOpen,
typeName(v.Type().String()), tokParenClose)
return ts
}
ts := []Token{tokAnd}
ts = append(ts, tz.tokenize(v.Elem())...)
return ts
}
func (tz *tokenizer) tokenizeJSON(v reflect.Value) ([]Token, bool) {
var jv interface{}
ts := []Token{}
s := ""
if v.Kind() == reflect.String {
s = v.String()
err := json.Unmarshal([]byte(s), &jv)
if err != nil {
return nil, false
}
ts = append(ts, tokFuncJSONStr)
} else if b, ok := v.Interface().([]byte); ok {
err := json.Unmarshal(b, &jv)
if err != nil {
return nil, false
}
s = string(b)
ts = append(ts, tokFuncJSONBytes)
}
_, isObj := jv.(map[string]interface{})
_, isArr := jv.([]interface{})
if isObj || isArr {
ts = append(ts, tokParenOpen)
ts = append(ts, TokenizeWithOptions(jv, tz.Options)...)
ts = append(ts, tokInlineComma,
&Lit{String, s}, tokParenClose)
return ts, true
}
return nil, false
}
func typeName(t string) Token {
return &Lit{TypeName, t}
}
golang-github-ysmood-gop-0.4.1/utils.go 0000664 0000000 0000000 00000001754 15206753664 0020052 0 ustar 00root root 0000000 0000000 package gop
import (
"fmt"
"reflect"
"strings"
"unsafe"
)
// GetPrivateField via field index
// TODO: we can use a LRU cache for the copy of the values, but it might be trivial for just testing.
func GetPrivateField(v reflect.Value, i int) reflect.Value {
if v.Kind() != reflect.Struct {
panic("expect v to be a struct")
}
copied := reflect.New(v.Type()).Elem()
copied.Set(v)
f := copied.Field(i)
return reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem()
}
// GetPrivateFieldByName is similar with GetPrivateField
func GetPrivateFieldByName(v reflect.Value, name string) reflect.Value {
if v.Kind() != reflect.Struct {
panic("expect v to be a struct")
}
copied := reflect.New(v.Type()).Elem()
copied.Set(v)
f := copied.FieldByName(name)
return reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem()
}
// compare returns the float value of x minus y
func compare(x, y interface{}) int {
return strings.Compare(fmt.Sprintf("%#v", x), fmt.Sprintf("%#v", y))
}