forked from mirrors/gotosocial
353 lines
7.7 KiB
Go
353 lines
7.7 KiB
Go
|
package format
|
||
|
|
||
|
import (
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// Formatter allows configuring value and string formatting.
|
||
|
type Formatter struct {
|
||
|
// MaxDepth specifies the max depth of fields the formatter will iterate.
|
||
|
// Once max depth is reached, value will simply be formatted as "...".
|
||
|
// e.g.
|
||
|
//
|
||
|
// MaxDepth=1
|
||
|
// type A struct{
|
||
|
// Nested B
|
||
|
// }
|
||
|
// type B struct{
|
||
|
// Nested C
|
||
|
// }
|
||
|
// type C struct{
|
||
|
// Field string
|
||
|
// }
|
||
|
//
|
||
|
// Append(&buf, A{}) => {Nested={Nested={Field=...}}}
|
||
|
MaxDepth uint8
|
||
|
}
|
||
|
|
||
|
// Append will append formatted form of supplied values into 'buf'.
|
||
|
func (f Formatter) Append(buf *Buffer, v ...interface{}) {
|
||
|
for _, v := range v {
|
||
|
appendIfaceOrRValue(format{maxd: f.MaxDepth, buf: buf}, v)
|
||
|
buf.AppendByte(' ')
|
||
|
}
|
||
|
if len(v) > 0 {
|
||
|
buf.Truncate(1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Appendf will append the formatted string with supplied values into 'buf'.
|
||
|
// Supported format directives:
|
||
|
// - '{}' => format supplied arg, in place
|
||
|
// - '{0}' => format arg at index 0 of supplied, in place
|
||
|
// - '{:?}' => format supplied arg verbosely, in place
|
||
|
// - '{:k}' => format supplied arg as key, in place
|
||
|
// - '{:v}' => format supplied arg as value, in place
|
||
|
//
|
||
|
// To escape either of '{}' simply append an additional brace e.g.
|
||
|
// - '{{' => '{'
|
||
|
// - '}}' => '}'
|
||
|
// - '{{}}' => '{}'
|
||
|
// - '{{:?}}' => '{:?}'
|
||
|
//
|
||
|
// More formatting directives might be included in the future.
|
||
|
func (f Formatter) Appendf(buf *Buffer, s string, a ...interface{}) {
|
||
|
const (
|
||
|
// ground state
|
||
|
modeNone = uint8(0)
|
||
|
|
||
|
// prev reached '{'
|
||
|
modeOpen = uint8(1)
|
||
|
|
||
|
// prev reached '}'
|
||
|
modeClose = uint8(2)
|
||
|
|
||
|
// parsing directive index
|
||
|
modeIdx = uint8(3)
|
||
|
|
||
|
// parsing directive operands
|
||
|
modeOp = uint8(4)
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// mode is current parsing mode
|
||
|
mode uint8
|
||
|
|
||
|
// arg is the current arg index
|
||
|
arg int
|
||
|
|
||
|
// carg is current directive-set arg index
|
||
|
carg int
|
||
|
|
||
|
// last is the trailing cursor to see slice windows
|
||
|
last int
|
||
|
|
||
|
// idx is the current index in 's'
|
||
|
idx int
|
||
|
|
||
|
// fmt is the base argument formatter
|
||
|
fmt = format{
|
||
|
maxd: f.MaxDepth,
|
||
|
buf: buf,
|
||
|
}
|
||
|
|
||
|
// NOTE: these functions are defined here as function
|
||
|
// locals as it turned out to be better for performance
|
||
|
// doing it this way, than encapsulating their logic in
|
||
|
// some kind of parsing structure. Maybe if the parser
|
||
|
// was pooled along with the buffers it might work out
|
||
|
// better, but then it makes more internal functions i.e.
|
||
|
// .Append() .Appendf() less accessible outside package.
|
||
|
//
|
||
|
// Currently, passing '-gcflags "-l=4"' causes a not
|
||
|
// insignificant decrease in ns/op, which is likely due
|
||
|
// to more aggressive function inlining, which this
|
||
|
// function can obviously stand to benefit from :)
|
||
|
|
||
|
// Str returns current string window slice, and updates
|
||
|
// the trailing cursor 'last' to current 'idx'
|
||
|
Str = func() string {
|
||
|
str := s[last:idx]
|
||
|
last = idx
|
||
|
return str
|
||
|
}
|
||
|
|
||
|
// MoveUp moves the trailing cursor 'last' just past 'idx'
|
||
|
MoveUp = func() {
|
||
|
last = idx + 1
|
||
|
}
|
||
|
|
||
|
// MoveUpTo moves the trailing cursor 'last' either up to
|
||
|
// closest '}', or current 'idx', whichever is furthest
|
||
|
MoveUpTo = func() {
|
||
|
if i := strings.IndexByte(s[idx:], '}'); i >= 0 {
|
||
|
idx += i
|
||
|
}
|
||
|
MoveUp()
|
||
|
}
|
||
|
|
||
|
// ParseIndex parses an integer from the current string
|
||
|
// window, updating 'last' to 'idx'. The string window
|
||
|
// is ASSUMED to contain only valid ASCII numbers. This
|
||
|
// only returns false if number exceeds platform int size
|
||
|
ParseIndex = func() bool {
|
||
|
// Get current window
|
||
|
str := Str()
|
||
|
if len(str) < 1 {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// Index HAS to fit within platform int
|
||
|
if !can32bitInt(str) && !can64bitInt(str) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Build integer from string
|
||
|
carg = 0
|
||
|
for _, c := range []byte(str) {
|
||
|
carg = carg*10 + int(c-'0')
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// ParseOp parses operands from the current string
|
||
|
// window, updating 'last' to 'idx'. The string window
|
||
|
// is ASSUMED to contain only valid operand ASCII. This
|
||
|
// returns success on parsing of operand logic
|
||
|
ParseOp = func() bool {
|
||
|
// Get current window
|
||
|
str := Str()
|
||
|
if len(str) < 1 {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// (for now) only
|
||
|
// accept length = 1
|
||
|
if len(str) > 1 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
switch str[0] {
|
||
|
case 'k':
|
||
|
fmt.flags |= isKeyBit
|
||
|
case 'v':
|
||
|
fmt.flags |= isValBit
|
||
|
case '?':
|
||
|
fmt.flags |= vboseBit
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// AppendArg will take either the directive-set, or
|
||
|
// iterated arg index, check within bounds of 'a' and
|
||
|
// append the that argument formatted to the buffer.
|
||
|
// On failure, it will append an error string
|
||
|
AppendArg = func() {
|
||
|
// Look for idx
|
||
|
if carg < 0 {
|
||
|
carg = arg
|
||
|
}
|
||
|
|
||
|
// Incr idx
|
||
|
arg++
|
||
|
|
||
|
if carg < len(a) {
|
||
|
// Append formatted argument value
|
||
|
appendIfaceOrRValue(fmt, a[carg])
|
||
|
} else {
|
||
|
// No argument found for index
|
||
|
buf.AppendString(`!{MISSING_ARG}`)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reset will reset the mode to ground, the flags
|
||
|
// to empty and parsed 'carg' to empty
|
||
|
Reset = func() {
|
||
|
mode = modeNone
|
||
|
fmt.flags = 0
|
||
|
carg = -1
|
||
|
}
|
||
|
)
|
||
|
|
||
|
for idx = 0; idx < len(s); idx++ {
|
||
|
// Get next char
|
||
|
c := s[idx]
|
||
|
|
||
|
switch mode {
|
||
|
// Ground mode
|
||
|
case modeNone:
|
||
|
switch c {
|
||
|
case '{':
|
||
|
// Enter open mode
|
||
|
buf.AppendString(Str())
|
||
|
mode = modeOpen
|
||
|
MoveUp()
|
||
|
case '}':
|
||
|
// Enter close mode
|
||
|
buf.AppendString(Str())
|
||
|
mode = modeClose
|
||
|
MoveUp()
|
||
|
}
|
||
|
|
||
|
// Encountered open '{'
|
||
|
case modeOpen:
|
||
|
switch c {
|
||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||
|
// Starting index
|
||
|
mode = modeIdx
|
||
|
MoveUp()
|
||
|
case '{':
|
||
|
// Escaped bracket
|
||
|
buf.AppendByte('{')
|
||
|
mode = modeNone
|
||
|
MoveUp()
|
||
|
case '}':
|
||
|
// Format arg
|
||
|
AppendArg()
|
||
|
Reset()
|
||
|
MoveUp()
|
||
|
case ':':
|
||
|
// Starting operands
|
||
|
mode = modeOp
|
||
|
MoveUp()
|
||
|
default:
|
||
|
// Bad char, missing a close
|
||
|
buf.AppendString(`!{MISSING_CLOSE}`)
|
||
|
mode = modeNone
|
||
|
MoveUpTo()
|
||
|
}
|
||
|
|
||
|
// Encountered close '}'
|
||
|
case modeClose:
|
||
|
switch c {
|
||
|
case '}':
|
||
|
// Escaped close bracket
|
||
|
buf.AppendByte('}')
|
||
|
mode = modeNone
|
||
|
MoveUp()
|
||
|
default:
|
||
|
// Missing an open bracket
|
||
|
buf.AppendString(`!{MISSING_OPEN}`)
|
||
|
mode = modeNone
|
||
|
MoveUp()
|
||
|
}
|
||
|
|
||
|
// Preparing index
|
||
|
case modeIdx:
|
||
|
switch c {
|
||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||
|
case ':':
|
||
|
if !ParseIndex() {
|
||
|
// Unable to parse an integer
|
||
|
buf.AppendString(`!{BAD_INDEX}`)
|
||
|
mode = modeNone
|
||
|
MoveUpTo()
|
||
|
} else {
|
||
|
// Starting operands
|
||
|
mode = modeOp
|
||
|
MoveUp()
|
||
|
}
|
||
|
case '}':
|
||
|
if !ParseIndex() {
|
||
|
// Unable to parse an integer
|
||
|
buf.AppendString(`!{BAD_INDEX}`)
|
||
|
} else {
|
||
|
// Format arg
|
||
|
AppendArg()
|
||
|
}
|
||
|
Reset()
|
||
|
MoveUp()
|
||
|
default:
|
||
|
// Not a valid index character
|
||
|
buf.AppendString(`!{BAD_INDEX}`)
|
||
|
mode = modeNone
|
||
|
MoveUpTo()
|
||
|
}
|
||
|
|
||
|
// Preparing operands
|
||
|
case modeOp:
|
||
|
switch c {
|
||
|
case 'k', 'v', '?':
|
||
|
// TODO: set flags as received
|
||
|
case '}':
|
||
|
if !ParseOp() {
|
||
|
// Unable to parse operands
|
||
|
buf.AppendString(`!{BAD_OPERAND}`)
|
||
|
} else {
|
||
|
// Format arg
|
||
|
AppendArg()
|
||
|
}
|
||
|
Reset()
|
||
|
MoveUp()
|
||
|
default:
|
||
|
// Not a valid operand char
|
||
|
buf.AppendString(`!{BAD_OPERAND}`)
|
||
|
Reset()
|
||
|
MoveUpTo()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Append any remaining
|
||
|
buf.AppendString(s[last:])
|
||
|
}
|
||
|
|
||
|
// formatter is the default formatter instance.
|
||
|
var formatter = Formatter{
|
||
|
MaxDepth: 10,
|
||
|
}
|
||
|
|
||
|
// Append will append formatted form of supplied values into 'buf' using default formatter.
|
||
|
// See Formatter.Append() for more documentation.
|
||
|
func Append(buf *Buffer, v ...interface{}) {
|
||
|
formatter.Append(buf, v...)
|
||
|
}
|
||
|
|
||
|
// Appendf will append the formatted string with supplied values into 'buf' using default formatter.
|
||
|
// See Formatter.Appendf() for more documentation.
|
||
|
func Appendf(buf *Buffer, s string, a ...interface{}) {
|
||
|
formatter.Appendf(buf, s, a...)
|
||
|
}
|