diff --git a/stringsx/consts.go b/stringsx/consts.go index b7892a8..ee76a31 100644 --- a/stringsx/consts.go +++ b/stringsx/consts.go @@ -4,8 +4,3 @@ const ( // DefMaskStr is the string used as the default maskStr if left empty in [Redact]. DefMaskStr string = "***" ) - -const ( - // DefIndentStr is the string used as the default indent if left empty in [Indent]. - DefIndentStr string = "\t" -) diff --git a/stringsx/doc.go b/stringsx/doc.go index e75c545..2307bba 100644 --- a/stringsx/doc.go +++ b/stringsx/doc.go @@ -1,4 +1,17 @@ /* Package stringsx aims to extend functionality of the stdlib [strings] module. + +Note that if you need a way of mimicking Bash's shell quoting rules, [desertbit/shlex] or [buildkite/shellwords] +would be better options than [google/shlex] but this package does not attempt to reproduce +any of that functionality. + +For line splitting, one should use [muesli/reflow/wordwrap]. +Likewise for indentation, one should use [muesli/reflow/indent]. + +[desertbit/shlex]: https://pkg.go.dev/github.com/desertbit/go-shlex +[buildkite/shellwords]: https://pkg.go.dev/github.com/buildkite/shellwords +[google/shlex]: https://pkg.go.dev/github.com/google/shlex +[muesli/reflow/wordwrap]: https://pkg.go.dev/github.com/muesli/reflow/wordwrap +[muesli/reflow/indent]: https://pkg.go.dev/github.com/muesli/reflow/indent */ package stringsx diff --git a/stringsx/funcs.go b/stringsx/funcs.go index 9ed1963..6b0bc75 100644 --- a/stringsx/funcs.go +++ b/stringsx/funcs.go @@ -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 - - -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") {