diff --git a/internal/app/reflow.go b/internal/app/reflow.go new file mode 100644 index 0000000..3a15cd9 --- /dev/null +++ b/internal/app/reflow.go @@ -0,0 +1,31 @@ +package app + +import "strings" + +// reflowBody joins indented continuation lines within list items so that +// goldmark does not render them as hard line breaks. +func reflowBody(body string) string { + lines := strings.Split(body, "\n") + out := make([]string, 0, len(lines)) + + prevBlank := false + + for _, line := range lines { + blank := strings.TrimSpace(line) == "" + if len(out) > 0 && !prevBlank && !blank && strings.HasPrefix(line, " ") && !isListItem(line) { + out[len(out)-1] += " " + strings.TrimSpace(line) + } else { + out = append(out, line) + } + + prevBlank = blank + } + + return strings.Join(out, "\n") +} + +func isListItem(s string) bool { + t := strings.TrimLeft(s, " \t") + + return strings.HasPrefix(t, "- ") || strings.HasPrefix(t, "* ") || strings.HasPrefix(t, "+ ") +} diff --git a/internal/app/reflow_test.go b/internal/app/reflow_test.go new file mode 100644 index 0000000..f6b80c0 --- /dev/null +++ b/internal/app/reflow_test.go @@ -0,0 +1,49 @@ +package app + +import "testing" + +func TestReflowBody(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + }{ + { + name: "joins indented continuation lines", + input: "- item one with a long description that\n wraps onto the next line\n- item two\n", + want: "- item one with a long description that wraps onto the next line\n- item two\n", + }, + { + name: "does not join across blank line", + input: "- item one\n \n indented after blank\n", + want: "- item one\n \n indented after blank\n", + }, + { + name: "does not join across double newline", + input: "- item one\n\n indented after blank\n", + want: "- item one\n\n indented after blank\n", + }, + { + name: "does not join paragraph soft breaks", + input: "A paragraph with\na manual break.\n", + want: "A paragraph with\na manual break.\n", + }, + { + name: "multiple continuation lines", + input: "- The service's logic is mature\n and well-tested.\n Minimising changes reduces risk.\n- item two\n", + want: "- The service's logic is mature and well-tested. Minimising changes reduces risk.\n- item two\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if got := reflowBody(tt.input); got != tt.want { + t.Errorf("got:\n%q\nwant:\n%q", got, tt.want) + } + }) + } +} diff --git a/internal/app/show.go b/internal/app/show.go index 221ce97..f15cfd3 100644 --- a/internal/app/show.go +++ b/internal/app/show.go @@ -23,7 +23,7 @@ func Show(conf *config.Config, number int) error { fmt.Sprintf("| Number | %04d |", found.Number), fmt.Sprintf("| Date | %s |", found.Date), fmt.Sprintf("| Status | %s |", found.Status), - }, "\n") + "\n\n" + string(found.Body) + }, "\n") + "\n\n" + reflowBody(string(found.Body)) renderer, err := glamour.NewTermRenderer( glamour.WithEnvironmentConfig(),