327 lines
7.8 KiB
Go
327 lines
7.8 KiB
Go
package stringsx
|
|
|
|
import (
|
|
`fmt`
|
|
`strings`
|
|
`unicode`
|
|
)
|
|
|
|
/*
|
|
LenSplit formats string `s` to break at, at most, every `width` characters.
|
|
|
|
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).
|
|
|
|
This also means that any newlines (\n or \r\n) are inherently removed from
|
|
`out` (even if included in `wordWrap`; see below).
|
|
|
|
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:
|
|
|
|
"foo\nbar\n\n" → []string{"foo", "bar", ""}
|
|
"foo\n\nbar\n" → []string{"foo", "", "bar"}
|
|
|
|
This splitter is particularly simple. If you need wordwrapping, it should be done
|
|
with e.g. [github.com/muesli/reflow/wordwrap].
|
|
*/
|
|
func LenSplit(s string, width uint) (out []string) {
|
|
|
|
var end int
|
|
var line string
|
|
var lineRunes []rune
|
|
|
|
if width == 0 {
|
|
out = []string{s}
|
|
return
|
|
}
|
|
|
|
for line = range strings.Lines(s) {
|
|
line = strings.TrimRight(line, "\n")
|
|
line = strings.TrimRight(line, "\r")
|
|
|
|
lineRunes = []rune(line)
|
|
|
|
if uint(len(lineRunes)) <= width {
|
|
out = append(out, line)
|
|
continue
|
|
}
|
|
|
|
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]))
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/*
|
|
Redact provides a "masked" version of string s (e.g. `my_terrible_password` -> `my****************rd`).
|
|
|
|
maskStr is the character or sequence of characters
|
|
to repeat for every masked character of s.
|
|
If an empty string, the default [DefMaskStr] will be used.
|
|
(maskStr does not need to be a single character.
|
|
It is recommended to use a multi-char mask to help obfuscate a string's length.)
|
|
|
|
leading specifies the number of leading characters of s to leave *unmasked*.
|
|
If 0, no leading characters will be unmasked.
|
|
|
|
trailing specifies the number of trailing characters of s to leave *unmasked*.
|
|
if 0, no trailing characters will be unmasked.
|
|
|
|
newlines, if true, will preserve newline characters - otherwise
|
|
they will be treated as regular characters.
|
|
|
|
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) {
|
|
|
|
var nl string
|
|
var numMasked int
|
|
var sb strings.Builder
|
|
var endIdx int = int(leading)
|
|
|
|
// This condition functionally won't do anything, so just return the input as-is.
|
|
if s == "" {
|
|
return
|
|
}
|
|
|
|
if maskStr == "" {
|
|
maskStr = DefMaskStr
|
|
}
|
|
|
|
if newlines {
|
|
for line := range strings.Lines(s) {
|
|
nl = getNewLine(line)
|
|
sb.WriteString(
|
|
Redact(
|
|
strings.TrimSuffix(line, nl), maskStr, leading, trailing, false,
|
|
),
|
|
)
|
|
sb.WriteString(nl)
|
|
}
|
|
} else {
|
|
if len(s) <= int(leading+trailing) {
|
|
redacted = strings.Repeat(maskStr, len(s))
|
|
return
|
|
}
|
|
|
|
if leading == 0 && trailing == 0 {
|
|
redacted = strings.Repeat(maskStr, len(s))
|
|
return
|
|
}
|
|
|
|
numMasked = len(s) - int(leading+trailing)
|
|
endIdx = endIdx + numMasked
|
|
|
|
if leading > 0 {
|
|
sb.WriteString(s[:int(leading)])
|
|
}
|
|
|
|
sb.WriteString(strings.Repeat(maskStr, numMasked))
|
|
|
|
if trailing > 0 {
|
|
sb.WriteString(s[endIdx:])
|
|
}
|
|
}
|
|
|
|
redacted = sb.String()
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
TrimLines is like [strings.TrimSpace] but operates on *each line* of s.
|
|
It is *NIX-newline (`\n`) vs. Windows-newline (`\r\n`) agnostic.
|
|
The first encountered linebreak (`\n` vs. `\r\n`) are assumed to be
|
|
the canonical linebreak for the rest of s.
|
|
|
|
left, if true, performs a [TrimSpaceLeft] on each line (retaining the newline).
|
|
|
|
right, if true, performs a [TrimSpaceRight] on each line (retaining the newline).
|
|
*/
|
|
func TrimLines(s string, left, right bool) (trimmed string) {
|
|
|
|
var sl string
|
|
var nl string
|
|
var sb strings.Builder
|
|
|
|
// These conditions functionally won't do anything, so just return the input as-is.
|
|
if s == "" {
|
|
return
|
|
}
|
|
if !left && !right {
|
|
trimmed = s
|
|
return
|
|
}
|
|
|
|
for line := range strings.Lines(s) {
|
|
nl = getNewLine(line)
|
|
sl = strings.TrimSuffix(line, nl)
|
|
if left && right {
|
|
sl = strings.TrimSpace(sl)
|
|
} else if left {
|
|
sl = TrimSpaceLeft(sl)
|
|
} else if right {
|
|
sl = TrimSpaceRight(sl)
|
|
}
|
|
sb.WriteString(sl + nl)
|
|
}
|
|
|
|
trimmed = sb.String()
|
|
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
TrimSpaceRight is like [strings.TrimSpace] but only removes trailing whitespace from string s.
|
|
*/
|
|
func TrimSpaceRight(s string) (trimmed string) {
|
|
|
|
trimmed = strings.TrimRightFunc(s, unicode.IsSpace)
|
|
|
|
return
|
|
}
|
|
|
|
// 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") {
|
|
nl = "\r\n"
|
|
} else if strings.HasSuffix(s, "\n") {
|
|
nl = "\n"
|
|
}
|
|
|
|
return
|
|
}
|