ADDED:
* stringsx functions
This commit is contained in:
brent saner
2025-11-30 16:53:56 -05:00
parent 79f10b7611
commit 6ddfcdb416
3 changed files with 158 additions and 73 deletions

View File

@@ -4,8 +4,3 @@ const (
// DefMaskStr is the string used as the default maskStr if left empty in [Redact]. // DefMaskStr is the string used as the default maskStr if left empty in [Redact].
DefMaskStr string = "***" DefMaskStr string = "***"
) )
const (
// DefIndentStr is the string used as the default indent if left empty in [Indent].
DefIndentStr string = "\t"
)

View File

@@ -1,4 +1,17 @@
/* /*
Package stringsx aims to extend functionality of the stdlib [strings] module. 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 package stringsx

View File

@@ -1,96 +1,170 @@
package stringsx package stringsx
import ( import (
`fmt`
`strings` `strings`
`unicode` `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. This also means that any newlines (\n or \r\n) are inherently removed from
(To then trim any extraneous trailing space, you may want to use [TrimSpaceRight] `out` (even if included in `wordWrap`; see below).
or [TrimLines].)
If empty is true, lines with no content will be replaced with lines that purely Note that if `s` is multiline (already contains newlines), they will be respected
consist of (indent * level) (otherwise they will be left as empty lines). 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. "foo\nbar\n\n" → []string{"foo", "bar", ""}
e.g: "foo\n\nbar\n" → []string{"foo", "", "bar"}
Indent("foo\nbar\nbaz\n", "# ", 1, false, false) This splitter is particularly simple. If you need wordwrapping, it should be done
with e.g. [github.com/muesli/reflow/wordwrap].
would yield:
# foo
# bar
# baz
<empty line>
thus allowing you to "comment out" multiple lines at once.
*/ */
func Indent(s, indent string, level uint, ws, empty bool) (indented string) { func LenSplit(s string, width uint) (out []string) {
var i string var end int
var nl string var line string
var endsNewline bool var lineRunes []rune
var sb strings.Builder
var lineStripped string
if indent == "" { if width == 0 {
indent = DefIndentStr out = []string{s}
}
// This condition functionally won't do anything, so just return the input as-is.
if level == 0 {
indented = s
return 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. lineRunes = []rune(line)
if s == "" {
if empty {
indented = i
}
return
}
for line := range strings.Lines(s) { if uint(len(lineRunes)) <= width {
lineStripped = strings.TrimSpace(line) out = append(out, 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)
continue 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. return
if endsNewline && empty {
nl = getNewLine(s)
sb.WriteString(i)
} }
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 return
} }
@@ -118,6 +192,9 @@ As a safety precaution, if:
len(s) <= (leading + trailing) len(s) <= (leading + trailing)
then the entire string will be *masked* and no unmasking will be performed. 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) { 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 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) { func TrimSpaceLeft(s string) (trimmed string) {
trimmed = strings.TrimLeftFunc(s, unicode.IsSpace) trimmed = strings.TrimLeftFunc(s, unicode.IsSpace)
@@ -236,7 +313,7 @@ func TrimSpaceRight(s string) (trimmed string) {
return 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) { func getNewLine(s string) (nl string) {
if strings.HasSuffix(s, "\r\n") { if strings.HasSuffix(s, "\r\n") {