internal/godoc: introduce TabSpacer

Mainly just isolate it from Presentation, but give it a nice API too.

Change-Id: I9650bf650a15bfd814fe4da58de6fa133a858d90
Reviewed-on: https://go-review.googlesource.com/c/website/+/296374
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/internal/godoc/godoc.go b/internal/godoc/godoc.go
index 25b3063..47c2b0f 100644
--- a/internal/godoc/godoc.go
+++ b/internal/godoc/godoc.go
@@ -605,7 +605,7 @@
 	}
 
 	mode := printer.TabIndent | printer.UseSpaces
-	err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(&tconv{p: p, output: out}, fset, x)
+	err := (&printer.Config{Mode: mode, Tabwidth: p.TabWidth}).Fprint(TabSpacer(out, p.TabWidth), fset, x)
 	if err != nil {
 		log.Print(err)
 	}
diff --git a/internal/godoc/tab.go b/internal/godoc/tab.go
index 944074e..17fe293 100644
--- a/internal/godoc/tab.go
+++ b/internal/godoc/tab.go
@@ -5,8 +5,6 @@
 //go:build go1.16
 // +build go1.16
 
-// TODO(bradfitz,adg): move to util
-
 package godoc
 
 import "io"
@@ -18,68 +16,74 @@
 	collecting
 )
 
-// A tconv is an io.Writer filter for converting leading tabs into spaces.
-type tconv struct {
-	output io.Writer
-	state  int // indenting or collecting
-	indent int // valid if state == indenting
-	p      *Presentation
+// TabSpacer returns a writer that passes writes through to w,
+// expanding tabs to one or more spaces ending at a width-spaces-aligned boundary.
+func TabSpacer(w io.Writer, width int) io.Writer {
+	return &tconv{output: w, tabWidth: width}
 }
 
-func (p *tconv) writeIndent() (err error) {
-	i := p.indent
+// A tconv is an io.Writer filter for converting leading tabs into spaces.
+type tconv struct {
+	output   io.Writer
+	state    int // indenting or collecting
+	indent   int // valid if state == indenting
+	tabWidth int
+}
+
+func (t *tconv) writeIndent() (err error) {
+	i := t.indent
 	for i >= len(spaces) {
 		i -= len(spaces)
-		if _, err = p.output.Write(spaces); err != nil {
+		if _, err = t.output.Write(spaces); err != nil {
 			return
 		}
 	}
 	// i < len(spaces)
 	if i > 0 {
-		_, err = p.output.Write(spaces[0:i])
+		_, err = t.output.Write(spaces[0:i])
 	}
 	return
 }
 
-func (p *tconv) Write(data []byte) (n int, err error) {
+func (t *tconv) Write(data []byte) (n int, err error) {
 	if len(data) == 0 {
 		return
 	}
 	pos := 0 // valid if p.state == collecting
 	var b byte
 	for n, b = range data {
-		switch p.state {
+		switch t.state {
 		case indenting:
 			switch b {
 			case '\t':
-				p.indent += p.p.TabWidth
+				t.indent += t.tabWidth
 			case '\n':
-				p.indent = 0
-				if _, err = p.output.Write(data[n : n+1]); err != nil {
+				t.indent = 0
+				if _, err = t.output.Write(data[n : n+1]); err != nil {
 					return
 				}
 			case ' ':
-				p.indent++
+				t.indent++
 			default:
-				p.state = collecting
+				t.state = collecting
 				pos = n
-				if err = p.writeIndent(); err != nil {
+				if err = t.writeIndent(); err != nil {
 					return
 				}
 			}
 		case collecting:
 			if b == '\n' {
-				p.state = indenting
-				p.indent = 0
-				if _, err = p.output.Write(data[pos : n+1]); err != nil {
+				t.state = indenting
+				t.indent = 0
+				if _, err = t.output.Write(data[pos : n+1]); err != nil {
 					return
 				}
 			}
 		}
 	}
 	n = len(data)
-	if pos < n && p.state == collecting {
-		_, err = p.output.Write(data[pos:])
+	if pos < n && t.state == collecting {
+		_, err = t.output.Write(data[pos:])
 	}
 	return
 }