├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── go.yml ├── go.mod ├── LICENSE ├── dedent.go ├── README.md └── dedent_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lithammer 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lithammer/dedent 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | platform: [ubuntu-latest, macos-latest, windows-latest] 12 | go-version: ["1.24", "1.25"] 13 | runs-on: ${{ matrix.platform }} 14 | steps: 15 | - name: Setup Go 16 | uses: actions/setup-go@v6 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | 20 | - name: Checkout 21 | uses: actions/checkout@v5 22 | 23 | - name: Lint 24 | uses: golangci/golangci-lint-action@v8.0.0 25 | 26 | - name: Build 27 | run: go build -v ./... 28 | 29 | - name: Test 30 | run: go test -v ./... 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Peter Lithammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /dedent.go: -------------------------------------------------------------------------------- 1 | package dedent 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$") 10 | leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])") 11 | ) 12 | 13 | // Dedent removes any common leading whitespace from every line in text. 14 | // 15 | // This can be used to make multiline strings to line up with the left edge of 16 | // the display, while still presenting them in the source code in indented 17 | // form. 18 | func Dedent(text string) string { 19 | var margin string 20 | 21 | text = whitespaceOnly.ReplaceAllString(text, "") 22 | indents := leadingWhitespace.FindAllStringSubmatch(text, -1) 23 | 24 | // Look for the longest leading string of spaces and tabs common to all 25 | // lines. 26 | for i, indent := range indents { 27 | if i == 0 { 28 | margin = indent[1] 29 | } else if strings.HasPrefix(indent[1], margin) { 30 | // Current line more deeply indented than previous winner: 31 | // no change (previous winner is still on top). 32 | continue 33 | } else if strings.HasPrefix(margin, indent[1]) { 34 | // Current line consistent with and no deeper than previous winner: 35 | // it's the new winner. 36 | margin = indent[1] 37 | } else { 38 | // Current line and previous winner have no common whitespace: 39 | // there is no margin. 40 | margin = "" 41 | break 42 | } 43 | } 44 | 45 | if margin != "" { 46 | text = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(text, "") 47 | } 48 | return text 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dedent 2 | 3 | [![Build Status](https://github.com/lithammer/dedent/workflows/Go/badge.svg)](https://github.com/lithammer/dedent/actions) 4 | [![Godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/lithammer/dedent) 5 | 6 | Removes common leading whitespace from multiline strings. Inspired by [`textwrap.dedent`](https://docs.python.org/3/library/textwrap.html#textwrap.dedent) in Python. 7 | 8 | ## Usage / example 9 | 10 | Imagine the following snippet that prints a multiline string. You want the indentation to both look nice in the code as well as in the actual output. 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | 18 | "github.com/lithammer/dedent" 19 | ) 20 | 21 | func main() { 22 | s := ` 23 | Lorem ipsum dolor sit amet, 24 | consectetur adipiscing elit. 25 | Curabitur justo tellus, facilisis nec efficitur dictum, 26 | fermentum vitae ligula. Sed eu convallis sapien.` 27 | fmt.Println(dedent.Dedent(s)) 28 | fmt.Println("-------------") 29 | fmt.Println(s) 30 | } 31 | ``` 32 | 33 | To illustrate the difference, here's the output: 34 | 35 | 36 | ```bash 37 | $ go run main.go 38 | Lorem ipsum dolor sit amet, 39 | consectetur adipiscing elit. 40 | Curabitur justo tellus, facilisis nec efficitur dictum, 41 | fermentum vitae ligula. Sed eu convallis sapien. 42 | ------------- 43 | 44 | Lorem ipsum dolor sit amet, 45 | consectetur adipiscing elit. 46 | Curabitur justo tellus, facilisis nec efficitur dictum, 47 | fermentum vitae ligula. Sed eu convallis sapien. 48 | ``` 49 | 50 | ## License 51 | 52 | MIT 53 | -------------------------------------------------------------------------------- /dedent_test.go: -------------------------------------------------------------------------------- 1 | package dedent 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | const errorMsg = "\nexpected %q\ngot %q" 9 | 10 | type dedentTest struct { 11 | text, expect string 12 | } 13 | 14 | func TestDedentNoMargin(t *testing.T) { 15 | texts := []string{ 16 | // No lines indented 17 | "Hello there.\nHow are you?\nOh good, I'm glad.", 18 | // Similar with a blank line 19 | "Hello there.\n\nBoo!", 20 | // Some lines indented, but overall margin is still zero 21 | "Hello there.\n This is indented.", 22 | // Again, add a blank line. 23 | "Hello there.\n\n Boo!\n", 24 | } 25 | 26 | for _, text := range texts { 27 | if text != Dedent(text) { 28 | t.Errorf(errorMsg, text, Dedent(text)) 29 | } 30 | } 31 | } 32 | 33 | func TestDedentEven(t *testing.T) { 34 | texts := []dedentTest{ 35 | { 36 | // All lines indented by two spaces 37 | text: " Hello there.\n How are ya?\n Oh good.", 38 | expect: "Hello there.\nHow are ya?\nOh good.", 39 | }, 40 | { 41 | // Same, with blank lines 42 | text: " Hello there.\n\n How are ya?\n Oh good.\n", 43 | expect: "Hello there.\n\nHow are ya?\nOh good.\n", 44 | }, 45 | { 46 | // Now indent one of the blank lines 47 | text: " Hello there.\n \n How are ya?\n Oh good.\n", 48 | expect: "Hello there.\n\nHow are ya?\nOh good.\n", 49 | }, 50 | } 51 | 52 | for _, text := range texts { 53 | if text.expect != Dedent(text.text) { 54 | t.Errorf(errorMsg, text.expect, Dedent(text.text)) 55 | } 56 | } 57 | } 58 | 59 | func TestDedentUneven(t *testing.T) { 60 | texts := []dedentTest{ 61 | { 62 | // Lines indented unevenly 63 | text: ` 64 | def foo(): 65 | while 1: 66 | return foo 67 | `, 68 | expect: ` 69 | def foo(): 70 | while 1: 71 | return foo 72 | `, 73 | }, 74 | { 75 | // Uneven indentation with a blank line 76 | text: " Foo\n Bar\n\n Baz\n", 77 | expect: "Foo\n Bar\n\n Baz\n", 78 | }, 79 | { 80 | // Uneven indentation with a whitespace-only line 81 | text: " Foo\n Bar\n \n Baz\n", 82 | expect: "Foo\n Bar\n\n Baz\n", 83 | }, 84 | } 85 | 86 | for _, text := range texts { 87 | if text.expect != Dedent(text.text) { 88 | t.Errorf(errorMsg, text.expect, Dedent(text.text)) 89 | } 90 | } 91 | } 92 | 93 | // Dedent() should not mangle internal tabs. 94 | func TestDedentPreserveInternalTabs(t *testing.T) { 95 | text := " hello\tthere\n how are\tyou?" 96 | expect := "hello\tthere\nhow are\tyou?" 97 | if expect != Dedent(text) { 98 | t.Errorf(errorMsg, expect, Dedent(text)) 99 | } 100 | 101 | // Make sure that it preserves tabs when it's not making any changes at all 102 | if expect != Dedent(expect) { 103 | t.Errorf(errorMsg, expect, Dedent(expect)) 104 | } 105 | } 106 | 107 | // Dedent() should not mangle tabs in the margin (i.e. tabs and spaces both 108 | // count as margin, but are *not* considered equivalent). 109 | func TestDedentPreserveMarginTabs(t *testing.T) { 110 | texts := []string{ 111 | " hello there\n\thow are you?", 112 | // Same effect even if we have 8 spaces 113 | " hello there\n\thow are you?", 114 | } 115 | 116 | for _, text := range texts { 117 | d := Dedent(text) 118 | if text != d { 119 | t.Errorf(errorMsg, text, d) 120 | } 121 | } 122 | 123 | texts2 := []dedentTest{ 124 | { 125 | // Dedent() only removes whitespace that can be uniformly removed! 126 | text: "\thello there\n\thow are you?", 127 | expect: "hello there\nhow are you?", 128 | }, 129 | { 130 | text: " \thello there\n \thow are you?", 131 | expect: "hello there\nhow are you?", 132 | }, 133 | { 134 | text: " \t hello there\n \t how are you?", 135 | expect: "hello there\nhow are you?", 136 | }, 137 | { 138 | text: " \thello there\n \t how are you?", 139 | expect: "hello there\n how are you?", 140 | }, 141 | } 142 | 143 | for _, text := range texts2 { 144 | if text.expect != Dedent(text.text) { 145 | t.Errorf(errorMsg, text.expect, Dedent(text.text)) 146 | } 147 | } 148 | } 149 | 150 | func ExampleDedent() { 151 | s := ` 152 | Lorem ipsum dolor sit amet, 153 | consectetur adipiscing elit. 154 | Curabitur justo tellus, facilisis nec efficitur dictum, 155 | fermentum vitae ligula. Sed eu convallis sapien.` 156 | fmt.Println(Dedent(s)) 157 | fmt.Println("-------------") 158 | fmt.Println(s) 159 | // Output: 160 | // Lorem ipsum dolor sit amet, 161 | // consectetur adipiscing elit. 162 | // Curabitur justo tellus, facilisis nec efficitur dictum, 163 | // fermentum vitae ligula. Sed eu convallis sapien. 164 | // ------------- 165 | // 166 | // Lorem ipsum dolor sit amet, 167 | // consectetur adipiscing elit. 168 | // Curabitur justo tellus, facilisis nec efficitur dictum, 169 | // fermentum vitae ligula. Sed eu convallis sapien. 170 | } 171 | 172 | func BenchmarkDedent(b *testing.B) { 173 | for i := 0; i < b.N; i++ { 174 | Dedent(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. 175 | Curabitur justo tellus, facilisis nec efficitur dictum, 176 | fermentum vitae ligula. Sed eu convallis sapien.`) 177 | } 178 | } 179 | --------------------------------------------------------------------------------