Skip to content

Commit

Permalink
Behave more like the Python implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Renström committed Aug 3, 2015
1 parent a1eba44 commit 48ba5c3
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 97 deletions.
47 changes: 20 additions & 27 deletions dedent.go
Expand Up @@ -5,45 +5,35 @@ import (
"strings"
)

var whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$")
var leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)")
var (
whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$")
leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])")
)

// Dedent removes any common leading whitespace from every line in s.
// Dedent removes any common leading whitespace from every line in text.
//
// This can be used to make multiline strings to line up with the left edge of
// the display, while still presenting them in the source code in indented
// form.
func Dedent(s string) string {
s = whitespaceOnly.ReplaceAllString(s, "")
margin := findMargin(s)
if len(margin) == 0 {
return s
}
return regexp.MustCompile("(?m)^"+margin).ReplaceAllString(s, "")
}

// Look for the longest leading string of spaces and tabs common to all lines.
func findMargin(s string) string {
func Dedent(text string) string {
var margin string

indents := leadingWhitespace.FindAllString(s, -1)
numIndents := len(indents)
for i, indent := range indents {
// Don't use last row if it is empty
if i == numIndents-1 && indent == "" {
break
}
text = whitespaceOnly.ReplaceAllString(text, "")
indents := leadingWhitespace.FindAllStringSubmatch(text, -1)

if margin == "" {
margin = indent
} else if strings.HasPrefix(indent, margin) {
// Look for the longest leading string of spaces and tabs common to all
// lines.
for i, indent := range indents {
if i == 0 {
margin = indent[1]
} else if strings.HasPrefix(indent[1], margin) {
// Current line more deeply indented than previous winner:
// no change (previous winner is still on top).
continue
} else if strings.HasPrefix(margin, indent) {
} else if strings.HasPrefix(margin, indent[1]) {
// Current line consistent with and no deeper than previous winner:
// it's the new winner.
margin = indent
margin = indent[1]
} else {
// Current line and previous winner have no common whitespace:
// there is no margin.
Expand All @@ -52,5 +42,8 @@ func findMargin(s string) string {
}
}

return margin
if margin != "" {
text = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(text, "")
}
return text
}
199 changes: 129 additions & 70 deletions dedent_test.go
Expand Up @@ -5,84 +5,151 @@ import (
"testing"
)

func TestFindMargin(t *testing.T) {
m := findMargin("Foo\n\t\t\tBar\n\t\tBaz")
if m != "\t\t" {
t.Errorf("Expected margin to be '\t\t'; got '%s'", m)
const errorMsg = "\nexpected %q\ngot %q"

type dedentTest struct {
text, expect string
}

func TestDedentNoMargin(t *testing.T) {
texts := []string{
// No lines indented
"Hello there.\nHow are you?\nOh good, I'm glad.",
// Similar with a blank line
"Hello there.\n\nBoo!",
// Some lines indented, but overall margin is still zero
"Hello there.\n This is indented.",
// Again, add a blank line.
"Hello there.\n\n Boo!\n",
}

for _, text := range texts {
if text != Dedent(text) {
t.Errorf(errorMsg, text, Dedent(text))
}
}
}

func TestFindMarginMultiline(t *testing.T) {
m := findMargin(`Foo
Bar
Baz
`)
if m != "\t\t" {
t.Errorf("Expected margin to be '\t\t'; got '%s'", m)
func TestDedentEven(t *testing.T) {
texts := []dedentTest{
{
// All lines indented by two spaces
text: " Hello there.\n How are ya?\n Oh good.",
expect: "Hello there.\nHow are ya?\nOh good.",
},
{
// Same, with blank lines
text: " Hello there.\n\n How are ya?\n Oh good.\n",
expect: "Hello there.\n\nHow are ya?\nOh good.\n",
},
{
// Now indent one of the blank lines
text: " Hello there.\n \n How are ya?\n Oh good.\n",
expect: "Hello there.\n\nHow are ya?\nOh good.\n",
},
}

for _, text := range texts {
if text.expect != Dedent(text.text) {
t.Errorf(errorMsg, text.expect, Dedent(text.text))
}
}
}

func TestDedentMultilineString(t *testing.T) {
s1 := `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur justo tellus, facilisis nec efficitur dictum,
fermentum vitae ligula. Sed eu convallis sapien.
`
s2 := `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur justo tellus, facilisis nec efficitur dictum,
fermentum vitae ligula. Sed eu convallis sapien.
`

if Dedent(s1) != s2 {
t.Errorf("expected string '%s'; got '%s'", s2, Dedent(s1))
func TestDedentUneven(t *testing.T) {
texts := []dedentTest{
{
// Lines indented unevenly
text: `
def foo():
while 1:
return foo
`,
expect: `
def foo():
while 1:
return foo
`,
},
{
// Uneven indentation with a blank line
text: " Foo\n Bar\n\n Baz\n",
expect: "Foo\n Bar\n\n Baz\n",
},
{
// Uneven indentation with a whitespace-only line
text: " Foo\n Bar\n \n Baz\n",
expect: "Foo\n Bar\n\n Baz\n",
},
}

for _, text := range texts {
if text.expect != Dedent(text.text) {
t.Errorf(errorMsg, text.expect, Dedent(text.text))
}
}
}

// Dedent() should not mangle internal tabs.
func TestDedentPreserveInternalTabs(t *testing.T) {
text := " hello\tthere\n how are\tyou?"
expect := "hello\tthere\nhow are\tyou?"
if expect != Dedent(text) {
t.Errorf(errorMsg, expect, Dedent(text))
}

// Make sure that it preserves tabs when it's not making any changes at all
if expect != Dedent(expect) {
t.Errorf(errorMsg, expect, Dedent(expect))
}
}

func TestDedentJSONString(t *testing.T) {
s1 := `
// Dedent() should not mangle tabs in the margin (i.e. tabs and spaces both
// count as margin, but are *not* considered equivalent).
func TestDedentPreserveMarginTabs(t *testing.T) {
texts := []string{
" hello there\n\thow are you?",
// Same effect even if we have 8 spaces
" hello there\n\thow are you?",
}

for _, text := range texts {
d := Dedent(text)
if text != d {
t.Errorf(errorMsg, text, d)
}
}

texts2 := []dedentTest{
{
// Dedent() only removes whitespace that can be uniformly removed!
text: "\thello there\n\thow are you?",
expect: "hello there\nhow are you?",
},
{
"key1": [
"l1",
"l2"
],
"key2": "value"
}`

s2 := `
{
"key1": [
"l1",
"l2"
],
"key2": "value"
}`

if Dedent(s1) != s2 {
t.Errorf("expected string '%s'; got '%s'", s2, Dedent(s1))
text: " \thello there\n \thow are you?",
expect: "hello there\nhow are you?",
},
{
text: " \t hello there\n \t how are you?",
expect: "hello there\nhow are you?",
},
{
text: " \thello there\n \t how are you?",
expect: "hello there\n how are you?",
},
}

s3 := `{
"key1": [
"l1",
"l2"
],
"key2": "value"
}`

s4 := `{
"key1": [
"l1",
"l2"
],
"key2": "value"
}`

if Dedent(s3) != s4 {
t.Errorf("expected string '%s'; got '%s'", s2, Dedent(s1))
for _, text := range texts2 {
if text.expect != Dedent(text.text) {
t.Errorf(errorMsg, text.expect, Dedent(text.text))
}
}
}

func ExampleDedent() {
fmt.Println(Dedent(`Lorem ipsum dolor sit amet,
fmt.Println(Dedent(`
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Curabitur justo tellus, facilisis nec efficitur dictum,
fermentum vitae ligula. Sed eu convallis sapien.`))
Expand All @@ -100,11 +167,3 @@ func BenchmarkDedent(b *testing.B) {
fermentum vitae ligula. Sed eu convallis sapien.`)
}
}

func BenchmarkFindMargin(b *testing.B) {
for i := 0; i < b.N; i++ {
findMargin(`Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur justo tellus, facilisis nec efficitur dictum,
fermentum vitae ligula. Sed eu convallis sapien.`)
}
}

0 comments on commit 48ba5c3

Please sign in to comment.