slog: implement scopes and groups for TextHandler

This initial implementation is slow and allocates too much.
Future CLs will optimize it.

Change-Id: Ic06f7e4c0d51ea12f320675b3a1b770122d0a786
Reviewed-on: https://go-review.googlesource.com/c/exp/+/438998
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/handler.go b/slog/handler.go
index ffd746c..06131a4 100644
--- a/slog/handler.go
+++ b/slog/handler.go
@@ -179,9 +179,11 @@
 	for _, a := range as {
 		state.appendAttr(a)
 	}
-	// Remember how many open scopes are in preformattedAttrs,
-	// so we don't open them again when we handle a Record.
-	h2.nOpenScopes = len(h2.scopes)
+	if h2.json {
+		// Remember how many open scopes are in preformattedAttrs,
+		// so we don't open them again when we handle a Record.
+		h2.nOpenScopes = len(h2.scopes)
+	}
 	return h2
 }
 
@@ -281,9 +283,10 @@
 // The initial value of sep determines whether to emit a separator
 // before the next key, after which it stays true.
 type handleState struct {
-	h   *commonHandler
-	buf *buffer.Buffer
-	sep string // separator to write before next key
+	h      *commonHandler
+	buf    *buffer.Buffer
+	sep    string // separator to write before next key
+	prefix string // for text: key prefix
 }
 
 func (s *handleState) openScopes() {
@@ -307,7 +310,8 @@
 		s.buf.WriteByte('{')
 		s.sep = ""
 	} else {
-		panic("unimplemented")
+		// TODO: fix escaping to make it easy to recover the original.
+		s.prefix += escapeDots(name) + "."
 	}
 }
 
@@ -316,7 +320,7 @@
 	if s.h.json {
 		s.buf.WriteByte('}')
 	} else {
-		panic("unimplemented -- but it will use name, I assure you")
+		s.prefix = s.prefix[:len(s.prefix)-len(name)-1]
 	}
 	s.sep = s.h.attrSep()
 }
@@ -352,6 +356,8 @@
 
 func (s *handleState) appendKey(key string) {
 	s.buf.WriteString(s.sep)
+	// TODO: make sure the entire prefix+key is quoted if any part of it needs to be.
+	s.buf.WriteString(s.prefix)
 	s.appendString(key)
 	if s.h.json {
 		s.buf.WriteByte(':')
diff --git a/slog/handler_test.go b/slog/handler_test.go
index d4c7a43..5a0ada7 100644
--- a/slog/handler_test.go
+++ b/slog/handler_test.go
@@ -160,25 +160,25 @@
 					Int("d", 4)),
 				Int("e", 5),
 			},
-			wantText: "SKIP",
+			wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
 			wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
 		},
 		{
 			name:     "empty group",
 			replace:  removeKeys(timeKey, levelKey),
 			attrs:    []Attr{Group("g"), Group("h", Int("a", 1))},
-			wantText: "SKIP",
+			wantText: "msg=message h.a=1",
 			wantJSON: `{"msg":"message","g":{},"h":{"a":1}}`,
 		},
 		{
-			name:    "Marshaler",
+			name:    "LogValuer",
 			replace: removeKeys(timeKey, levelKey),
 			attrs: []Attr{
 				Int("a", 1),
-				Any("name", marshalName{"Ren", "Hoek"}),
+				Any("name", logValueName{"Ren", "Hoek"}),
 				Int("b", 2),
 			},
-			wantText: "SKIP",
+			wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
 			wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
 		},
 		{
@@ -186,7 +186,7 @@
 			replace:  removeKeys(timeKey, levelKey),
 			with:     func(h Handler) Handler { return h.With(preAttrs).WithScope("s") },
 			attrs:    attrs,
-			wantText: "SKIP",
+			wantText: "msg=message pre=3 x=y s.a=one s.b=2",
 			wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
 		},
 		{
@@ -199,7 +199,7 @@
 					WithScope("s2")
 			},
 			attrs:    attrs,
-			wantText: "SKIP",
+			wantText: "msg=message p1=1 s1.p2=2 s1.s2.a=one s1.s2.b=2",
 			wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"a":"one","b":2}}}`,
 		},
 		{
@@ -211,7 +211,7 @@
 					WithScope("s2")
 			},
 			attrs:    attrs,
-			wantText: "SKIP",
+			wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
 			wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
 		},
 	} {
@@ -229,9 +229,6 @@
 				{"json", opts.NewJSONHandler(&buf), test.wantJSON},
 			} {
 				t.Run(handler.name, func(t *testing.T) {
-					if handler.want == "SKIP" {
-						t.Skip("feature unimplemented")
-					}
 					h := handler.h
 					if test.with != nil {
 						h = test.with(h)
@@ -255,11 +252,11 @@
 	return a
 }
 
-type marshalName struct {
+type logValueName struct {
 	first, last string
 }
 
-func (n marshalName) LogValue() Value {
+func (n logValueName) LogValue() Value {
 	return GroupValue(
 		String("first", n.first),
 		String("last", n.last))
diff --git a/slog/text_handler.go b/slog/text_handler.go
index b5daab7..2268f76 100644
--- a/slog/text_handler.go
+++ b/slog/text_handler.go
@@ -8,6 +8,7 @@
 	"encoding"
 	"fmt"
 	"io"
+	"strings"
 	"unicode"
 	"unicode/utf8"
 )
@@ -99,6 +100,13 @@
 	return nil
 }
 
+// escapeDots replaces all '.' runes in s with the equivalent hex escape code.
+// This allows the scope/group structure to be retrieved from the dot-separated
+// components of a key.
+func escapeDots(s string) string {
+	return strings.ReplaceAll(s, ".", `\x2E`)
+}
+
 func needsQuoting(s string) bool {
 	for i := 0; i < len(s); {
 		b := s[i]