width: implement Span interface

Change-Id: I73658f436dda909986b6a847c54945ec85a39744
Reviewed-on: https://go-review.googlesource.com/28133
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/width/transform.go b/width/transform.go
index 2ed2509..0049f70 100644
--- a/width/transform.go
+++ b/width/transform.go
@@ -14,6 +14,32 @@
 	transform.NopResetter
 }
 
+func (foldTransform) Span(src []byte, atEOF bool) (n int, err error) {
+	for n < len(src) {
+		if src[n] < utf8.RuneSelf {
+			// ASCII fast path.
+			for n++; n < len(src) && src[n] < utf8.RuneSelf; n++ {
+			}
+			continue
+		}
+		v, size := trie.lookup(src[n:])
+		if size == 0 { // incomplete UTF-8 encoding
+			if !atEOF {
+				err = transform.ErrShortSrc
+			} else {
+				n = len(src)
+			}
+			break
+		}
+		if elem(v)&tagNeedsFold != 0 {
+			err = transform.ErrEndOfSpan
+			break
+		}
+		n += size
+	}
+	return n, err
+}
+
 func (foldTransform) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
 	for nSrc < len(src) {
 		if src[nSrc] < utf8.RuneSelf {
@@ -70,6 +96,33 @@
 	transform.NopResetter
 }
 
+func (narrowTransform) Span(src []byte, atEOF bool) (n int, err error) {
+	for n < len(src) {
+		if src[n] < utf8.RuneSelf {
+			// ASCII fast path.
+			for n++; n < len(src) && src[n] < utf8.RuneSelf; n++ {
+			}
+			continue
+		}
+		v, size := trie.lookup(src[n:])
+		if size == 0 { // incomplete UTF-8 encoding
+			if !atEOF {
+				err = transform.ErrShortSrc
+			} else {
+				n = len(src)
+			}
+			break
+		}
+		if k := elem(v).kind(); byte(v) == 0 || k != EastAsianFullwidth && k != EastAsianWide && k != EastAsianAmbiguous {
+		} else {
+			err = transform.ErrEndOfSpan
+			break
+		}
+		n += size
+	}
+	return n, err
+}
+
 func (narrowTransform) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
 	for nSrc < len(src) {
 		if src[nSrc] < utf8.RuneSelf {
@@ -126,6 +179,30 @@
 	transform.NopResetter
 }
 
+func (wideTransform) Span(src []byte, atEOF bool) (n int, err error) {
+	for n < len(src) {
+		// TODO: Consider ASCII fast path. Special-casing ASCII handling can
+		// reduce the ns/op of BenchmarkWideASCII by about 30%. This is probably
+		// not enough to warrant the extra code and complexity.
+		v, size := trie.lookup(src[n:])
+		if size == 0 { // incomplete UTF-8 encoding
+			if !atEOF {
+				err = transform.ErrShortSrc
+			} else {
+				n = len(src)
+			}
+			break
+		}
+		if k := elem(v).kind(); byte(v) == 0 || k != EastAsianHalfwidth && k != EastAsianNarrow {
+		} else {
+			err = transform.ErrEndOfSpan
+			break
+		}
+		n += size
+	}
+	return n, err
+}
+
 func (wideTransform) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
 	for nSrc < len(src) {
 		// TODO: Consider ASCII fast path. Special-casing ASCII handling can
diff --git a/width/transform_test.go b/width/transform_test.go
index 13d3c01..f9122d6 100644
--- a/width/transform_test.go
+++ b/width/transform_test.go
@@ -52,163 +52,199 @@
 }
 
 type transformTest struct {
-	desc  string
-	src   string
-	nBuf  int
-	nDst  int
-	atEOF bool
-	dst   string
-	nSrc  int
-	err   error
+	desc    string
+	src     string
+	nBuf    int
+	nDst    int
+	atEOF   bool
+	dst     string
+	nSrc    int
+	err     error
+	nSpan   int
+	errSpan error
 }
 
 func (tc *transformTest) doTest(t *testing.T, tr Transformer) {
-	b := make([]byte, tc.nBuf)
-	nDst, nSrc, err := tr.Transform(b, []byte(tc.src), tc.atEOF)
-	if got := string(b[:nDst]); got != tc.dst[:nDst] {
-		t.Errorf("%s: dst was %+q; want %+q", tc.desc, got, tc.dst)
-	}
-	if nDst != tc.nDst {
-		t.Errorf("%s: nDst was %d; want %d", tc.desc, nDst, tc.nDst)
-	}
-	if nSrc != tc.nSrc {
-		t.Errorf("%s: nSrc was %d; want %d", tc.desc, nSrc, tc.nSrc)
-	}
-	if err != tc.err {
-		t.Errorf("%s: error was %v; want %v", tc.desc, err, tc.err)
-	}
-	if got := tr.String(tc.src); got != tc.dst {
-		t.Errorf("%s:String(%q) = %q; want %q", tc.desc, tc.src, got, tc.dst)
-	}
+	testtext.Run(t, tc.desc, func(t *testing.T) {
+		b := make([]byte, tc.nBuf)
+		nDst, nSrc, err := tr.Transform(b, []byte(tc.src), tc.atEOF)
+		if got := string(b[:nDst]); got != tc.dst[:nDst] {
+			t.Errorf("dst was %+q; want %+q", got, tc.dst)
+		}
+		if nDst != tc.nDst {
+			t.Errorf("nDst was %d; want %d", nDst, tc.nDst)
+		}
+		if nSrc != tc.nSrc {
+			t.Errorf("nSrc was %d; want %d", nSrc, tc.nSrc)
+		}
+		if err != tc.err {
+			t.Errorf("error was %v; want %v", err, tc.err)
+		}
+		if got := tr.String(tc.src); got != tc.dst {
+			t.Errorf("String(%q) = %q; want %q", tc.src, got, tc.dst)
+		}
+		n, err := tr.Span([]byte(tc.src), tc.atEOF)
+		if n != tc.nSpan || err != tc.errSpan {
+			t.Errorf("Span: got %d, %v; want %d, %v", n, err, tc.nSpan, tc.errSpan)
+		}
+	})
 }
 
 func TestFold(t *testing.T) {
 	for _, tc := range []transformTest{{
-		desc:  "empty",
-		src:   "",
-		nBuf:  10,
-		dst:   "",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: false,
-		err:   nil,
+		desc:    "empty",
+		src:     "",
+		nBuf:    10,
+		dst:     "",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: nil,
 	}, {
-		desc:  "short source 1",
-		src:   "a\xc2",
-		nBuf:  10,
-		dst:   "a\xc2",
-		nDst:  1,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortSrc,
+		desc:    "short source 1",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    1,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   1,
+		errSpan: transform.ErrShortSrc,
 	}, {
-		desc:  "short source 2",
-		src:   "a\xe0\x80",
-		nBuf:  10,
-		dst:   "a\xe0\x80",
-		nDst:  1,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortSrc,
+		desc:    "short source 2",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    1,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   1,
+		errSpan: transform.ErrShortSrc,
 	}, {
-		desc:  "incomplete but terminated source 1",
-		src:   "a\xc2",
-		nBuf:  10,
-		dst:   "a\xc2",
-		nDst:  2,
-		nSrc:  2,
-		atEOF: true,
-		err:   nil,
+		desc:    "incomplete but terminated source 1",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    2,
+		nSrc:    2,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   2,
+		errSpan: nil,
 	}, {
-		desc:  "incomplete but terminated source 2",
-		src:   "a\xe0\x80",
-		nBuf:  10,
-		dst:   "a\xe0\x80",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: true,
-		err:   nil,
+		desc:    "incomplete but terminated source 2",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   3,
+		errSpan: nil,
 	}, {
-		desc:  "exact fit dst",
-		src:   "a\uff01",
-		nBuf:  2,
-		dst:   "a!",
-		nDst:  2,
-		nSrc:  4,
-		atEOF: false,
-		err:   nil,
+		desc:    "exact fit dst",
+		src:     "a\uff01",
+		nBuf:    2,
+		dst:     "a!",
+		nDst:    2,
+		nSrc:    4,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   1,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "exact fit dst and src ascii",
-		src:   "ab",
-		nBuf:  2,
-		dst:   "ab",
-		nDst:  2,
-		nSrc:  2,
-		atEOF: true,
-		err:   nil,
+		desc:    "exact fit dst and src ascii",
+		src:     "ab",
+		nBuf:    2,
+		dst:     "ab",
+		nDst:    2,
+		nSrc:    2,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   2,
+		errSpan: nil,
 	}, {
-		desc:  "empty dst",
-		src:   "\u0300",
-		nBuf:  0,
-		dst:   "\u0300",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "empty dst",
+		src:     "\u0300",
+		nBuf:    0,
+		dst:     "\u0300",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   2,
+		errSpan: nil,
 	}, {
-		desc:  "empty dst ascii",
-		src:   "a",
-		nBuf:  0,
-		dst:   "a",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "empty dst ascii",
+		src:     "a",
+		nBuf:    0,
+		dst:     "a",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   1,
+		errSpan: nil,
 	}, {
-		desc:  "short dst 1",
-		src:   "a\uffe0", // ¢
-		nBuf:  2,
-		dst:   "a\u00a2", // ¢
-		nDst:  1,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortDst,
+		desc:    "short dst 1",
+		src:     "a\uffe0", // ¢
+		nBuf:    2,
+		dst:     "a\u00a2", // ¢
+		nDst:    1,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortDst,
+		nSpan:   1,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "short dst 2",
-		src:   "不夠",
-		nBuf:  3,
-		dst:   "不夠",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short dst 2",
+		src:     "不夠",
+		nBuf:    3,
+		dst:     "不夠",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   6,
+		errSpan: nil,
 	}, {
-		desc:  "short dst fast path",
-		src:   "fast",
-		nDst:  3,
-		dst:   "fast",
-		nBuf:  3,
-		nSrc:  3,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short dst fast path",
+		src:     "fast",
+		nDst:    3,
+		dst:     "fast",
+		nBuf:    3,
+		nSrc:    3,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   4,
+		errSpan: nil,
 	}, {
-		desc:  "short dst larger buffer",
-		src:   "\uff21" + strings.Repeat("0", 127) + "B",
-		nBuf:  128,
-		dst:   "A" + strings.Repeat("0", 127) + "B",
-		nDst:  128,
-		nSrc:  130,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short dst larger buffer",
+		src:     "\uff21" + strings.Repeat("0", 127) + "B",
+		nBuf:    128,
+		dst:     "A" + strings.Repeat("0", 127) + "B",
+		nDst:    128,
+		nSrc:    130,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "fast path alternation",
-		src:   "fast路徑fast路徑",
-		nBuf:  20,
-		dst:   "fast路徑fast路徑",
-		nDst:  20,
-		nSrc:  20,
-		atEOF: true,
-		err:   nil,
+		desc:    "fast path alternation",
+		src:     "fast路徑fast路徑",
+		nBuf:    20,
+		dst:     "fast路徑fast路徑",
+		nDst:    20,
+		nSrc:    20,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   20,
+		errSpan: nil,
 	}} {
 		tc.doTest(t, Fold)
 	}
@@ -230,113 +266,181 @@
 
 func TestWiden(t *testing.T) {
 	for _, tc := range []transformTest{{
-		desc:  "empty",
-		src:   "",
-		nBuf:  10,
-		dst:   "",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: false,
-		err:   nil,
+		desc:    "empty",
+		src:     "",
+		nBuf:    10,
+		dst:     "",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: nil,
 	}, {
-		desc:  "short source 1",
-		src:   "a\xc2",
-		nBuf:  10,
-		dst:   "a\xc2",
-		nDst:  3,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortSrc,
+		desc:    "short source 1",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    3,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "short source 2",
-		src:   "a\xe0\x80",
-		nBuf:  10,
-		dst:   "a\xe0\x80",
-		nDst:  3,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortSrc,
+		desc:    "short source 2",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    3,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "incomplete but terminated source 1",
-		src:   "a\xc2",
-		nBuf:  10,
-		dst:   "a\xc2",
-		nDst:  4,
-		nSrc:  2,
-		atEOF: true,
-		err:   nil,
+		desc:    "incomplete but terminated source 1",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    4,
+		nSrc:    2,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "incomplete but terminated source 2",
-		src:   "a\xe0\x80",
-		nBuf:  10,
-		dst:   "a\xe0\x80",
-		nDst:  5,
-		nSrc:  3,
-		atEOF: true,
-		err:   nil,
+		desc:    "incomplete but terminated source 2",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    5,
+		nSrc:    3,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "exact fit dst",
-		src:   "a!",
-		nBuf:  6,
-		dst:   "a\uff01",
-		nDst:  6,
-		nSrc:  2,
-		atEOF: false,
-		err:   nil,
+		desc:    "short source 1 some span",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   3,
+		errSpan: transform.ErrShortSrc,
 	}, {
-		desc:  "empty dst",
-		src:   "\u0300",
-		nBuf:  0,
-		dst:   "\u0300",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short source 2 some span",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   3,
+		errSpan: transform.ErrShortSrc,
 	}, {
-		desc:  "empty dst ascii",
-		src:   "a",
-		nBuf:  0,
-		dst:   "a",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "incomplete but terminated source 1 some span",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    4,
+		nSrc:    4,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   4,
+		errSpan: nil,
 	}, {
-		desc:  "short dst 1",
-		src:   "a\uffe0",
-		nBuf:  4,
-		dst:   "a\uffe0",
-		nDst:  3,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortDst,
+		desc:    "incomplete but terminated source 2 some span",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    5,
+		nSrc:    5,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   5,
+		errSpan: nil,
 	}, {
-		desc:  "short dst 2",
-		src:   "不夠",
-		nBuf:  3,
-		dst:   "不夠",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "exact fit dst",
+		src:     "a!",
+		nBuf:    6,
+		dst:     "a\uff01",
+		nDst:    6,
+		nSrc:    2,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "short dst ascii",
-		src:   "ascii",
-		nBuf:  3,
-		dst:   "ascii", // U+ff41, ...
-		nDst:  3,
-		nSrc:  1,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "empty dst",
+		src:     "\u0300",
+		nBuf:    0,
+		dst:     "\u0300",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   2,
+		errSpan: nil,
 	}, {
-		desc:  "ambiguous",
-		src:   "\uffe9",
-		nBuf:  4,
-		dst:   "\u2190",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: false,
-		err:   nil,
+		desc:    "empty dst ascii",
+		src:     "a",
+		nBuf:    0,
+		dst:     "a",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
+	}, {
+		desc:    "short dst 1",
+		src:     "a\uffe0",
+		nBuf:    4,
+		dst:     "a\uffe0",
+		nDst:    3,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortDst,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
+	}, {
+		desc:    "short dst 2",
+		src:     "不夠",
+		nBuf:    3,
+		dst:     "不夠",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   6,
+		errSpan: nil,
+	}, {
+		desc:    "short dst ascii",
+		src:     "ascii",
+		nBuf:    3,
+		dst:     "ascii", // U+ff41, ...
+		nDst:    3,
+		nSrc:    1,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
+	}, {
+		desc:    "ambiguous",
+		src:     "\uffe9",
+		nBuf:    4,
+		dst:     "\u2190",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}} {
 		tc.doTest(t, Widen)
 	}
@@ -358,141 +462,171 @@
 
 func TestNarrow(t *testing.T) {
 	for _, tc := range []transformTest{{
-		desc:  "empty",
-		src:   "",
-		nBuf:  10,
-		dst:   "",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: false,
-		err:   nil,
+		desc:    "empty",
+		src:     "",
+		nBuf:    10,
+		dst:     "",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: nil,
 	}, {
-		desc:  "short source 1",
-		src:   "a\xc2",
-		nBuf:  10,
-		dst:   "a\xc2",
-		nDst:  1,
-		nSrc:  1,
-		atEOF: false,
-		err:   transform.ErrShortSrc,
+		desc:    "short source 1",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    1,
+		nSrc:    1,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   1,
+		errSpan: transform.ErrShortSrc,
 	}, {
-		desc:  "short source 2",
-		src:   "a\xe0\x80",
-		nBuf:  10,
-		dst:   "a\xe0\x80",
-		nDst:  1,
-		nSrc:  3,
-		atEOF: false,
-		err:   transform.ErrShortSrc,
+		desc:    "short source 2",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    1,
+		nSrc:    3,
+		atEOF:   false,
+		err:     transform.ErrShortSrc,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "incomplete but terminated source 1",
-		src:   "a\xc2",
-		nBuf:  10,
-		dst:   "a\xc2",
-		nDst:  2,
-		nSrc:  4,
-		atEOF: true,
-		err:   nil,
+		desc:    "incomplete but terminated source 1",
+		src:     "a\xc2",
+		nBuf:    10,
+		dst:     "a\xc2",
+		nDst:    2,
+		nSrc:    4,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "incomplete but terminated source 2",
-		src:   "a\xe0\x80",
-		nBuf:  10,
-		dst:   "a\xe0\x80",
-		nDst:  3,
-		nSrc:  5,
-		atEOF: true,
-		err:   nil,
+		desc:    "incomplete but terminated source 2",
+		src:     "a\xe0\x80",
+		nBuf:    10,
+		dst:     "a\xe0\x80",
+		nDst:    3,
+		nSrc:    5,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "exact fit dst",
-		src:   "a\uff01",
-		nBuf:  2,
-		dst:   "a!",
-		nDst:  2,
-		nSrc:  6,
-		atEOF: false,
-		err:   nil,
+		desc:    "exact fit dst",
+		src:     "a\uff01",
+		nBuf:    2,
+		dst:     "a!",
+		nDst:    2,
+		nSrc:    6,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "exact fit dst",
-		src:   "a\uff01",
-		nBuf:  2,
-		dst:   "a!",
-		nDst:  2,
-		nSrc:  4,
-		atEOF: false,
-		err:   nil,
+		desc:    "exact fit dst some span",
+		src:     "a\uff01",
+		nBuf:    2,
+		dst:     "a!",
+		nDst:    2,
+		nSrc:    4,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   1,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "empty dst",
-		src:   "\u0300",
-		nBuf:  0,
-		dst:   "\u0300",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "empty dst",
+		src:     "\u0300",
+		nBuf:    0,
+		dst:     "\u0300",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   2,
+		errSpan: nil,
 	}, {
-		desc:  "empty dst ascii",
-		src:   "a",
-		nBuf:  0,
-		dst:   "a",
-		nDst:  0,
-		nSrc:  0,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "empty dst ascii",
+		src:     "a",
+		nBuf:    0,
+		dst:     "a",
+		nDst:    0,
+		nSrc:    0,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   1,
+		errSpan: nil,
 	}, {
-		desc:  "short dst 1",
-		src:   "a\uffe0", // ¢
-		nBuf:  2,
-		dst:   "a\u00a2", // ¢
-		nDst:  1,
-		nSrc:  3,
-		atEOF: false,
-		err:   transform.ErrShortDst,
+		desc:    "short dst 1",
+		src:     "a\uffe0", // ¢
+		nBuf:    2,
+		dst:     "a\u00a2", // ¢
+		nDst:    1,
+		nSrc:    3,
+		atEOF:   false,
+		err:     transform.ErrShortDst,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "short dst 2",
-		src:   "不夠",
-		nBuf:  3,
-		dst:   "不夠",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short dst 2",
+		src:     "不夠",
+		nBuf:    3,
+		dst:     "不夠",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   6,
+		errSpan: nil,
 	}, {
 		// Create a narrow variant of ambiguous runes, if they exist.
-		desc:  "ambiguous",
-		src:   "\u2190",
-		nBuf:  4,
-		dst:   "\uffe9",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: false,
-		err:   nil,
+		desc:    "ambiguous",
+		src:     "\u2190",
+		nBuf:    4,
+		dst:     "\uffe9",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   false,
+		err:     nil,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "short dst fast path",
-		src:   "fast",
-		nBuf:  3,
-		dst:   "fast",
-		nDst:  3,
-		nSrc:  3,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short dst fast path",
+		src:     "fast",
+		nBuf:    3,
+		dst:     "fast",
+		nDst:    3,
+		nSrc:    3,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   4,
+		errSpan: nil,
 	}, {
-		desc:  "short dst larger buffer",
-		src:   "\uff21" + strings.Repeat("0", 127) + "B",
-		nBuf:  128,
-		dst:   "A" + strings.Repeat("0", 127) + "B",
-		nDst:  128,
-		nSrc:  130,
-		atEOF: true,
-		err:   transform.ErrShortDst,
+		desc:    "short dst larger buffer",
+		src:     "\uff21" + strings.Repeat("0", 127) + "B",
+		nBuf:    128,
+		dst:     "A" + strings.Repeat("0", 127) + "B",
+		nDst:    128,
+		nSrc:    130,
+		atEOF:   true,
+		err:     transform.ErrShortDst,
+		nSpan:   0,
+		errSpan: transform.ErrEndOfSpan,
 	}, {
-		desc:  "fast path alternation",
-		src:   "fast路徑fast路徑",
-		nBuf:  20,
-		dst:   "fast路徑fast路徑",
-		nDst:  20,
-		nSrc:  20,
-		atEOF: true,
-		err:   nil,
+		desc:    "fast path alternation",
+		src:     "fast路徑fast路徑",
+		nBuf:    20,
+		dst:     "fast路徑fast路徑",
+		nDst:    20,
+		nSrc:    20,
+		atEOF:   true,
+		err:     nil,
+		nSpan:   20,
+		errSpan: nil,
 	}} {
 		tc.doTest(t, Narrow)
 	}
diff --git a/width/width.go b/width/width.go
index dc026ee..f1639ca 100644
--- a/width/width.go
+++ b/width/width.go
@@ -153,17 +153,22 @@
 
 // Transformer implements the transform.Transformer interface.
 type Transformer struct {
-	t transform.Transformer
+	t transform.SpanningTransformer
 }
 
 // Reset implements the transform.Transformer interface.
 func (t Transformer) Reset() { t.t.Reset() }
 
-// Transform implements the Transformer interface.
+// Transform implements the transform.Transformer interface.
 func (t Transformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
 	return t.t.Transform(dst, src, atEOF)
 }
 
+// Span implements the transform.SpanningTransformer interface.
+func (t Transformer) Span(src []byte, atEOF bool) (n int, err error) {
+	return t.t.Span(src, atEOF)
+}
+
 // Bytes returns a new byte slice with the result of applying t to b.
 func (t Transformer) Bytes(b []byte) []byte {
 	b, _, _ = transform.Bytes(t, b)