v1.13.0
ADDED: * stringsx functions
This commit is contained in:
@@ -4,8 +4,3 @@ const (
|
||||
// DefMaskStr is the string used as the default maskStr if left empty in [Redact].
|
||||
DefMaskStr string = "***"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefIndentStr is the string used as the default indent if left empty in [Indent].
|
||||
DefIndentStr string = "\t"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
/*
|
||||
Package stringsx aims to extend functionality of the stdlib [strings] module.
|
||||
|
||||
Note that if you need a way of mimicking Bash's shell quoting rules, [desertbit/shlex] or [buildkite/shellwords]
|
||||
would be better options than [google/shlex] but this package does not attempt to reproduce
|
||||
any of that functionality.
|
||||
|
||||
For line splitting, one should use [muesli/reflow/wordwrap].
|
||||
Likewise for indentation, one should use [muesli/reflow/indent].
|
||||
|
||||
[desertbit/shlex]: https://pkg.go.dev/github.com/desertbit/go-shlex
|
||||
[buildkite/shellwords]: https://pkg.go.dev/github.com/buildkite/shellwords
|
||||
[google/shlex]: https://pkg.go.dev/github.com/google/shlex
|
||||
[muesli/reflow/wordwrap]: https://pkg.go.dev/github.com/muesli/reflow/wordwrap
|
||||
[muesli/reflow/indent]: https://pkg.go.dev/github.com/muesli/reflow/indent
|
||||
*/
|
||||
package stringsx
|
||||
|
||||
@@ -1,96 +1,170 @@
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`strings`
|
||||
`unicode`
|
||||
)
|
||||
|
||||
/*
|
||||
Indent takes string s and indents it with string `indent` `level` times.
|
||||
LenSplit formats string `s` to break at, at most, every `width` characters.
|
||||
|
||||
If indent is an empty string, [DefIndentStr] will be used.
|
||||
Any existing newlines (e.g. \r\n) will be removed during a string/
|
||||
substring/line's length calculation. (e.g. `foobarbaz\n` and `foobarbaz\r\n` are
|
||||
both considered to be lines of length 9, not 10 and 11 respectively).
|
||||
|
||||
If ws is true, lines consisting of only whitespace will be indented as well.
|
||||
(To then trim any extraneous trailing space, you may want to use [TrimSpaceRight]
|
||||
or [TrimLines].)
|
||||
This also means that any newlines (\n or \r\n) are inherently removed from
|
||||
`out` (even if included in `wordWrap`; see below).
|
||||
|
||||
If empty is true, lines with no content will be replaced with lines that purely
|
||||
consist of (indent * level) (otherwise they will be left as empty lines).
|
||||
Note that if `s` is multiline (already contains newlines), they will be respected
|
||||
as-is - that is, if a line ends with less than `width` chars and then has a newline,
|
||||
it will be preserved as an empty element. That is to say:
|
||||
|
||||
This function can also be used to prefix lines with arbitrary strings as well.
|
||||
e.g:
|
||||
"foo\nbar\n\n" → []string{"foo", "bar", ""}
|
||||
"foo\n\nbar\n" → []string{"foo", "", "bar"}
|
||||
|
||||
Indent("foo\nbar\nbaz\n", "# ", 1, false, false)
|
||||
|
||||
would yield:
|
||||
|
||||
# foo
|
||||
# bar
|
||||
# baz
|
||||
<empty line>
|
||||
|
||||
thus allowing you to "comment out" multiple lines at once.
|
||||
This splitter is particularly simple. If you need wordwrapping, it should be done
|
||||
with e.g. [github.com/muesli/reflow/wordwrap].
|
||||
*/
|
||||
func Indent(s, indent string, level uint, ws, empty bool) (indented string) {
|
||||
func LenSplit(s string, width uint) (out []string) {
|
||||
|
||||
var i string
|
||||
var nl string
|
||||
var endsNewline bool
|
||||
var sb strings.Builder
|
||||
var lineStripped string
|
||||
var end int
|
||||
var line string
|
||||
var lineRunes []rune
|
||||
|
||||
if indent == "" {
|
||||
indent = DefIndentStr
|
||||
}
|
||||
|
||||
// This condition functionally won't do anything, so just return the input as-is.
|
||||
if level == 0 {
|
||||
indented = s
|
||||
if width == 0 {
|
||||
out = []string{s}
|
||||
return
|
||||
}
|
||||
|
||||
i = strings.Repeat(indent, int(level))
|
||||
for line = range strings.Lines(s) {
|
||||
line = strings.TrimRight(line, "\n")
|
||||
line = strings.TrimRight(line, "\r")
|
||||
|
||||
// This condition functionally won't do anything, so just return the input as-is.
|
||||
if s == "" {
|
||||
if empty {
|
||||
indented = i
|
||||
}
|
||||
return
|
||||
}
|
||||
lineRunes = []rune(line)
|
||||
|
||||
for line := range strings.Lines(s) {
|
||||
lineStripped = strings.TrimSpace(line)
|
||||
nl = getNewLine(line)
|
||||
endsNewline = nl != ""
|
||||
// fmt.Printf("%#v => %#v\n", line, lineStripped)
|
||||
if lineStripped == "" {
|
||||
// fmt.Printf("WS/EMPTY LINE (%#v) (ws %v, empty %v): \n", s, ws, empty)
|
||||
if line != (lineStripped + nl) {
|
||||
// whitespace-only line
|
||||
if ws {
|
||||
sb.WriteString(i)
|
||||
}
|
||||
} else {
|
||||
// empty line
|
||||
if empty {
|
||||
sb.WriteString(i)
|
||||
}
|
||||
}
|
||||
sb.WriteString(line)
|
||||
if uint(len(lineRunes)) <= width {
|
||||
out = append(out, line)
|
||||
continue
|
||||
}
|
||||
// non-empty/non-whitespace-only line.
|
||||
sb.WriteString(i + line)
|
||||
|
||||
for i := 0; i < len(lineRunes); i += int(width) {
|
||||
end = i + int(width)
|
||||
if end > len(lineRunes) {
|
||||
end = len(lineRunes)
|
||||
}
|
||||
out = append(out, string(lineRunes[i:end]))
|
||||
}
|
||||
}
|
||||
|
||||
// If it ends with a trailing newline and nothing after, strings.Lines() will skip the last (empty) line.
|
||||
if endsNewline && empty {
|
||||
nl = getNewLine(s)
|
||||
sb.WriteString(i)
|
||||
return
|
||||
}
|
||||
|
||||
indented = sb.String()
|
||||
/*
|
||||
LenSplitStr wraps [LenSplit] but recombines into a new string with newlines.
|
||||
|
||||
It's mostly just a convenience wrapper.
|
||||
|
||||
All arguments remain the same as in [LenSplit] with an additional one,
|
||||
`winNewLine`, which if true will use \r\n as the newline instead of \n.
|
||||
*/
|
||||
func LenSplitStr(s string, width uint, winNewline bool) (out string) {
|
||||
|
||||
var outSl []string = LenSplit(s, width)
|
||||
|
||||
if winNewline {
|
||||
out = strings.Join(outSl, "\r\n")
|
||||
} else {
|
||||
out = strings.Join(outSl, "\n")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Pad pads each element in `s` to length `width` using `pad`.
|
||||
If `pad` is empty, a single space (0x20) will be assumed.
|
||||
Note that `width` operates on rune size, not byte size.
|
||||
(In ASCII, they will be the same size.)
|
||||
|
||||
If a line in `s` is greater than or equal to `width`,
|
||||
no padding will be performed.
|
||||
|
||||
If `leftPad` is true, padding will be applied to the "left" (beginning")
|
||||
of each element instead of the "right" ("end").
|
||||
*/
|
||||
func Pad(s []string, width uint, pad string, leftPad bool) (out []string) {
|
||||
|
||||
var idx int
|
||||
var padIdx int
|
||||
var runeIdx int
|
||||
var padLen uint
|
||||
var elem string
|
||||
var unpadLen uint
|
||||
var tmpPadLen int
|
||||
var padRunes []rune
|
||||
var tmpPad []rune
|
||||
|
||||
if width == 0 {
|
||||
out = s
|
||||
return
|
||||
}
|
||||
|
||||
out = make([]string, len(s))
|
||||
|
||||
// Easy; supported directly in fmt.
|
||||
if pad == "" {
|
||||
for idx, elem = range s {
|
||||
if leftPad {
|
||||
out[idx] = fmt.Sprintf("%*s", width, elem)
|
||||
} else {
|
||||
out[idx] = fmt.Sprintf("%-*s", width, elem)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// This gets a little more tricky.
|
||||
padRunes = []rune(pad)
|
||||
padLen = uint(len(padRunes))
|
||||
for idx, elem = range s {
|
||||
// First we need to know the number of runes in elem.
|
||||
unpadLen = uint(len([]rune(elem)))
|
||||
// If it's more than/equal to width, as-is.
|
||||
if unpadLen >= width {
|
||||
out[idx] = elem
|
||||
} else {
|
||||
// Otherwise, we need to construct/calculate a pad.
|
||||
if (width-unpadLen)%padLen == 0 {
|
||||
// Also easy enough.
|
||||
if leftPad {
|
||||
out[idx] = fmt.Sprintf("%s%s", strings.Repeat(pad, int((width-unpadLen)/padLen)), elem)
|
||||
} else {
|
||||
out[idx] = fmt.Sprintf("%s%s", elem, strings.Repeat(pad, int((width-unpadLen)/padLen)))
|
||||
}
|
||||
} else {
|
||||
// This is where it gets a little hairy.
|
||||
tmpPad = []rune{}
|
||||
tmpPadLen = int(width - unpadLen)
|
||||
idx = 0
|
||||
padIdx = 0
|
||||
for runeIdx = range tmpPadLen {
|
||||
tmpPad[runeIdx] = padRunes[padIdx]
|
||||
if uint(padIdx) >= padLen {
|
||||
padIdx = 0
|
||||
} else {
|
||||
padIdx++
|
||||
}
|
||||
runeIdx++
|
||||
}
|
||||
if leftPad {
|
||||
out[idx] = fmt.Sprintf("%s%s", string(tmpPad), elem)
|
||||
} else {
|
||||
out[idx] = fmt.Sprintf("%s%s", elem, string(tmpPad))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -118,6 +192,9 @@ As a safety precaution, if:
|
||||
len(s) <= (leading + trailing)
|
||||
|
||||
then the entire string will be *masked* and no unmasking will be performed.
|
||||
|
||||
Note that this DOES NOT do a string *replace*, it provides a masked version of `s` itself.
|
||||
Wrap Redact with [strings.ReplaceAll] if you want to replace a certain value with a masked one.
|
||||
*/
|
||||
func Redact(s, maskStr string, leading, trailing uint, newlines bool) (redacted string) {
|
||||
|
||||
@@ -218,7 +295,7 @@ func TrimLines(s string, left, right bool) (trimmed string) {
|
||||
return
|
||||
}
|
||||
|
||||
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string s.
|
||||
// TrimSpaceLeft is like [strings.TrimSpace] but only removes leading whitespace from string `s`.
|
||||
func TrimSpaceLeft(s string) (trimmed string) {
|
||||
|
||||
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
|
||||
@@ -236,7 +313,7 @@ func TrimSpaceRight(s string) (trimmed string) {
|
||||
return
|
||||
}
|
||||
|
||||
// getNewLine is too unpredictable to be used outside of this package so it isn't exported.
|
||||
// getNewLine is too unpredictable/nuanced to be used as part of a public API promise so it isn't exported.
|
||||
func getNewLine(s string) (nl string) {
|
||||
|
||||
if strings.HasSuffix(s, "\r\n") {
|
||||
|
||||
Reference in New Issue
Block a user