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 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 }