diff --git a/go.mod b/go.mod index 95f88d17cc570050691e0e2d9c5813188a9d593f..9c36cf4692345209a7fa8dd824c323fa5ed25111 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,12 @@ go 1.25.3 require ( github.com/charmbracelet/fang v0.4.3 + github.com/microcosm-cc/bluemonday v1.0.27 github.com/spf13/cobra v1.10.1 ) require ( + github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.3.2 // indirect github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea // indirect github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect @@ -21,6 +23,7 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -32,6 +35,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.24.0 // indirect diff --git a/go.sum b/go.sum index 215d661b503b5183c51075d0c7afa1cc73775b25..0e371833fe91431ffe7bea45ed0dbbc83f711d89 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY= @@ -25,12 +27,16 @@ github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soH github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -57,6 +63,10 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= diff --git a/justfile b/justfile index a5d0c9b97733c1f155a88abcd45f1aa2db369a81..ce2aea7ef61725e83bb9e9e67fe8f998b098c707 100644 --- a/justfile +++ b/justfile @@ -37,11 +37,11 @@ reuse: build: # Building formatted-commit - CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go build -o formatted-commit -ldflags "-s -w -X main.version={{VERSION}}" ./main.go + CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go build -o formatted-commit -ldflags "-s -w -X main.version={{VERSION}}" run *FLAGS: # Running formatted-commit - CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go run -ldflags "-s -w -X main.version={{VERSION}}" ./main.go {{FLAGS}} + CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go run -ldflags "-s -w -X main.version={{VERSION}}" . {{FLAGS}} pack: # Packing formatted-commit diff --git a/main.go b/main.go index 30e4975fdd84e0e6f097b5b081710edfa49a3162..da9ab7cedfaea036da8c486e0ee88879f51eef29 100644 --- a/main.go +++ b/main.go @@ -54,8 +54,27 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \ return err } - _ = subject + var commitMsg strings.Builder + commitMsg.WriteString(subject) + + if body != "" { + formattedBody, err := formatBody(body) + if err != nil { + return fmt.Errorf("failed to format body: %w", err) + } + commitMsg.WriteString("\n\n") + commitMsg.WriteString(formattedBody) + } + + if len(trailers) > 0 { + commitMsg.WriteString("\n\n") + for _, trailer := range trailers { + commitMsg.WriteString(trailer) + commitMsg.WriteString("\n") + } + } + fmt.Print(commitMsg.String()) return nil }, } diff --git a/wrapBody.go b/wrapBody.go new file mode 100644 index 0000000000000000000000000000000000000000..b323552de6c527b11ffe8cd73012a3213f810a15 --- /dev/null +++ b/wrapBody.go @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package main + +import ( + "regexp" + "strings" + + "github.com/microcosm-cc/bluemonday" +) + +var numberedListRegex = regexp.MustCompile(`^\d+\.\s`) + +func formatBody(body string) (string, error) { + p := bluemonday.UGCPolicy() + sanitized := p.Sanitize(body) + + lines := strings.Split(sanitized, "\n") + var result []string + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + result = append(result, "") + continue + } + + if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") { + marker := trimmed[:2] + content := trimmed[2:] + wrapped := wrapWithHangingIndent(marker, " ", content, 72) + result = append(result, wrapped) + continue + } + + if numberedListRegex.MatchString(trimmed) { + parts := strings.SplitN(trimmed, " ", 2) + marker := parts[0] + " " + content := "" + if len(parts) > 1 { + content = parts[1] + } + indent := strings.Repeat(" ", len(marker)) + wrapped := wrapWithHangingIndent(marker, indent, content, 72) + result = append(result, wrapped) + continue + } + + result = append(result, wordWrap(trimmed, 72)) + } + + return strings.Join(result, "\n"), nil +} + +func wrapWithHangingIndent(firstPrefix, contPrefix, text string, width int) string { + firstWidth := width - len(firstPrefix) + contWidth := width - len(contPrefix) + + words := strings.Fields(text) + if len(words) == 0 { + return firstPrefix + } + + var lines []string + var currentLine strings.Builder + var currentWidth int + isFirstLine := true + + for _, word := range words { + wordLen := len(word) + maxWidth := firstWidth + if !isFirstLine { + maxWidth = contWidth + } + + if currentLine.Len() == 0 { + currentLine.WriteString(word) + currentWidth = wordLen + } else if currentWidth+1+wordLen <= maxWidth { + currentLine.WriteString(" ") + currentLine.WriteString(word) + currentWidth += 1 + wordLen + } else { + if isFirstLine { + lines = append(lines, firstPrefix+currentLine.String()) + isFirstLine = false + } else { + lines = append(lines, contPrefix+currentLine.String()) + } + currentLine.Reset() + currentLine.WriteString(word) + currentWidth = wordLen + } + } + + if currentLine.Len() > 0 { + if isFirstLine { + lines = append(lines, firstPrefix+currentLine.String()) + } else { + lines = append(lines, contPrefix+currentLine.String()) + } + } + + return strings.Join(lines, "\n") +} + +func wordWrap(text string, width int) string { + words := strings.Fields(text) + if len(words) == 0 { + return "" + } + + var result strings.Builder + var currentLine strings.Builder + var currentWidth int + + for _, word := range words { + wordLen := len(word) + + if currentLine.Len() == 0 { + currentLine.WriteString(word) + currentWidth = wordLen + } else if currentWidth+1+wordLen <= width { + currentLine.WriteString(" ") + currentLine.WriteString(word) + currentWidth += 1 + wordLen + } else { + if result.Len() > 0 { + result.WriteString("\n") + } + result.WriteString(currentLine.String()) + currentLine.Reset() + currentLine.WriteString(word) + currentWidth = wordLen + } + } + + if currentLine.Len() > 0 { + if result.Len() > 0 { + result.WriteString("\n") + } + result.WriteString(currentLine.String()) + } + + return result.String() +}