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 }