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

@@ -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
}
/*
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")
}
indented = sb.String()
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") {