bump deps
This commit is contained in:
+16
-16
@@ -12,9 +12,6 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Value type wraps interface{}
|
||||
type Value interface{}
|
||||
|
||||
// Sizer allows to perform size-based restrictions, optional.
|
||||
// If not defined both maxValueSize and maxCacheSize checks will be ignored
|
||||
type Sizer interface {
|
||||
@@ -23,14 +20,14 @@ type Sizer interface {
|
||||
|
||||
// LoadingCache defines guava-like cache with Get method returning cached value ao retrieving it if not in cache
|
||||
type LoadingCache interface {
|
||||
Get(key string, fn func() (Value, error)) (val Value, err error) // load or get from cache
|
||||
Peek(key string) (Value, bool) // get from cache by key
|
||||
Invalidate(fn func(key string) bool) // invalidate items for func(key) == true
|
||||
Delete(key string) // delete by key
|
||||
Purge() // clear cache
|
||||
Stat() CacheStat // cache stats
|
||||
Keys() []string // list of all keys
|
||||
Close() error // close open connections
|
||||
Get(key string, fn func() (interface{}, error)) (val interface{}, err error) // load or get from cache
|
||||
Peek(key string) (interface{}, bool) // get from cache by key
|
||||
Invalidate(fn func(key string) bool) // invalidate items for func(key) == true
|
||||
Delete(key string) // delete by key
|
||||
Purge() // clear cache
|
||||
Stat() CacheStat // cache stats
|
||||
Keys() []string // list of all keys
|
||||
Close() error // close open connections
|
||||
}
|
||||
|
||||
// CacheStat represent stats values
|
||||
@@ -44,8 +41,12 @@ type CacheStat struct {
|
||||
|
||||
// String formats cache stats
|
||||
func (s CacheStat) String() string {
|
||||
return fmt.Sprintf("{hits:%d, misses:%d, ratio:%.1f%%, keys:%d, size:%d, errors:%d}",
|
||||
s.Hits, s.Misses, 100*(float64(s.Hits)/float64(s.Hits+s.Misses)), s.Keys, s.Size, s.Errors)
|
||||
ratio := 0.0
|
||||
if s.Hits+s.Misses > 0 {
|
||||
ratio = float64(s.Hits) / float64(s.Hits+s.Misses)
|
||||
}
|
||||
return fmt.Sprintf("{hits:%d, misses:%d, ratio:%.2f, keys:%d, size:%d, errors:%d}",
|
||||
s.Hits, s.Misses, ratio, s.Keys, s.Size, s.Errors)
|
||||
}
|
||||
|
||||
// Nop is do-nothing implementation of LoadingCache
|
||||
@@ -57,10 +58,10 @@ func NewNopCache() *Nop {
|
||||
}
|
||||
|
||||
// Get calls fn without any caching
|
||||
func (n *Nop) Get(key string, fn func() (Value, error)) (Value, error) { return fn() }
|
||||
func (n *Nop) Get(key string, fn func() (interface{}, error)) (interface{}, error) { return fn() }
|
||||
|
||||
// Peek does nothing and always returns false
|
||||
func (n *Nop) Peek(key string) (Value, bool) { return nil, false }
|
||||
func (n *Nop) Peek(key string) (interface{}, bool) { return nil, false }
|
||||
|
||||
// Invalidate does nothing for nop cache
|
||||
func (n *Nop) Invalidate(fn func(key string) bool) {}
|
||||
@@ -83,4 +84,3 @@ func (n *Nop) Stat() CacheStat {
|
||||
func (n *Nop) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -69,7 +69,7 @@ func NewExpirableCache(opts ...Option) (*ExpirableCache, error) {
|
||||
}
|
||||
|
||||
// Get gets value by key or load with fn if not found in cache
|
||||
func (c *ExpirableCache) Get(key string, fn func() (Value, error)) (data Value, err error) {
|
||||
func (c *ExpirableCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
||||
if v, ok := c.backend.Get(key); ok {
|
||||
atomic.AddInt64(&c.Hits, 1)
|
||||
return v, nil
|
||||
@@ -104,7 +104,7 @@ func (c *ExpirableCache) Invalidate(fn func(key string) bool) {
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *ExpirableCache) Peek(key string) (Value, bool) {
|
||||
func (c *ExpirableCache) Peek(key string) (interface{}, bool) {
|
||||
return c.backend.Peek(key)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func (c *ExpirableCache) keys() int {
|
||||
return c.backend.ItemCount()
|
||||
}
|
||||
|
||||
func (c *ExpirableCache) allowed(key string, data Value) bool {
|
||||
func (c *ExpirableCache) allowed(key string, data interface{}) bool {
|
||||
if c.backend.ItemCount() >= c.maxKeys {
|
||||
return false
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
module github.com/go-pkgz/lcw
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/alicebob/miniredis/v2 v2.11.4
|
||||
github.com/go-redis/redis/v7 v7.2.0
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/go-redis/redis/v7 v7.4.0
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/stretchr/testify v1.6.1
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
||||
+9
-7
@@ -9,15 +9,15 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs=
|
||||
github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
|
||||
github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
@@ -40,9 +40,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -67,6 +68,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+3
-3
@@ -65,7 +65,7 @@ func (c *LruCache) init() error {
|
||||
}
|
||||
|
||||
// Get gets value by key or load with fn if not found in cache
|
||||
func (c *LruCache) Get(key string, fn func() (Value, error)) (data Value, err error) {
|
||||
func (c *LruCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
||||
if v, ok := c.backend.Get(key); ok {
|
||||
atomic.AddInt64(&c.Hits, 1)
|
||||
return v, nil
|
||||
@@ -97,7 +97,7 @@ func (c *LruCache) Get(key string, fn func() (Value, error)) (data Value, err er
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *LruCache) Peek(key string) (Value, bool) {
|
||||
func (c *LruCache) Peek(key string) (interface{}, bool) {
|
||||
return c.backend.Peek(key)
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ func (c *LruCache) keys() int {
|
||||
return c.backend.Len()
|
||||
}
|
||||
|
||||
func (c *LruCache) allowed(key string, data Value) bool {
|
||||
func (c *LruCache) allowed(key string, data interface{}) bool {
|
||||
if c.maxKeySize > 0 && len(key) > c.maxKeySize {
|
||||
return false
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ type options struct {
|
||||
maxKeySize int
|
||||
maxCacheSize int64
|
||||
ttl time.Duration
|
||||
onEvicted func(key string, value Value)
|
||||
onEvicted func(key string, value interface{})
|
||||
eventBus eventbus.PubSub
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func TTL(ttl time.Duration) Option {
|
||||
}
|
||||
|
||||
// OnEvicted sets callback on invalidation event
|
||||
func OnEvicted(fn func(key string, value Value)) Option {
|
||||
func OnEvicted(fn func(key string, value interface{})) Option {
|
||||
return func(o *options) error {
|
||||
o.onEvicted = fn
|
||||
return nil
|
||||
|
||||
+3
-3
@@ -41,7 +41,7 @@ func NewRedisCache(backend *redis.Client, opts ...Option) (*RedisCache, error) {
|
||||
}
|
||||
|
||||
// Get gets value by key or load with fn if not found in cache
|
||||
func (c *RedisCache) Get(key string, fn func() (Value, error)) (data Value, err error) {
|
||||
func (c *RedisCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
||||
v, getErr := c.backend.Get(key).Result()
|
||||
switch getErr {
|
||||
// RedisClient returns nil when find a key in DB
|
||||
@@ -84,7 +84,7 @@ func (c *RedisCache) Invalidate(fn func(key string) bool) {
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *RedisCache) Peek(key string) (Value, bool) {
|
||||
func (c *RedisCache) Peek(key string) (interface{}, bool) {
|
||||
ret, err := c.backend.Get(key).Result()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
@@ -132,7 +132,7 @@ func (c *RedisCache) keys() int {
|
||||
return int(c.backend.DBSize().Val())
|
||||
}
|
||||
|
||||
func (c *RedisCache) allowed(key string, data Value) bool {
|
||||
func (c *RedisCache) allowed(key string, data interface{}) bool {
|
||||
if c.maxKeys > 0 && c.backend.DBSize().Val() >= int64(c.maxKeys) {
|
||||
return false
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ func NewScache(lc LoadingCache) *Scache {
|
||||
// Get retrieves a key from underlying backend
|
||||
func (m *Scache) Get(key Key, fn func() ([]byte, error)) (data []byte, err error) {
|
||||
keyStr := key.String()
|
||||
val, err := m.lc.Get(keyStr, func() (value Value, e error) {
|
||||
val, err := m.lc.Get(keyStr, func() (value interface{}, e error) {
|
||||
return fn()
|
||||
})
|
||||
return val.([]byte), err
|
||||
|
||||
+18
-8
@@ -23,11 +23,12 @@ linters-settings:
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- megacheck
|
||||
- golint
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
@@ -42,19 +43,28 @@ linters:
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- varcheck
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- scopelint
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
fast: false
|
||||
|
||||
disable-all: true
|
||||
|
||||
run:
|
||||
# modules-download-mode: vendor
|
||||
output:
|
||||
format: tab
|
||||
skip-dirs:
|
||||
- vendor
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- text: "weak cryptographic primitive"
|
||||
- text: "should have a package comment, unless it's in another file for this package"
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.16.x
|
||||
- golint
|
||||
- text: "at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- stylecheck
|
||||
exclude-use-default: false
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Umputun
|
||||
Copyright (c) 2020 Umputun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+38
-9
@@ -1,4 +1,5 @@
|
||||
# lgr - simple logger with some extras [](https://github.com/go-pkgz/lgr/actions) [](https://coveralls.io/github/go-pkgz/lgr?branch=master) [](https://godoc.org/github.com/go-pkgz/lgr)
|
||||
# lgr - simple logger with some extras
|
||||
[](https://github.com/go-pkgz/lgr/actions) [](https://coveralls.io/github/go-pkgz/lgr?branch=master) [](https://godoc.org/github.com/go-pkgz/lgr)
|
||||
|
||||
## install
|
||||
|
||||
@@ -24,7 +25,7 @@ _Without `lgr.Caller*` it will drop `{caller}` part_
|
||||
|
||||
### interfaces and default loggers
|
||||
|
||||
- `lgr` package provides a single interface `lgr.L` with a single method `Logf(format string, args ...interface{})`. Function wrapper `lgr.Func` allows to make `lgr.L` from a function directly.
|
||||
- `lgr` package provides a single interface `lgr.L` with a single method `Logf(format string, args ...interface{})`. Function wrapper `lgr.Func` allows making `lgr.L` from a function directly.
|
||||
- Default logger functionality can be used without `lgr.New` (see "global logger")
|
||||
- Two predefined loggers available: `lgr.NoOp` (do-nothing logger) and `lgr.Std` (passing directly to stdlib log)
|
||||
|
||||
@@ -41,8 +42,10 @@ _Without `lgr.Caller*` it will drop `{caller}` part_
|
||||
- `lgr.CallerPkg` - adds the caller package
|
||||
- `lgr.LevelBraces` - wraps levels with "[" and "]"
|
||||
- `lgr.Msec` - adds milliseconds to timestamp
|
||||
- `lgr.Format` - sets custom template, overwrite all other formatting modifiers.
|
||||
- `lgr.Format` - sets a custom template, overwrite all other formatting modifiers.
|
||||
- `lgr.Secret(secret ...)` - sets list of the secrets to hide from the logging outputs.
|
||||
- `lgr.Map(mapper)` - sets mapper functions to change elements of the logging output based on levels.
|
||||
- `lgr.StackTraceOnError` - turns on stack trace for ERROR level.
|
||||
|
||||
example: `l := lgr.New(lgr.Debug, lgr.Msec)`
|
||||
|
||||
@@ -61,16 +64,16 @@ Several predefined templates provided and can be passed directly to `lgr.Format`
|
||||
|
||||
User can make a custom template and pass it directly to `lgr.Format`. For example:
|
||||
|
||||
```go
|
||||
```go
|
||||
lgr.Format(`{{.Level}} - {{.DT.Format "2006-01-02T15:04:05Z07:00"}} - {{.CallerPkg}} - {{.Message}}`)
|
||||
```
|
||||
|
||||
_Note: formatter (predefined or custom) adds measurable overhead - the cost will depend on the version of Go, but is between 30
|
||||
and 50% in recent tests with 1.12. You can validate this in your environment via benchmarks: `go test -bench=. -run=Bench`_
|
||||
|
||||
|
||||
### levels
|
||||
|
||||
`lgr.Logf` recognizes prefixes like "INFO" or "[INFO]" as levels. The full list of supported levels - "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "PANIC" and "FATAL"
|
||||
`lgr.Logf` recognize prefixes like `INFO` or `[INFO]` as levels. The full list of supported levels - `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `PANIC` and `FATAL`.
|
||||
|
||||
- `TRACE` will be filtered unless `lgr.Trace` option defined
|
||||
- `DEBUG` will be filtered unless `lgr.Debug` or `lgr.Trace` options defined
|
||||
@@ -78,7 +81,29 @@ _Note: formatter (predefined or custom) adds measurable overhead - the cost will
|
||||
- `ERROR` sends messages to both out and err writers
|
||||
- `FATAL` and send messages to both out and err writers and exit(1)
|
||||
- `PANIC` does the same as `FATAL` but in addition sends dump of callers and runtime info to err.
|
||||
|
||||
|
||||
### mapper
|
||||
|
||||
Elements of the output can be altered with a set of user defined function passed as `lgr.Map` options. Such a mapper changes
|
||||
the value of an element (i.e. timestamp, level, message, caller) and has separate functions for each level. Note: both level
|
||||
and messages elements handled by the same function for a given level.
|
||||
|
||||
_A typical use-case is to produce colorful output with a user-define colorization library._
|
||||
|
||||
example with [fatih/color](https://github.com/fatih/color):
|
||||
|
||||
```go
|
||||
colorizer := lgr.Mapper{
|
||||
ErrorFunc: func(s string) string { return color.New(color.FgHiRed).Sprint(s) },
|
||||
WarnFunc: func(s string) string { return color.New(color.FgHiYellow).Sprint(s) },
|
||||
InfoFunc: func(s string) string { return color.New(color.FgHiWhite).Sprint(s) },
|
||||
DebugFunc: func(s string) string { return color.New(color.FgWhite).Sprint(s) },
|
||||
CallerFunc: func(s string) string { return color.New(color.FgBlue).Sprint(s) },
|
||||
TimeFunc: func(s string) string { return color.New(color.FgCyan).Sprint(s) },
|
||||
}
|
||||
|
||||
logOpts := []lgr.Option{lgr.Msec, lgr.LevelBraces, lgr.Map(colorizer)}
|
||||
```
|
||||
### adaptors
|
||||
|
||||
`lgr` logger can be converted to `io.Writer` or `*log.Logger`
|
||||
@@ -86,10 +111,14 @@ _Note: formatter (predefined or custom) adds measurable overhead - the cost will
|
||||
- `lgr.ToWriter(l lgr.L, level string) io.Writer` - makes io.Writer forwarding write ops to underlying `lgr.L`
|
||||
- `lgr.ToStdLogger(l lgr.L, level string) *log.Logger` - makes standard logger on top of `lgr.L`
|
||||
|
||||
_`level` parameter is optional, if defined (non-empty) will enforce the level._
|
||||
|
||||
_`level` parameter is optional, if defined (non-empty) will enforce the level._
|
||||
|
||||
- `lgr.SetupStdLogger(opts ...Option)` initializes std global logger (`log.std`) with lgr logger and given options.
|
||||
All standard methods like `log.Print`, `log.Println`, `log.Fatal` and so on will be forwarder to lgr.
|
||||
|
||||
### global logger
|
||||
|
||||
Users **should avoid** global logger and pass the concrete logger as a dependency. However, in some cases a global logger may be needed, for example migration from stdlib `log` to `lgr`. For such cases `log "github.com/go-pkgz/lgr"` can be imported instead of `log` package.
|
||||
|
||||
Global logger provides `lgr.Printf`, `lgr.Print` and `lgr.Fatalf` functions. User can customize the logger by calling `lgr.Setup(options ...)`. The instance of this logger can be retrieved with `lgr.Default()`
|
||||
|
||||
|
||||
+11
@@ -25,6 +25,17 @@ func ToWriter(l L, level string) *Writer {
|
||||
return &Writer{l, level}
|
||||
}
|
||||
|
||||
// ToStdLogger makes standard logger
|
||||
func ToStdLogger(l L, level string) *log.Logger {
|
||||
return log.New(ToWriter(l, level), "", 0)
|
||||
}
|
||||
|
||||
// SetupStdLogger makes the default std logger with lgr.L
|
||||
func SetupStdLogger(opts ...Option) {
|
||||
logOpts := append([]Option{CallerDepth(3)}, opts...) // skip 3 more frames to compensate stdlog calls
|
||||
l := New(logOpts...)
|
||||
l.reTrace = reTraceStd // std logger split on log/ path
|
||||
log.SetOutput(ToWriter(l, ""))
|
||||
log.SetPrefix("")
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
module github.com/go-pkgz/lgr
|
||||
|
||||
require github.com/stretchr/testify v1.3.0
|
||||
require github.com/stretchr/testify v1.6.1
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
+7
-2
@@ -2,6 +2,11 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+95
-20
@@ -6,7 +6,6 @@
|
||||
// Debug and trace levels can be filtered based on lgr.Trace and lgr.Debug options.
|
||||
// ERROR, FATAL and PANIC levels send to err as well. FATAL terminate caller application with os.Exit(1)
|
||||
// and PANIC also prints stack trace.
|
||||
|
||||
package lgr
|
||||
|
||||
import (
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,16 +26,27 @@ import (
|
||||
var levels = []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "PANIC", "FATAL"}
|
||||
|
||||
const (
|
||||
Short = `{{.DT.Format "2006/01/02 15:04:05"}} {{.Level}} {{.Message}}`
|
||||
WithMsec = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} {{.Message}}`
|
||||
WithPkg = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerPkg}}) {{.Message}}`
|
||||
// Short logging format
|
||||
Short = `{{.DT.Format "2006/01/02 15:04:05"}} {{.Level}} {{.Message}}`
|
||||
// WithMsec is a logging format with milliseconds
|
||||
WithMsec = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} {{.Message}}`
|
||||
// WithPkg is WithMsec logging format with caller package
|
||||
WithPkg = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerPkg}}) {{.Message}}`
|
||||
// ShortDebug is WithMsec logging format with caller file and line
|
||||
ShortDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}}) {{.Message}}`
|
||||
FuncDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFunc}}) {{.Message}}`
|
||||
FullDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}} {{.CallerFunc}}) {{.Message}}`
|
||||
// FuncDebug is WithMsec logging format with caller function
|
||||
FuncDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFunc}}) {{.Message}}`
|
||||
// FullDebug is WithMsec logging format with caller file, line and function
|
||||
FullDebug = `{{.DT.Format "2006/01/02 15:04:05.000"}} {{.Level}} ({{.CallerFile}}:{{.CallerLine}} {{.CallerFunc}}) {{.Message}}`
|
||||
)
|
||||
|
||||
var secretReplacement = []byte("******")
|
||||
|
||||
var (
|
||||
reTraceDefault = regexp.MustCompile(`.*/lgr/logger\.go.*\n`)
|
||||
reTraceStd = regexp.MustCompile(`.*/log/log\.go.*\n`)
|
||||
)
|
||||
|
||||
// Logger provided simple logger with basic support of levels. Thread safe
|
||||
type Logger struct {
|
||||
// set with Option calls
|
||||
@@ -48,7 +59,8 @@ type Logger struct {
|
||||
levelBraces bool // encloses level with [], i.e. [INFO]
|
||||
callerDepth int // how many stack frames to skip, relative to the real (reported) frame
|
||||
format string // layout template
|
||||
secrets []string // sub-strings to secrets by matching
|
||||
secrets [][]byte // sub-strings to secrets by matching
|
||||
mapper Mapper // map (alter) output based on levels
|
||||
|
||||
// internal use
|
||||
now nowFn
|
||||
@@ -57,7 +69,9 @@ type Logger struct {
|
||||
lock sync.Mutex
|
||||
callerOn bool
|
||||
levelBracesOn bool
|
||||
errorDump bool
|
||||
templ *template.Template
|
||||
reTrace *regexp.Regexp
|
||||
}
|
||||
|
||||
// can be redefined internally for testing
|
||||
@@ -85,6 +99,8 @@ func New(options ...Option) *Logger {
|
||||
stdout: os.Stdout,
|
||||
stderr: os.Stderr,
|
||||
callerDepth: 0,
|
||||
mapper: nopMapper,
|
||||
reTrace: reTraceDefault,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(&res)
|
||||
@@ -124,9 +140,16 @@ func (l *Logger) Logf(format string, args ...interface{}) {
|
||||
l.logf(format, args...)
|
||||
}
|
||||
|
||||
//nolint gocyclo
|
||||
func (l *Logger) logf(format string, args ...interface{}) {
|
||||
|
||||
lv, msg := l.extractLevel(fmt.Sprintf(format, args...))
|
||||
var lv, msg string
|
||||
if len(args) == 0 {
|
||||
lv, msg = l.extractLevel(format)
|
||||
} else {
|
||||
lv, msg = l.extractLevel(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
if lv == "DEBUG" && !l.dbg {
|
||||
return
|
||||
}
|
||||
@@ -177,6 +200,15 @@ func (l *Logger) logf(format string, args ...interface{}) {
|
||||
if l.stderr != l.stdout {
|
||||
_, _ = l.stderr.Write(data)
|
||||
}
|
||||
if l.errorDump {
|
||||
stackInfo := make([]byte, 1024*1024)
|
||||
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
|
||||
traceLines := l.reTrace.Split(string(stackInfo[:stackSize]), -1)
|
||||
if len(traceLines) > 0 {
|
||||
_, _ = l.stdout.Write([]byte(">>> stack trace:\n" + traceLines[len(traceLines)-1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "FATAL":
|
||||
if l.stderr != l.stdout {
|
||||
_, _ = l.stderr.Write(data)
|
||||
@@ -195,7 +227,7 @@ func (l *Logger) logf(format string, args ...interface{}) {
|
||||
|
||||
func (l *Logger) hideSecrets(data []byte) []byte {
|
||||
for _, h := range l.secrets {
|
||||
data = bytes.Replace(data, []byte(h), secretReplacement, -1)
|
||||
data = bytes.Replace(data, h, secretReplacement, -1)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -268,15 +300,17 @@ func (l *Logger) formatWithOptions(elems layout) (res string) {
|
||||
|
||||
parts := make([]string, 0, 4)
|
||||
|
||||
parts = append(parts, orElse(l.msec,
|
||||
func() string { return elems.DT.Format("2006/01/02 15:04:05.000") },
|
||||
func() string { return elems.DT.Format("2006/01/02 15:04:05") },
|
||||
))
|
||||
|
||||
parts = append(parts, orElse(l.levelBraces,
|
||||
func() string { return `[` + elems.Level + `]` },
|
||||
func() string { return elems.Level },
|
||||
))
|
||||
parts = append(
|
||||
parts,
|
||||
l.mapper.TimeFunc(orElse(l.msec,
|
||||
func() string { return elems.DT.Format("2006/01/02 15:04:05.000") },
|
||||
func() string { return elems.DT.Format("2006/01/02 15:04:05") },
|
||||
)),
|
||||
l.levelMapper(elems.Level)(orElse(l.levelBraces,
|
||||
func() string { return `[` + elems.Level + `]` },
|
||||
func() string { return elems.Level },
|
||||
)),
|
||||
)
|
||||
|
||||
if l.callerFile || l.callerFunc || l.callerPkg {
|
||||
var callerParts []string
|
||||
@@ -290,10 +324,20 @@ func (l *Logger) formatWithOptions(elems layout) (res string) {
|
||||
if v := orElse(l.callerPkg, func() string { return elems.CallerPkg }, nothing); v != "" {
|
||||
callerParts = append(callerParts, v)
|
||||
}
|
||||
parts = append(parts, "{"+strings.Join(callerParts, " ")+"}")
|
||||
|
||||
caller := "{" + strings.Join(callerParts, " ") + "}"
|
||||
if l.mapper.CallerFunc != nil {
|
||||
caller = l.mapper.CallerFunc(caller)
|
||||
}
|
||||
parts = append(parts, caller)
|
||||
}
|
||||
|
||||
parts = append(parts, elems.Message)
|
||||
msg := elems.Message
|
||||
if l.mapper.MessageFunc != nil {
|
||||
msg = l.mapper.MessageFunc(elems.Message)
|
||||
}
|
||||
|
||||
parts = append(parts, l.levelMapper(elems.Level)(msg))
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
@@ -320,6 +364,37 @@ func (l *Logger) extractLevel(line string) (level, msg string) {
|
||||
return "INFO", line
|
||||
}
|
||||
|
||||
func (l *Logger) levelMapper(level string) mapFunc {
|
||||
|
||||
nop := func(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
switch level {
|
||||
case "TRACE", "DEBUG":
|
||||
if l.mapper.DebugFunc == nil {
|
||||
return nop
|
||||
}
|
||||
return l.mapper.DebugFunc
|
||||
case "INFO ":
|
||||
if l.mapper.InfoFunc == nil {
|
||||
return nop
|
||||
}
|
||||
return l.mapper.InfoFunc
|
||||
case "WARN ":
|
||||
if l.mapper.WarnFunc == nil {
|
||||
return nop
|
||||
}
|
||||
return l.mapper.WarnFunc
|
||||
case "ERROR", "PANIC", "FATAL":
|
||||
if l.mapper.ErrorFunc == nil {
|
||||
return nop
|
||||
}
|
||||
return l.mapper.ErrorFunc
|
||||
}
|
||||
return func(s string) string { return s }
|
||||
}
|
||||
|
||||
// getDump reads runtime stack and returns as a string
|
||||
func getDump() []byte {
|
||||
maxSize := 5 * 1024 * 1024
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package lgr
|
||||
|
||||
// Mapper defines optional functions to change elements of the logged message for each part, based on levels.
|
||||
// Only some mapFunc can be defined, by default does nothing. Can be used to alter the output, for example making some
|
||||
// part of the output colorful.
|
||||
type Mapper struct {
|
||||
MessageFunc mapFunc // message mapper on all levels
|
||||
ErrorFunc mapFunc // message mapper on ERROR level
|
||||
WarnFunc mapFunc // message mapper on WARN level
|
||||
InfoFunc mapFunc // message mapper on INFO level
|
||||
DebugFunc mapFunc // message mapper on DEBUG level
|
||||
|
||||
CallerFunc mapFunc // caller mapper, all levels
|
||||
TimeFunc mapFunc // time mapper, all levels
|
||||
}
|
||||
|
||||
type mapFunc func(string) string
|
||||
|
||||
// nopMapper is a default, doing nothing
|
||||
var nopMapper = Mapper{
|
||||
MessageFunc: func(s string) string { return s },
|
||||
ErrorFunc: func(s string) string { return s },
|
||||
WarnFunc: func(s string) string { return s },
|
||||
InfoFunc: func(s string) string { return s },
|
||||
DebugFunc: func(s string) string { return s },
|
||||
CallerFunc: func(s string) string { return s },
|
||||
TimeFunc: func(s string) string { return s },
|
||||
}
|
||||
+15
-1
@@ -73,6 +73,20 @@ func Msec(l *Logger) {
|
||||
// Useful to prevent passwords or other sensitive tokens to be logged.
|
||||
func Secret(vals ...string) Option {
|
||||
return func(l *Logger) {
|
||||
l.secrets = vals
|
||||
for _, v := range vals {
|
||||
l.secrets = append(l.secrets, []byte(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map sets mapper functions to change elements of the logged message based on levels.
|
||||
func Map(m Mapper) Option {
|
||||
return func(l *Logger) {
|
||||
l.mapper = m
|
||||
}
|
||||
}
|
||||
|
||||
// StackTraceOnError turns on stack trace for ERROR level.
|
||||
func StackTraceOnError(l *Logger) {
|
||||
l.errorDump = true
|
||||
}
|
||||
|
||||
-2
@@ -1,12 +1,10 @@
|
||||
dist: xenial
|
||||
sudo: false
|
||||
language: go
|
||||
|
||||
services:
|
||||
- redis-server
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
- tip
|
||||
|
||||
+4
@@ -4,6 +4,10 @@
|
||||
|
||||
- Existing `HMSet` is renamed to `HSet` and old deprecated `HMSet` is restored for Redis 3 users.
|
||||
|
||||
## v7.1
|
||||
|
||||
- Existing `Cmd.String` is renamed to `Cmd.Text`. New `Cmd.String` implements `fmt.Stringer` interface.
|
||||
|
||||
## v7
|
||||
|
||||
- *Important*. Tx.Pipeline now returns a non-transactional pipeline. Use Tx.TxPipeline for a transactional pipeline.
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ bench: testdeps
|
||||
|
||||
testdata/redis:
|
||||
mkdir -p $@
|
||||
wget -qO- http://download.redis.io/releases/redis-5.0.7.tar.gz | tar xvz --strip-components=1 -C $@
|
||||
wget -qO- http://download.redis.io/redis-stable.tar.gz | tar xvz --strip-components=1 -C $@
|
||||
|
||||
testdata/redis/src/redis-server: testdata/redis
|
||||
cd $< && make all
|
||||
|
||||
+5
-2
@@ -101,7 +101,7 @@ set, err := client.SetNX("key", "value", 10*time.Second).Result()
|
||||
vals, err := client.Sort("list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result()
|
||||
|
||||
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2
|
||||
vals, err := client.ZRangeByScoreWithScores("zset", redis.ZRangeBy{
|
||||
vals, err := client.ZRangeByScoreWithScores("zset", &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: "+inf",
|
||||
Offset: 0,
|
||||
@@ -109,7 +109,10 @@ vals, err := client.ZRangeByScoreWithScores("zset", redis.ZRangeBy{
|
||||
}).Result()
|
||||
|
||||
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM
|
||||
vals, err := client.ZInterStore("out", redis.ZStore{Weights: []int64{2, 3}}, "zset1", "zset2").Result()
|
||||
vals, err := client.ZInterStore("out", &redis.ZStore{
|
||||
Keys: []string{"zset1", "zset2"},
|
||||
Weights: []int64{2, 3}
|
||||
}).Result()
|
||||
|
||||
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello"
|
||||
vals, err := client.Eval("return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result()
|
||||
|
||||
+10
-1
@@ -57,6 +57,7 @@ type ClusterOptions struct {
|
||||
|
||||
OnConnect func(*Conn) error
|
||||
|
||||
Username string
|
||||
Password string
|
||||
|
||||
MaxRetries int
|
||||
@@ -67,6 +68,9 @@ type ClusterOptions struct {
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// NewClient creates a cluster node client with provided name and options.
|
||||
NewClient func(opt *Options) *Client
|
||||
|
||||
// PoolSize applies per cluster node and not for the whole cluster.
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
@@ -118,6 +122,10 @@ func (opt *ClusterOptions) init() {
|
||||
case 0:
|
||||
opt.MaxRetryBackoff = 512 * time.Millisecond
|
||||
}
|
||||
|
||||
if opt.NewClient == nil {
|
||||
opt.NewClient = NewClient
|
||||
}
|
||||
}
|
||||
|
||||
func (opt *ClusterOptions) clientOptions() *Options {
|
||||
@@ -130,6 +138,7 @@ func (opt *ClusterOptions) clientOptions() *Options {
|
||||
MaxRetries: opt.MaxRetries,
|
||||
MinRetryBackoff: opt.MinRetryBackoff,
|
||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
readOnly: opt.ReadOnly,
|
||||
|
||||
@@ -162,7 +171,7 @@ func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode {
|
||||
opt := clOpt.clientOptions()
|
||||
opt.Addr = addr
|
||||
node := clusterNode{
|
||||
Client: NewClient(opt),
|
||||
Client: clOpt.NewClient(opt),
|
||||
}
|
||||
|
||||
node.latency = math.MaxUint32
|
||||
|
||||
+32
-3
@@ -671,7 +671,7 @@ func (cmd *StringCmd) Time() (time.Time, error) {
|
||||
if cmd.err != nil {
|
||||
return time.Time{}, cmd.err
|
||||
}
|
||||
return time.Parse(time.RFC3339, cmd.Val())
|
||||
return time.Parse(time.RFC3339Nano, cmd.Val())
|
||||
}
|
||||
|
||||
func (cmd *StringCmd) Scan(val interface{}) error {
|
||||
@@ -1885,6 +1885,7 @@ type CommandInfo struct {
|
||||
Name string
|
||||
Arity int8
|
||||
Flags []string
|
||||
ACLFlags []string
|
||||
FirstKeyPos int8
|
||||
LastKeyPos int8
|
||||
StepCount int8
|
||||
@@ -1934,8 +1935,14 @@ func (cmd *CommandsInfoCmd) readReply(rd *proto.Reader) error {
|
||||
}
|
||||
|
||||
func commandInfoParser(rd *proto.Reader, n int64) (interface{}, error) {
|
||||
if n != 6 {
|
||||
return nil, fmt.Errorf("redis: got %d elements in COMMAND reply, wanted 6", n)
|
||||
const numArgRedis5 = 6
|
||||
const numArgRedis6 = 7
|
||||
|
||||
switch n {
|
||||
case numArgRedis5, numArgRedis6:
|
||||
// continue
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: got %d elements in COMMAND reply, wanted 7", n)
|
||||
}
|
||||
|
||||
var cmd CommandInfo
|
||||
@@ -1995,6 +2002,28 @@ func commandInfoParser(rd *proto.Reader, n int64) (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if n == numArgRedis5 {
|
||||
return &cmd, nil
|
||||
}
|
||||
|
||||
_, err = rd.ReadReply(func(rd *proto.Reader, n int64) (interface{}, error) {
|
||||
cmd.ACLFlags = make([]string, n)
|
||||
for i := 0; i < len(cmd.ACLFlags); i++ {
|
||||
switch s, err := rd.ReadString(); {
|
||||
case err == Nil:
|
||||
cmd.ACLFlags[i] = ""
|
||||
case err != nil:
|
||||
return nil, err
|
||||
default:
|
||||
cmd.ACLFlags[i] = s
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmd, nil
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -302,6 +302,7 @@ type Cmdable interface {
|
||||
type StatefulCmdable interface {
|
||||
Cmdable
|
||||
Auth(password string) *StatusCmd
|
||||
AuthACL(username, password string) *StatusCmd
|
||||
Select(index int) *StatusCmd
|
||||
SwapDB(index1, index2 int) *StatusCmd
|
||||
ClientSetName(name string) *BoolCmd
|
||||
@@ -324,6 +325,15 @@ func (c statefulCmdable) Auth(password string) *StatusCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Perform an AUTH command, using the given user and pass.
|
||||
// Should be used to authenticate the current connection with one of the connections defined in the ACL list
|
||||
// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
||||
func (c statefulCmdable) AuthACL(username, password string) *StatusCmd {
|
||||
cmd := NewStatusCmd("auth", username, password)
|
||||
_ = c(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Echo(message interface{}) *StringCmd {
|
||||
cmd := NewStringCmd("echo", message)
|
||||
_ = c(cmd)
|
||||
|
||||
+15
@@ -6,9 +6,24 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v7/internal/pool"
|
||||
"github.com/go-redis/redis/v7/internal/proto"
|
||||
)
|
||||
|
||||
var ErrClosed = pool.ErrClosed
|
||||
|
||||
type Error interface {
|
||||
error
|
||||
|
||||
// RedisError is a no-op function but
|
||||
// serves to distinguish types that are Redis
|
||||
// errors from ordinary errors: a type is a
|
||||
// Redis error if it has a RedisError method.
|
||||
RedisError()
|
||||
}
|
||||
|
||||
var _ Error = proto.RedisError("")
|
||||
|
||||
func isRetryableError(err error, retryTimeout bool) bool {
|
||||
switch err {
|
||||
case nil, context.Canceled, context.DeadlineExceeded:
|
||||
|
||||
+2
@@ -94,7 +94,9 @@ func NewConnPool(opt *Options) *ConnPool {
|
||||
closedCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
p.connsMu.Lock()
|
||||
p.checkMinIdleConns()
|
||||
p.connsMu.Unlock()
|
||||
|
||||
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
|
||||
go p.reaper(opt.IdleCheckFrequency)
|
||||
|
||||
+2
@@ -24,6 +24,8 @@ type RedisError string
|
||||
|
||||
func (e RedisError) Error() string { return string(e) }
|
||||
|
||||
func (RedisError) RedisError() {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type MultiBulkParse func(*Reader, int64) (interface{}, error)
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ func (w *Writer) writeArg(v interface{}) error {
|
||||
}
|
||||
return w.int(0)
|
||||
case time.Time:
|
||||
return w.string(v.Format(time.RFC3339))
|
||||
return w.string(v.Format(time.RFC3339Nano))
|
||||
case encoding.BinaryMarshaler:
|
||||
b, err := v.MarshalBinary()
|
||||
if err != nil {
|
||||
|
||||
+7
-1
@@ -40,8 +40,13 @@ type Options struct {
|
||||
// Hook that is called when new connection is established.
|
||||
OnConnect func(*Conn) error
|
||||
|
||||
// Use the specified Username to authenticate the current connection with one of the connections defined in the ACL
|
||||
// list when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
||||
Username string
|
||||
|
||||
// Optional password. Must match the password specified in the
|
||||
// requirepass server configuration option.
|
||||
// requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower),
|
||||
// or the User Password when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
||||
Password string
|
||||
// Database to be selected after connecting to the server.
|
||||
DB int
|
||||
@@ -187,6 +192,7 @@ func ParseURL(redisURL string) (*Options, error) {
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
o.Username = u.User.Username()
|
||||
if p, ok := u.User.Password(); ok {
|
||||
o.Password = p
|
||||
}
|
||||
|
||||
+3
-1
@@ -309,9 +309,11 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
|
||||
case []interface{}:
|
||||
switch kind := reply[0].(string); kind {
|
||||
case "subscribe", "unsubscribe", "psubscribe", "punsubscribe":
|
||||
// Can be nil in case of "unsubscribe".
|
||||
channel, _ := reply[1].(string)
|
||||
return &Subscription{
|
||||
Kind: kind,
|
||||
Channel: reply[1].(string),
|
||||
Channel: channel,
|
||||
Count: int(reply[2].(int64)),
|
||||
}, nil
|
||||
case "message":
|
||||
|
||||
+5
-1
@@ -241,7 +241,11 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
|
||||
|
||||
_, err := conn.Pipelined(func(pipe Pipeliner) error {
|
||||
if c.opt.Password != "" {
|
||||
pipe.Auth(c.opt.Password)
|
||||
if c.opt.Username != "" {
|
||||
pipe.AuthACL(c.opt.Username, c.opt.Password)
|
||||
} else {
|
||||
pipe.Auth(c.opt.Password)
|
||||
}
|
||||
}
|
||||
|
||||
if c.opt.DB > 0 {
|
||||
|
||||
+8
@@ -98,6 +98,14 @@ func NewStringIntMapCmdResult(val map[string]int64, err error) *StringIntMapCmd
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// NewTimeCmdResult returns a TimeCmd initialised with val and err for testing
|
||||
func NewTimeCmdResult(val time.Time, err error) *TimeCmd {
|
||||
var cmd TimeCmd
|
||||
cmd.val = val
|
||||
cmd.SetErr(err)
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// NewZSliceCmdResult returns a ZSliceCmd initialised with val and err for testing
|
||||
func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd {
|
||||
var cmd ZSliceCmd
|
||||
|
||||
+9
-1
@@ -56,6 +56,9 @@ type RingOptions struct {
|
||||
// See https://arxiv.org/abs/1406.2294 for reference
|
||||
HashReplicas int
|
||||
|
||||
// NewClient creates a shard client with provided name and options.
|
||||
NewClient func(name string, opt *Options) *Client
|
||||
|
||||
// Optional hook that is called when a new shard is created.
|
||||
OnNewShard func(*Client)
|
||||
|
||||
@@ -390,7 +393,12 @@ func NewRing(opt *RingOptions) *Ring {
|
||||
func newRingShard(opt *RingOptions, name, addr string) *Client {
|
||||
clopt := opt.clientOptions(name)
|
||||
clopt.Addr = addr
|
||||
shard := NewClient(clopt)
|
||||
var shard *Client
|
||||
if opt.NewClient != nil {
|
||||
shard = opt.NewClient(name, clopt)
|
||||
} else {
|
||||
shard = NewClient(clopt)
|
||||
}
|
||||
if opt.OnNewShard != nil {
|
||||
opt.OnNewShard(shard)
|
||||
}
|
||||
|
||||
+6
@@ -22,6 +22,7 @@ type FailoverOptions struct {
|
||||
MasterName string
|
||||
// A seed list of host:port addresses of sentinel nodes.
|
||||
SentinelAddrs []string
|
||||
SentinelUsername string
|
||||
SentinelPassword string
|
||||
|
||||
// Following options are copied from Options struct.
|
||||
@@ -29,6 +30,7 @@ type FailoverOptions struct {
|
||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
OnConnect func(*Conn) error
|
||||
|
||||
Username string
|
||||
Password string
|
||||
DB int
|
||||
|
||||
@@ -57,6 +59,7 @@ func (opt *FailoverOptions) options() *Options {
|
||||
OnConnect: opt.OnConnect,
|
||||
|
||||
DB: opt.DB,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
|
||||
MaxRetries: opt.MaxRetries,
|
||||
@@ -88,6 +91,7 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
|
||||
failover := &sentinelFailover{
|
||||
masterName: failoverOpt.MasterName,
|
||||
sentinelAddrs: failoverOpt.SentinelAddrs,
|
||||
username: failoverOpt.SentinelUsername,
|
||||
password: failoverOpt.SentinelPassword,
|
||||
|
||||
opt: opt,
|
||||
@@ -281,6 +285,7 @@ type sentinelFailover struct {
|
||||
sentinelAddrs []string
|
||||
|
||||
opt *Options
|
||||
username string
|
||||
password string
|
||||
|
||||
pool *pool.ConnPool
|
||||
@@ -372,6 +377,7 @@ func (c *sentinelFailover) masterAddr() (string, error) {
|
||||
Addr: sentinelAddr,
|
||||
Dialer: c.opt.Dialer,
|
||||
|
||||
Username: c.username,
|
||||
Password: c.password,
|
||||
|
||||
MaxRetries: c.opt.MaxRetries,
|
||||
|
||||
+4
@@ -22,6 +22,7 @@ type UniversalOptions struct {
|
||||
|
||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
OnConnect func(*Conn) error
|
||||
Username string
|
||||
Password string
|
||||
MaxRetries int
|
||||
MinRetryBackoff time.Duration
|
||||
@@ -60,6 +61,7 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
|
||||
Dialer: o.Dialer,
|
||||
OnConnect: o.OnConnect,
|
||||
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
|
||||
MaxRedirects: o.MaxRedirects,
|
||||
@@ -99,6 +101,7 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
|
||||
OnConnect: o.OnConnect,
|
||||
|
||||
DB: o.DB,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
|
||||
MaxRetries: o.MaxRetries,
|
||||
@@ -133,6 +136,7 @@ func (o *UniversalOptions) Simple() *Options {
|
||||
OnConnect: o.OnConnect,
|
||||
|
||||
DB: o.DB,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
|
||||
MaxRetries: o.MaxRetries,
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ change is the ability to represent an invalid UUID (vs a NIL UUID).
|
||||
|
||||
Full `go doc` style documentation for the package can be viewed online without
|
||||
installing this package by using the GoDoc site here:
|
||||
http://godoc.org/github.com/google/uuid
|
||||
http://pkg.go.dev/github.com/google/uuid
|
||||
|
||||
+4
-3
@@ -16,10 +16,11 @@ func (uuid UUID) MarshalText() ([]byte, error) {
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (uuid *UUID) UnmarshalText(data []byte) error {
|
||||
id, err := ParseBytes(data)
|
||||
if err == nil {
|
||||
*uuid = id
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
*uuid = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
|
||||
+6
-6
@@ -17,12 +17,6 @@ import (
|
||||
//
|
||||
// In most cases, New should be used.
|
||||
func NewUUID() (UUID, error) {
|
||||
nodeMu.Lock()
|
||||
if nodeID == zeroID {
|
||||
setNodeInterface("")
|
||||
}
|
||||
nodeMu.Unlock()
|
||||
|
||||
var uuid UUID
|
||||
now, seq, err := GetTime()
|
||||
if err != nil {
|
||||
@@ -38,7 +32,13 @@ func NewUUID() (UUID, error) {
|
||||
binary.BigEndian.PutUint16(uuid[4:], timeMid)
|
||||
binary.BigEndian.PutUint16(uuid[6:], timeHi)
|
||||
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||
|
||||
nodeMu.Lock()
|
||||
if nodeID == zeroID {
|
||||
setNodeInterface("")
|
||||
}
|
||||
copy(uuid[10:], nodeID[:])
|
||||
nodeMu.Unlock()
|
||||
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
+6
-1
@@ -27,8 +27,13 @@ func New() UUID {
|
||||
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||
// year and having one duplicate.
|
||||
func NewRandom() (UUID, error) {
|
||||
return NewRandomFromReader(rander)
|
||||
}
|
||||
|
||||
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
|
||||
func NewRandomFromReader(r io.Reader) (UUID, error) {
|
||||
var uuid UUID
|
||||
_, err := io.ReadFull(rander, uuid[:])
|
||||
_, err := io.ReadFull(r, uuid[:])
|
||||
if err != nil {
|
||||
return Nil, err
|
||||
}
|
||||
|
||||
+67
-23
@@ -1,4 +1,6 @@
|
||||
Blackfriday [](https://travis-ci.org/russross/blackfriday)
|
||||
Blackfriday
|
||||
[![Build Status][BuildV2SVG]][BuildV2URL]
|
||||
[![PkgGoDev][PkgGoDevV2SVG]][PkgGoDevV2URL]
|
||||
===========
|
||||
|
||||
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
|
||||
@@ -16,19 +18,21 @@ It started as a translation from C of [Sundown][3].
|
||||
Installation
|
||||
------------
|
||||
|
||||
Blackfriday is compatible with any modern Go release. With Go 1.7 and git
|
||||
installed:
|
||||
Blackfriday is compatible with modern Go releases in module mode.
|
||||
With Go installed:
|
||||
|
||||
go get gopkg.in/russross/blackfriday.v2
|
||||
go get github.com/russross/blackfriday/v2
|
||||
|
||||
will download, compile, and install the package into your `$GOPATH`
|
||||
directory hierarchy. Alternatively, you can achieve the same if you
|
||||
import it into a project:
|
||||
will resolve and add the package to the current development module,
|
||||
then build and install it. Alternatively, you can achieve the same
|
||||
if you import it in a package:
|
||||
|
||||
import "gopkg.in/russross/blackfriday.v2"
|
||||
import "github.com/russross/blackfriday/v2"
|
||||
|
||||
and `go get` without parameters.
|
||||
|
||||
Legacy GOPATH mode is unsupported.
|
||||
|
||||
|
||||
Versions
|
||||
--------
|
||||
@@ -36,13 +40,9 @@ Versions
|
||||
Currently maintained and recommended version of Blackfriday is `v2`. It's being
|
||||
developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the
|
||||
documentation is available at
|
||||
https://godoc.org/gopkg.in/russross/blackfriday.v2.
|
||||
https://pkg.go.dev/github.com/russross/blackfriday/v2.
|
||||
|
||||
It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`,
|
||||
but we highly recommend using package management tool like [dep][7] or
|
||||
[Glide][8] and make use of semantic versioning. With package management you
|
||||
should import `github.com/russross/blackfriday` and specify that you're using
|
||||
version 2.0.0.
|
||||
It is `go get`-able in module mode at `github.com/russross/blackfriday/v2`.
|
||||
|
||||
Version 2 offers a number of improvements over v1:
|
||||
|
||||
@@ -62,6 +62,11 @@ Potential drawbacks:
|
||||
v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for
|
||||
tracking.
|
||||
|
||||
If you are still interested in the legacy `v1`, you can import it from
|
||||
`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found
|
||||
here: https://pkg.go.dev/github.com/russross/blackfriday.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
@@ -91,7 +96,7 @@ Here's an example of simple usage of Blackfriday together with Bluemonday:
|
||||
```go
|
||||
import (
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// ...
|
||||
@@ -104,6 +109,8 @@ html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
If you want to customize the set of options, use `blackfriday.WithExtensions`,
|
||||
`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`.
|
||||
|
||||
### `blackfriday-tool`
|
||||
|
||||
You can also check out `blackfriday-tool` for a more complete example
|
||||
of how to use it. Download and install it using:
|
||||
|
||||
@@ -114,7 +121,7 @@ markdown file using a standalone program. You can also browse the
|
||||
source directly on github if you are just looking for some example
|
||||
code:
|
||||
|
||||
* <http://github.com/russross/blackfriday-tool>
|
||||
* <https://github.com/russross/blackfriday-tool>
|
||||
|
||||
Note that if you have not already done so, installing
|
||||
`blackfriday-tool` will be sufficient to download and install
|
||||
@@ -123,6 +130,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that
|
||||
can be copied to wherever you need it without worrying about
|
||||
dependencies and library versions.
|
||||
|
||||
### Sanitized anchor names
|
||||
|
||||
Blackfriday includes an algorithm for creating sanitized anchor names
|
||||
corresponding to a given input text. This algorithm is used to create
|
||||
anchors for headings when `AutoHeadingIDs` extension is enabled. The
|
||||
algorithm has a specification, so that other packages can create
|
||||
compatible anchor names and links to those anchors.
|
||||
|
||||
The specification is located at https://pkg.go.dev/github.com/russross/blackfriday/v2#hdr-Sanitized_Anchor_Names.
|
||||
|
||||
[`SanitizedAnchorName`](https://pkg.go.dev/github.com/russross/blackfriday/v2#SanitizedAnchorName) exposes this functionality, and can be used to
|
||||
create compatible links to the anchor names generated by blackfriday.
|
||||
This algorithm is also implemented in a small standalone package at
|
||||
[`github.com/shurcooL/sanitized_anchor_name`](https://pkg.go.dev/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients
|
||||
that want a small package and don't need full functionality of blackfriday.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
@@ -199,6 +222,15 @@ implements the following extensions:
|
||||
You can use 3 or more backticks to mark the beginning of the
|
||||
block, and the same number to mark the end of the block.
|
||||
|
||||
To preserve classes of fenced code blocks while using the bluemonday
|
||||
HTML sanitizer, use the following policy:
|
||||
|
||||
```go
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code")
|
||||
html := p.SanitizeBytes(unsafe)
|
||||
```
|
||||
|
||||
* **Definition lists**. A simple definition list is made of a single-line
|
||||
term followed by a colon and the definition for that term.
|
||||
|
||||
@@ -250,7 +282,7 @@ Other renderers
|
||||
Blackfriday is structured to allow alternative rendering engines. Here
|
||||
are a few of note:
|
||||
|
||||
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
|
||||
* [github_flavored_markdown](https://pkg.go.dev/github.com/shurcooL/github_flavored_markdown):
|
||||
provides a GitHub Flavored Markdown renderer with fenced code block
|
||||
highlighting, clickable heading anchor links.
|
||||
|
||||
@@ -261,20 +293,28 @@ are a few of note:
|
||||
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
|
||||
but for markdown.
|
||||
|
||||
* [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX):
|
||||
* [LaTeX output](https://gitlab.com/ambrevar/blackfriday-latex):
|
||||
renders output as LaTeX.
|
||||
|
||||
* [bfchroma](https://github.com/Depado/bfchroma/): provides convenience
|
||||
integration with the [Chroma](https://github.com/alecthomas/chroma) code
|
||||
highlighting library. bfchroma is only compatible with v2 of Blackfriday and
|
||||
provides a drop-in renderer ready to use with Blackfriday, as well as
|
||||
options and means for further customization.
|
||||
|
||||
* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer.
|
||||
|
||||
* [Blackfriday-Slack](https://github.com/karriereat/blackfriday-slack): converts markdown to slack message style
|
||||
|
||||
Todo
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
* More unit testing
|
||||
* Improve unicode support. It does not understand all unicode
|
||||
* Improve Unicode support. It does not understand all Unicode
|
||||
rules (about what constitutes a letter, a punctuation symbol,
|
||||
etc.), so it may fail to detect word boundaries correctly in
|
||||
some instances. It is safe on all utf-8 input.
|
||||
some instances. It is safe on all UTF-8 input.
|
||||
|
||||
|
||||
License
|
||||
@@ -286,6 +326,10 @@ License
|
||||
[1]: https://daringfireball.net/projects/markdown/ "Markdown"
|
||||
[2]: https://golang.org/ "Go Language"
|
||||
[3]: https://github.com/vmg/sundown "Sundown"
|
||||
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
|
||||
[4]: https://pkg.go.dev/github.com/russross/blackfriday/v2#Parse "Parse func"
|
||||
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
|
||||
[6]: https://labix.org/gopkg.in "gopkg.in"
|
||||
|
||||
[BuildV2SVG]: https://travis-ci.org/russross/blackfriday.svg?branch=v2
|
||||
[BuildV2URL]: https://travis-ci.org/russross/blackfriday
|
||||
[PkgGoDevV2SVG]: https://pkg.go.dev/badge/github.com/russross/blackfriday/v2
|
||||
[PkgGoDevV2URL]: https://pkg.go.dev/github.com/russross/blackfriday/v2
|
||||
|
||||
+26
-4
@@ -18,8 +18,7 @@ import (
|
||||
"html"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/shurcooL/sanitized_anchor_name"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -259,7 +258,7 @@ func (p *Markdown) prefixHeading(data []byte) int {
|
||||
}
|
||||
if end > i {
|
||||
if id == "" && p.extensions&AutoHeadingIDs != 0 {
|
||||
id = sanitized_anchor_name.Create(string(data[i:end]))
|
||||
id = SanitizedAnchorName(string(data[i:end]))
|
||||
}
|
||||
block := p.addBlock(Heading, data[i:end])
|
||||
block.HeadingID = id
|
||||
@@ -673,6 +672,7 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int {
|
||||
if beg == 0 || beg >= len(data) {
|
||||
return 0
|
||||
}
|
||||
fenceLength := beg - 1
|
||||
|
||||
var work bytes.Buffer
|
||||
work.Write([]byte(info))
|
||||
@@ -706,6 +706,7 @@ func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int {
|
||||
if doRender {
|
||||
block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer
|
||||
block.IsFenced = true
|
||||
block.FenceLength = fenceLength
|
||||
finalizeCodeBlock(block)
|
||||
}
|
||||
|
||||
@@ -1503,7 +1504,7 @@ func (p *Markdown) paragraph(data []byte) int {
|
||||
|
||||
id := ""
|
||||
if p.extensions&AutoHeadingIDs != 0 {
|
||||
id = sanitized_anchor_name.Create(string(data[prev:eol]))
|
||||
id = SanitizedAnchorName(string(data[prev:eol]))
|
||||
}
|
||||
|
||||
block := p.addBlock(Heading, data[prev:eol])
|
||||
@@ -1588,3 +1589,24 @@ func skipUntilChar(text []byte, start int, char byte) int {
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// SanitizedAnchorName returns a sanitized anchor name for the given text.
|
||||
//
|
||||
// It implements the algorithm specified in the package comment.
|
||||
func SanitizedAnchorName(text string) string {
|
||||
var anchorName []rune
|
||||
futureDash := false
|
||||
for _, r := range text {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsNumber(r):
|
||||
if futureDash && len(anchorName) > 0 {
|
||||
anchorName = append(anchorName, '-')
|
||||
}
|
||||
futureDash = false
|
||||
anchorName = append(anchorName, unicode.ToLower(r))
|
||||
default:
|
||||
futureDash = true
|
||||
}
|
||||
}
|
||||
return string(anchorName)
|
||||
}
|
||||
|
||||
+28
@@ -15,4 +15,32 @@
|
||||
//
|
||||
// If you're interested in calling Blackfriday from command line, see
|
||||
// https://github.com/russross/blackfriday-tool.
|
||||
//
|
||||
// Sanitized Anchor Names
|
||||
//
|
||||
// Blackfriday includes an algorithm for creating sanitized anchor names
|
||||
// corresponding to a given input text. This algorithm is used to create
|
||||
// anchors for headings when AutoHeadingIDs extension is enabled. The
|
||||
// algorithm is specified below, so that other packages can create
|
||||
// compatible anchor names and links to those anchors.
|
||||
//
|
||||
// The algorithm iterates over the input text, interpreted as UTF-8,
|
||||
// one Unicode code point (rune) at a time. All runes that are letters (category L)
|
||||
// or numbers (category N) are considered valid characters. They are mapped to
|
||||
// lower case, and included in the output. All other runes are considered
|
||||
// invalid characters. Invalid characters that precede the first valid character,
|
||||
// as well as invalid character that follow the last valid character
|
||||
// are dropped completely. All other sequences of invalid characters
|
||||
// between two valid characters are replaced with a single dash character '-'.
|
||||
//
|
||||
// SanitizedAnchorName exposes this functionality, and can be used to
|
||||
// create compatible links to the anchor names generated by blackfriday.
|
||||
// This algorithm is also implemented in a small standalone package at
|
||||
// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients
|
||||
// that want a small package and don't need full functionality of blackfriday.
|
||||
package blackfriday
|
||||
|
||||
// NOTE: Keep Sanitized Anchor Name algorithm in sync with package
|
||||
// github.com/shurcooL/sanitized_anchor_name.
|
||||
// Otherwise, users of sanitized_anchor_name will get anchor names
|
||||
// that are incompatible with those generated by blackfriday.
|
||||
|
||||
+2236
File diff suppressed because it is too large
Load Diff
+39
-3
@@ -13,13 +13,27 @@ var htmlEscaper = [256][]byte{
|
||||
}
|
||||
|
||||
func escapeHTML(w io.Writer, s []byte) {
|
||||
escapeEntities(w, s, false)
|
||||
}
|
||||
|
||||
func escapeAllHTML(w io.Writer, s []byte) {
|
||||
escapeEntities(w, s, true)
|
||||
}
|
||||
|
||||
func escapeEntities(w io.Writer, s []byte, escapeValidEntities bool) {
|
||||
var start, end int
|
||||
for end < len(s) {
|
||||
escSeq := htmlEscaper[s[end]]
|
||||
if escSeq != nil {
|
||||
w.Write(s[start:end])
|
||||
w.Write(escSeq)
|
||||
start = end + 1
|
||||
isEntity, entityEnd := nodeIsEntity(s, end)
|
||||
if isEntity && !escapeValidEntities {
|
||||
w.Write(s[start : entityEnd+1])
|
||||
start = entityEnd + 1
|
||||
} else {
|
||||
w.Write(s[start:end])
|
||||
w.Write(escSeq)
|
||||
start = end + 1
|
||||
}
|
||||
}
|
||||
end++
|
||||
}
|
||||
@@ -28,6 +42,28 @@ func escapeHTML(w io.Writer, s []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func nodeIsEntity(s []byte, end int) (isEntity bool, endEntityPos int) {
|
||||
isEntity = false
|
||||
endEntityPos = end + 1
|
||||
|
||||
if s[end] == '&' {
|
||||
for endEntityPos < len(s) {
|
||||
if s[endEntityPos] == ';' {
|
||||
if entities[string(s[end:endEntityPos+1])] {
|
||||
isEntity = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isalnum(s[endEntityPos]) && s[endEntityPos] != '&' && s[endEntityPos] != '#' {
|
||||
break
|
||||
}
|
||||
endEntityPos++
|
||||
}
|
||||
}
|
||||
|
||||
return isEntity, endEntityPos
|
||||
}
|
||||
|
||||
func escLink(w io.Writer, text []byte) {
|
||||
unesc := html.UnescapeString(string(text))
|
||||
escapeHTML(w, []byte(unesc))
|
||||
|
||||
+6
-3
@@ -132,7 +132,10 @@ func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
|
||||
}
|
||||
|
||||
if params.FootnoteReturnLinkContents == "" {
|
||||
params.FootnoteReturnLinkContents = `<sup>[return]</sup>`
|
||||
// U+FE0E is VARIATION SELECTOR-15.
|
||||
// It suppresses automatic emoji presentation of the preceding
|
||||
// U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS.
|
||||
params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
|
||||
}
|
||||
|
||||
return &HTMLRenderer{
|
||||
@@ -616,7 +619,7 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
|
||||
}
|
||||
case Code:
|
||||
r.out(w, codeTag)
|
||||
escapeHTML(w, node.Literal)
|
||||
escapeAllHTML(w, node.Literal)
|
||||
r.out(w, codeCloseTag)
|
||||
case Document:
|
||||
break
|
||||
@@ -762,7 +765,7 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt
|
||||
r.cr(w)
|
||||
r.out(w, preTag)
|
||||
r.tag(w, codeTag[:len(codeTag)-1], attrs)
|
||||
escapeHTML(w, node.Literal)
|
||||
escapeAllHTML(w, node.Literal)
|
||||
r.out(w, codeCloseTag)
|
||||
r.out(w, preCloseTag)
|
||||
if node.Parent.Type != Item {
|
||||
|
||||
+1
-1
@@ -278,7 +278,7 @@ func link(p *Markdown, data []byte, offset int) (int, *Node) {
|
||||
case data[i] == '\n':
|
||||
textHasNl = true
|
||||
|
||||
case data[i-1] == '\\':
|
||||
case isBackslashEscaped(data, i):
|
||||
continue
|
||||
|
||||
case data[i] == '[':
|
||||
|
||||
+9
-3
@@ -199,7 +199,8 @@ func (n *Node) InsertBefore(sibling *Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) isContainer() bool {
|
||||
// IsContainer returns true if 'n' can contain children.
|
||||
func (n *Node) IsContainer() bool {
|
||||
switch n.Type {
|
||||
case Document:
|
||||
fallthrough
|
||||
@@ -238,6 +239,11 @@ func (n *Node) isContainer() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsLeaf returns true if 'n' is a leaf node.
|
||||
func (n *Node) IsLeaf() bool {
|
||||
return !n.IsContainer()
|
||||
}
|
||||
|
||||
func (n *Node) canContain(t NodeType) bool {
|
||||
if n.Type == List {
|
||||
return t == Item
|
||||
@@ -309,11 +315,11 @@ func newNodeWalker(root *Node) *nodeWalker {
|
||||
}
|
||||
|
||||
func (nw *nodeWalker) next() {
|
||||
if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root {
|
||||
if (!nw.current.IsContainer() || !nw.entering) && nw.current == nw.root {
|
||||
nw.current = nil
|
||||
return
|
||||
}
|
||||
if nw.entering && nw.current.isContainer() {
|
||||
if nw.entering && nw.current.IsContainer() {
|
||||
if nw.current.FirstChild != nil {
|
||||
nw.current = nw.current.FirstChild
|
||||
nw.entering = true
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.x
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 Dmitri Shuralyov
|
||||
|
||||
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.
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
sanitized_anchor_name
|
||||
=====================
|
||||
|
||||
[](https://travis-ci.org/shurcooL/sanitized_anchor_name) [](https://godoc.org/github.com/shurcooL/sanitized_anchor_name)
|
||||
|
||||
Package sanitized_anchor_name provides a func to create sanitized anchor names.
|
||||
|
||||
Its logic can be reused by multiple packages to create interoperable anchor names
|
||||
and links to those anchors.
|
||||
|
||||
At this time, it does not try to ensure that generated anchor names
|
||||
are unique, that responsibility falls on the caller.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
```bash
|
||||
go get -u github.com/shurcooL/sanitized_anchor_name
|
||||
```
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```Go
|
||||
anchorName := sanitized_anchor_name.Create("This is a header")
|
||||
|
||||
fmt.Println(anchorName)
|
||||
|
||||
// Output:
|
||||
// this-is-a-header
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
- [MIT License](LICENSE)
|
||||
-1
@@ -1 +0,0 @@
|
||||
module github.com/shurcooL/sanitized_anchor_name
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
// Package sanitized_anchor_name provides a func to create sanitized anchor names.
|
||||
//
|
||||
// Its logic can be reused by multiple packages to create interoperable anchor names
|
||||
// and links to those anchors.
|
||||
//
|
||||
// At this time, it does not try to ensure that generated anchor names
|
||||
// are unique, that responsibility falls on the caller.
|
||||
package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name"
|
||||
|
||||
import "unicode"
|
||||
|
||||
// Create returns a sanitized anchor name for the given text.
|
||||
func Create(text string) string {
|
||||
var anchorName []rune
|
||||
var futureDash = false
|
||||
for _, r := range text {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsNumber(r):
|
||||
if futureDash && len(anchorName) > 0 {
|
||||
anchorName = append(anchorName, '-')
|
||||
}
|
||||
futureDash = false
|
||||
anchorName = append(anchorName, unicode.ToLower(r))
|
||||
default:
|
||||
futureDash = true
|
||||
}
|
||||
}
|
||||
return string(anchorName)
|
||||
}
|
||||
Reference in New Issue
Block a user