ADDED:
* `stringsx` package
** `stringsx.Indent()`, to indent/prefix multiline strings
** `stringsx.Redact()`, to mask strings
** `stringsx.TrimLines()`, like strings.TrimSpace() but multiline
** `stringsx.TrimSpaceLeft()`, like strings.TrimSpace() but only to the
left of a string.
** `stringsx.TrimSpaceRight()`, like strings.TrimSpace() but only to the
right of a string.
250 lines
5.6 KiB
Go
250 lines
5.6 KiB
Go
package stringsx
|
|
|
|
import (
|
|
`strings`
|
|
`unicode`
|
|
)
|
|
|
|
/*
|
|
Indent takes string s and indents it with string `indent` `level` times.
|
|
|
|
If indent is an empty string, [DefIndentStr] will be used.
|
|
|
|
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].)
|
|
|
|
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).
|
|
|
|
This function can also be used to prefix lines with arbitrary strings as well.
|
|
e.g:
|
|
|
|
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.
|
|
*/
|
|
func Indent(s, indent string, level uint, ws, empty bool) (indented string) {
|
|
|
|
var i string
|
|
var nl string
|
|
var endsNewline bool
|
|
var sb strings.Builder
|
|
var lineStripped string
|
|
|
|
if indent == "" {
|
|
indent = DefIndentStr
|
|
}
|
|
|
|
// This condition functionally won't do anything, so just return the input as-is.
|
|
if level == 0 {
|
|
indented = s
|
|
return
|
|
}
|
|
|
|
i = strings.Repeat(indent, int(level))
|
|
|
|
// This condition functionally won't do anything, so just return the input as-is.
|
|
if s == "" {
|
|
if empty {
|
|
indented = i
|
|
}
|
|
return
|
|
}
|
|
|
|
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)
|
|
continue
|
|
}
|
|
// non-empty/non-whitespace-only line.
|
|
sb.WriteString(i + line)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
indented = sb.String()
|
|
|
|
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.
|
|
*/
|
|
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 to be used outside of this package 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
|
|
}
|