diff --git a/main.go b/main.go index 6a9f59e..3b0105e 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,8 @@ import ( "golang.org/x/term" ) +const DEBUG = false + var ( // upper-left, upper-right, left side, right side, bottom-left, bottom-right SAY_BORDER = []rune{'/', '\\', '|', '|', '\\', '/'} @@ -103,7 +105,7 @@ func main() { if err != nil { log.Fatalf("error reading from stdin: %v", err) } - input = strings.TrimSpace(string(pipedInput)) + input = string(bytes.TrimSpace(pipedInput)) } if input == "" { @@ -115,7 +117,7 @@ func main() { log.Fatal(err) } - generateCow(string(b), input, opts) + fmt.Print(generateCow(string(b), input, opts)) } func generateCow(cow, say string, opt options) string { @@ -157,21 +159,73 @@ func generateCow(cow, say string, opt options) string { return out.String() } +// this is an attempt to emulate the rather poorly-documented behavior of Text::Wrap::fill +// from CPAN: https://github.com/ap/Text-Tabs/blob/master/lib.modern/Text/Wrap.pm +// The basic idea is: take an input. Format each "paragraph" of text independently, +// then merge with empty lines between. +// +// What is under-defined in the documentation is what fill() considers a "paragraph". +// From testing and reviewing the rather obtuse code, a paragraph is any number of lines +// that start with a line that has optional whitespace at its start, then all subsequent lines +// that have no whitespace at the start, until either reaching another line that has leading +// whitespace, or a blank line. fill() also "destroy[s] any whitespace in the original text", +// by which it appearently means all tabs and multiple-spaces are replaced with single-spaces. +// +// So, for example, `text\ntext` is one paragraph, `text\n text` is two paragraphs. Since for +// our purposes we don't want any indentation of paragraphs, the output of these two examples +// should be `text text` and `text\n\ntext`, respectively. +// +// This is a pretty gnarly! Might be easier in a multi-pass model: collect together each paragraph, +// collapse them into single strings and format in a sub-function, then merge them all together. +// That's closer to how the perl module does it. func wordWrap(text []string, column int) ([]string, int) { out := []string{} length := 0 var b strings.Builder - for _, line := range text { - b = strings.Builder{} + var inParagraph bool + for beginning, line := range text { // remove control character whitespace line = strings.ReplaceAll(line, "\t", " ") line = strings.ReplaceAll(line, "\v", " ") line = strings.ReplaceAll(line, "\f", " ") + line = strings.ReplaceAll(line, "\r", " ") words := strings.Split(line, " ") + // skip empty newlines if not in a paragraph, but start a new paragraph if we are. + if strings.TrimSpace(line) == "" { + if inParagraph { + length = max(length, b.Len()-1) + out = append(out, b.String()[0:b.Len()-1]) + out = append(out, "") + b = strings.Builder{} + inParagraph = false + } + continue + } // we -1 all over the place to clean up the loose space on the end of the builder - for _, word := range words { // split into words - if b.Len() > 0 && word == "" { - // skip multiple spaces in a row + for i, word := range words { // split into words + if DEBUG { + log.Printf("%d, `%s`", i, b.String()) + } + if inParagraph && i == 0 && word == "" && len(words) != 0 { + // we've found a new paragraph while we were still processing the old one. + // (that is, the new line we're parsing had leading whitespace) + length = max(length, b.Len()-1) + out = append(out, b.String()[0:b.Len()-1]) + out = append(out, "") + b = strings.Builder{} + log.Println("here") + } + // bizarrely, cowsay allows for indentation to survive in the /first/ paragraph, + // but only up to two spaces worth. + if beginning == 0 && (b.Len() == 0 || b.String() == " ") && word == "" { + b.WriteRune(' ') + } + if b.Len() == 0 && word != "" { + inParagraph = true + } + if b.Len() >= 0 && word == "" && i+1 != len(words) { + // skip multiple spaces in a row... + // ...but a single trailing space on a line is OK?? continue } if b.Len() > 0 && b.Len()+len(word)+1 > column { @@ -182,8 +236,9 @@ func wordWrap(text []string, column int) ([]string, int) { out = append(out, b.String()[0:b.Len()-1]) b = strings.Builder{} } - if b.Len() == 0 && len(word) >= column { + for b.Len() == 0 && len(word) >= column { // our word is longer than our maximum column size. let's break it up. + // we loop until we've consumed the full overly-long word. length = max(length, column-1) out = append(out, word[0:column-1]) word = word[column-1:] @@ -192,11 +247,14 @@ func wordWrap(text []string, column int) ([]string, int) { // actually append the word and a space b.WriteString(word) b.WriteRune(' ') + if DEBUG { + log.Printf("%d, `%s`", i, b.String()) + } } - // out of words in the line, so save it off and start on the next one - length = max(length, b.Len()-1) - out = append(out, b.String()[0:b.Len()-1]) } + // out of words! save off our last line. + length = max(length, b.Len()-1) + out = append(out, b.String()[0:b.Len()-1]) return out, length } diff --git a/main_test.go b/main_test.go index 3997df7..87bef08 100644 --- a/main_test.go +++ b/main_test.go @@ -43,28 +43,37 @@ func TestGenerateCow(t *testing.T) { expected: string(must(os.ReadFile("./testdata/basic.cow"))), }, { - name: "multiline test", + name: "single-paragraph test", args: args{ cow: string(must(os.ReadFile("./cows/default.cow"))), say: "test\ntext", opt: defaultOptions, }, - expected: string(must(os.ReadFile("./testdata/multiline.cow"))), + expected: string(must(os.ReadFile("./testdata/one_paragraph.cow"))), }, { name: "multiline with wordwrap test", args: args{ cow: string(must(os.ReadFile("./cows/default.cow"))), - say: "this is a long block of text.\n\n\nIt goes over many lines! It'll even word-wrap for us.", + say: "this is a long block of text.\nIt goes over many lines! It'll even word-wrap for us.", opt: defaultOptions, }, expected: string(must(os.ReadFile("./testdata/longer_multiline.cow"))), }, + { + name: "two-paragraph multiline with wordwrap test", + args: args{ + cow: string(must(os.ReadFile("./cows/default.cow"))), + say: "this is a long block of text.\n\nIt goes over many lines! It'll even word-wrap for us.", + opt: defaultOptions, + }, + expected: string(must(os.ReadFile("./testdata/two_paragraph_multiline.cow"))), + }, { name: "offset bubble", args: args{ cow: string(must(os.ReadFile("./cows/dragon-and-cow.cow"))), - say: "this is a long block of text.\n\n\nIt goes over many lines! It'll even word-wrap for us.", + say: "this is a long block of text.\nIt goes over many lines! It'll even word-wrap for us.", opt: defaultOptions, }, expected: string(must(os.ReadFile("./testdata/dragon.cow"))), @@ -105,6 +114,25 @@ func TestGenerateCow(t *testing.T) { }, expected: string(must(os.ReadFile("./testdata/too_long.cow"))), }, + { + name: "fortune cow", + args: args{ + cow: string(must(os.ReadFile("./cows/default.cow"))), + say: `Most people eat as though they were fattening themselves for market. + -- E.W. Howe`, + opt: defaultOptions, + }, + expected: string(must(os.ReadFile("./testdata/fortune.cow"))), + }, + { + name: "trailing space cow", + args: args{ + cow: string(must(os.ReadFile("./cows/default.cow"))), + say: `test `, + opt: defaultOptions, + }, + expected: string(must(os.ReadFile("./testdata/trailing_space.cow"))), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/testdata/dragon.cow b/testdata/dragon.cow index e773e7c..d2338a0 100644 --- a/testdata/dragon.cow +++ b/testdata/dragon.cow @@ -1,10 +1,8 @@ - _____________________________________ - / this is a long block of text. \ - | | - | | - | It goes over many lines! It'll even | - \ word-wrap for us. / - ------------------------------------- + _______________________________________ + / this is a long block of text. It goes \ + | over many lines! It'll even word-wrap | + \ for us. / + --------------------------------------- \ ^ /^ \ / \ // \ \ |\___/| / \// .\ diff --git a/testdata/fortune.cow b/testdata/fortune.cow new file mode 100644 index 0000000..db77feb --- /dev/null +++ b/testdata/fortune.cow @@ -0,0 +1,11 @@ + _____________________________________ +/ Most people eat as though they were \ +| fattening themselves for market. | +| | +\ -- E.W. Howe / + ------------------------------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || diff --git a/testdata/fuzz/FuzzCow/7b9a589d5aa859e5 b/testdata/fuzz/FuzzCow/7b9a589d5aa859e5 new file mode 100644 index 0000000..4dbc723 --- /dev/null +++ b/testdata/fuzz/FuzzCow/7b9a589d5aa859e5 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0 \n0") diff --git a/testdata/fuzz/FuzzCow/93d7daf11243d295 b/testdata/fuzz/FuzzCow/93d7daf11243d295 new file mode 100644 index 0000000..2583705 --- /dev/null +++ b/testdata/fuzz/FuzzCow/93d7daf11243d295 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0000000000000000000000000000000000000000000000000000000000000000000000000000000") diff --git a/testdata/fuzz/FuzzCow/ceaa3f997e266d98 b/testdata/fuzz/FuzzCow/ceaa3f997e266d98 new file mode 100644 index 0000000..434e125 --- /dev/null +++ b/testdata/fuzz/FuzzCow/ceaa3f997e266d98 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0 \n 0") diff --git a/testdata/fuzz/FuzzCow/dde7a120ccdf11af b/testdata/fuzz/FuzzCow/dde7a120ccdf11af new file mode 100644 index 0000000..2e299a0 --- /dev/null +++ b/testdata/fuzz/FuzzCow/dde7a120ccdf11af @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0\r0") diff --git a/testdata/longer_multiline.cow b/testdata/longer_multiline.cow index f2b3dad..75fb746 100644 --- a/testdata/longer_multiline.cow +++ b/testdata/longer_multiline.cow @@ -1,10 +1,8 @@ - _____________________________________ -/ this is a long block of text. \ -| | -| | -| It goes over many lines! It'll even | -\ word-wrap for us. / - ------------------------------------- + _______________________________________ +/ this is a long block of text. It goes \ +| over many lines! It'll even word-wrap | +\ for us. / + --------------------------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ diff --git a/testdata/one_paragraph.cow b/testdata/one_paragraph.cow new file mode 100644 index 0000000..79a0547 --- /dev/null +++ b/testdata/one_paragraph.cow @@ -0,0 +1,8 @@ + ___________ +< test text > + ----------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || diff --git a/testdata/multiline.cow b/testdata/trailing_space.cow similarity index 77% rename from testdata/multiline.cow rename to testdata/trailing_space.cow index baaff9c..fb6aee5 100644 --- a/testdata/multiline.cow +++ b/testdata/trailing_space.cow @@ -1,7 +1,6 @@ - ______ -/ test \ -\ text / - ------ + _______ +< test > + ------- \ ^__^ \ (oo)\_______ (__)\ )\/\ diff --git a/testdata/two_paragraph_multiline.cow b/testdata/two_paragraph_multiline.cow new file mode 100644 index 0000000..b3da94e --- /dev/null +++ b/testdata/two_paragraph_multiline.cow @@ -0,0 +1,11 @@ + _____________________________________ +/ this is a long block of text. \ +| | +| It goes over many lines! It'll even | +\ word-wrap for us. / + ------------------------------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || ||