Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement request: OutputLinePrefix - Add function to allow for >2 line only #15

Open
udf2457 opened this issue Jan 2, 2021 · 7 comments

Comments

@udf2457
Copy link

udf2457 commented Jan 2, 2021

Use case : using bbrks/wrap with email headers (https://tools.ietf.org/html/rfc2822#section-2.2.3)

i.e. ability to add a whitespace only to line numbers >=2, leaving first line untouched.

@bbrks
Copy link
Owner

bbrks commented Jan 2, 2021

I have a patch that makes this work, but I need to think about it a bit more to make sure I'm not overfitting the solution specifically for email header folding/making the options confusing 😄

diff --git a/wrapper.go b/wrapper.go
index 2d2e421..c08beb0 100644
--- a/wrapper.go
+++ b/wrapper.go
@@ -26,6 +26,11 @@ type Wrapper struct {
        // Default: ""
        OutputLinePrefix string

+       // If set, OutputFirstLinePrefix overrides OutputLinePrefix for the first line of output.
+       // Can be set to an empty string to avoid prepending a space in the case of email header folding.
+       // Default: nil
+       OutputFirstLinePrefix *string
+
        // OutputLineSuffix is appended to any output lines.
        // Default: ""
        OutputLineSuffix string
@@ -82,7 +87,7 @@ func (w Wrapper) Wrap(s string, limit int) string {
        for _, str := range strings.Split(s, w.Newline) {
                str = strings.TrimPrefix(str, w.TrimInputPrefix)
                str = strings.TrimSuffix(str, w.TrimInputSuffix)
-               ret += w.line(str, limit) + w.Newline
+               ret += w.line(str, limit, 0) + w.Newline
        }

        if w.StripTrailingNewline {
@@ -93,9 +98,14 @@ func (w Wrapper) Wrap(s string, limit int) string {

 // line will wrap a single line of text at the given length.
 // If limit is less than 1, the string remains unwrapped.
-func (w Wrapper) line(s string, limit int) string {
+func (w Wrapper) line(s string, limit int, depth int) string {
+       outputLinePrefix := w.OutputLinePrefix
+       if depth == 0 && w.OutputFirstLinePrefix != nil {
+               outputLinePrefix = *w.OutputFirstLinePrefix
+       }
+
        if limit < 1 || utf8.RuneCountInString(s) < limit+1 {
-               return w.OutputLinePrefix + s + w.OutputLineSuffix
+               return outputLinePrefix + s + w.OutputLineSuffix
        }

        // Find the index of the last breakpoint within the limit.
@@ -114,11 +124,11 @@ func (w Wrapper) line(s string, limit int) string {
                        i = strings.IndexAny(s, w.Breakpoints)
                        // Nothing left to do!
                        if i < 0 {
-                               return w.OutputLinePrefix + s + w.OutputLineSuffix
+                               return outputLinePrefix + s + w.OutputLineSuffix
                        }
                }
        }

        // Recurse until we have nothing left to do.
-       return w.OutputLinePrefix + s[:i] + w.OutputLineSuffix + w.Newline + w.line(s[i+breakpointWidth:], limit)
+       return outputLinePrefix + s[:i] + w.OutputLineSuffix + w.Newline + w.line(s[i+breakpointWidth:], limit, depth+1)
 }
diff --git a/wrapper_test.go b/wrapper_test.go
index 7d115bf..d4e6432 100644
--- a/wrapper_test.go
+++ b/wrapper_test.go
@@ -105,3 +105,38 @@ oooooord`,
                })
        }
 }
+
+func TestWrapper_Wrap_EmailHeaderFolding(t *testing.T) {
+       const limit = 14
+
+       tests := []struct {
+               input    string
+               expected string
+       }{
+               {
+                       input: "Subject: This is a test",
+                       expected: `Subject: This
+ is a test`,
+               },
+       }
+
+       for i, test := range tests {
+               t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) {
+                       w := wrap.NewWrapper()
+                       w.CutLongWords = true
+                       w.OutputLinePrefix = " "
+                       w.OutputFirstLinePrefix = strPtr("")
+                       w.StripTrailingNewline = true
+
+                       actual := w.Wrap(test.input, limit)
+
+                       if actual != test.expected {
+                               t.Errorf("expected %q but got %q", test.expected, actual)
+                       }
+               })
+       }
+}
+
+func strPtr(s string) *string {
+       return &s
+}

@bbrks
Copy link
Owner

bbrks commented Jan 3, 2021

I thought about it a bit more and found a neater solution without any library changes.

You can set the newline character to be "\n " such that any time Wrap breaks a line, it is followed by a single space char.

Currently Wrap doesn't take into account the width of a custom Newline string, but I think it should so I'll fix that. For now, you can subtract 1 from limit to always be within your specified width.

https://play.golang.org/p/pMyy2u043Xr

package main

import (
	"fmt"

	"github.com/bbrks/wrap/v2"
)

func main() {
	w := wrap.NewWrapper()

	// break with a newline and then a space
	w.Newline = "\n "

	const width = 80
	const text = "Subject: This is a test of wrapping where folded lines should be indented with whitespace. Each time Wrap breaks a line, the newline is followed by a single space character. We've managed to do this with existing options, rather than adding a special case for line prefixes."

	// width-1 to account for the space after \n
	fmt.Println(w.Wrap(text, width-1))
}

@udf2457
Copy link
Author

udf2457 commented Jan 3, 2021

Thanks for your thoughts on this. I'll go play with that suggestion you made. Only comment to make for now is that newline in mail is \r\n (CRLF), so would require a custom newline string, but your limit fix should be able to take care of that for now.

@udf2457
Copy link
Author

udf2457 commented Jan 3, 2021

Hmm....is it just me or have I inadvertently stumbled across a bug ?

https://play.golang.org/p/OB0h83c2BiN

The splitting behavior, even with CutLongWords = true set is, well, odd ? Splits on the hyphen in Content-Type.

Might (?) be the expected behavior with CutLongWords = false, but shouldn't be when true.

@bbrks
Copy link
Owner

bbrks commented Jan 3, 2021

It's sort of intentional for breaking in English prose, but in this case it can be fixed by limiting what characters can be considered breakpoints to only spaces:

	w.Breakpoints = " "

@bbrks
Copy link
Owner

bbrks commented Jan 3, 2021

Ps: You'll still want to -1 from limit, not len(w.Newline), we're only counting the characters that come after the break (\r\n)

@udf2457
Copy link
Author

udf2457 commented Jan 3, 2021

Thanks !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants