v0.1.0 3b1b6ef

marshal.go RawHistoryFolder

1package kvconf
2
3import (
4	"encoding"
5	"errors"
6	"fmt"
7	"maps"
8	"reflect"
9	"slices"
10	"strconv"
11	"strings"
12	"time"
13	"unsafe"
14)
15
16// Quote returns v as it must be written so Parse reads it back unchanged:
17// as is when possible, else as a Go "..." string.
18func Quote(v string) string {
19	if needsQuote(v) {
20		return strconv.Quote(v)
21	}
22	return v
23}
24
25func needsQuote(v string) bool {
26	if v == "" || v != strings.TrimSpace(v) {
27		return true
28	}
29	if v[0] == '"' || v[0] == '\'' {
30		return true // it would be unquoted on reading
31	}
32	// A '#' after a space ends the value unless it is inside a quoted word,
33	// so quote any value where Parse would cut it.
34	return strings.ContainsAny(v, "\n\r") || stripComment(v, CommentSpaced) != v
35}
36
37// validKey reports whether key can be written and read back as one key.
38func validKey(key string) bool {
39	return key != "" && key[0] != '#' && key == strings.TrimSpace(key) &&
40		!strings.ContainsAny(key, "=\n\r")
41}
42
43func appendQuoted(b []byte, v string) []byte {
44	if needsQuote(v) {
45		return strconv.AppendQuote(b, v)
46	}
47	return append(b, v...)
48}
49
50// Marshal writes the tagged fields of v (a struct or a pointer to one) as
51// `key = value` lines in declaration order; map entries are sorted by name.
52func Marshal(v any) ([]byte, error) {
53	rv := reflect.Indirect(reflect.ValueOf(v))
54	if rv.Kind() != reflect.Struct {
55		return nil, errors.New("kvconf: Marshal needs a struct")
56	}
57	s, err := schemaOf(rv.Type())
58	if err != nil {
59		return nil, err
60	}
61	m := marshaler{b: make([]byte, 0, 32*len(s.order))}
62	for _, f := range s.order {
63		fv := rv.FieldByIndex(f.index)
64		if f.elem == nil && f.val == nil {
65			if err := m.line(f.key, "", "", fv, f); err != nil {
66				return nil, err
67			}
68			continue
69		}
70		// Copy the map out once, into reused values, then sort by name.
71		n := fv.Len()
72		names := make([]string, 0, n)
73		vals := reflect.MakeSlice(reflect.SliceOf(f.typ.Elem()), n, n)
74		key := reflect.New(f.typ.Key()).Elem()
75		for it, i := fv.MapRange(), 0; it.Next(); i++ {
76			key.SetIterKey(it)
77			names = append(names, key.String())
78			vals.Index(i).SetIterValue(it)
79		}
80		order := make([]int, n)
81		for i := range order {
82			order[i] = i
83		}
84		slices.SortFunc(order, func(a, b int) int { return strings.Compare(names[a], names[b]) })
85		for _, i := range order {
86			name, ev := names[i], vals.Index(i)
87			if f.elem == nil {
88				if err := m.line(f.key, name, "", ev, f.val); err != nil {
89					return nil, err
90				}
91				continue
92			}
93			for _, sub := range f.elem.order {
94				if err := m.line(f.key, name, sub.key, ev.FieldByIndex(sub.index), sub); err != nil {
95					return nil, err
96				}
97			}
98		}
99	}
100	if s.catchAll != nil {
101		fv := rv.FieldByIndex(s.catchAll.index)
102		if es, ok := fv.Interface().([]Entry); ok {
103			for _, e := range es {
104				if err := m.raw(e.Key, e.Value); err != nil {
105					return nil, err
106				}
107			}
108		} else {
109			kv := fv.Interface().(map[string]string)
110			for _, k := range slices.Sorted(maps.Keys(kv)) {
111				if err := m.raw(k, kv[k]); err != nil {
112					return nil, err
113				}
114			}
115		}
116	}
117	return m.b, nil
118}
119
120type marshaler struct {
121	b   []byte
122	tmp []byte // the value being formatted, reused
123}
124
125func (m *marshaler) raw(key, value string) error {
126	if !validKey(key) {
127		return fmt.Errorf("kvconf: invalid key %q", key)
128	}
129	m.b = append(m.b, key...)
130	m.b = append(m.b, " = "...)
131	m.b = appendQuoted(m.b, value)
132	m.b = append(m.b, '\n')
133	return nil
134}
135
136// line writes prefix+name[.sub] = value.
137func (m *marshaler) line(prefix, name, sub string, v reflect.Value, f *field) error {
138	var err error
139	if name != "" && (strings.ContainsAny(name, "=\n\r") || name != strings.TrimSpace(name)) {
140		return fmt.Errorf("kvconf: %s: invalid map name %q", prefix, name)
141	}
142	if m.tmp, err = appendValue(m.tmp[:0], v, f); err != nil {
143		key := prefix + name
144		if sub != "" {
145			key += "." + sub
146		}
147		return fmt.Errorf("kvconf: %s: %w", key, err)
148	}
149	m.b = append(m.b, prefix...)
150	m.b = append(m.b, name...)
151	if sub != "" {
152		m.b = append(m.b, '.')
153		m.b = append(m.b, sub...)
154	}
155	m.b = append(m.b, " = "...)
156	// tmp is only read here, before its next reuse.
157	m.b = appendQuoted(m.b, unsafe.String(unsafe.SliceData(m.tmp), len(m.tmp)))
158	m.b = append(m.b, '\n')
159	return nil
160}
161
162func appendValue(b []byte, v reflect.Value, f *field) ([]byte, error) {
163	if f.off && v.IsZero() {
164		return append(b, "off"...), nil
165	}
166	if f.marshal {
167		if !v.CanAddr() { // a map element: copy it to call a pointer method
168			p := reflect.New(v.Type())
169			p.Elem().Set(v)
170			v = p.Elem()
171		}
172		var tm encoding.TextMarshaler
173		if v.Type().Implements(marshalType) {
174			tm = v.Interface().(encoding.TextMarshaler)
175		} else {
176			tm = v.Addr().Interface().(encoding.TextMarshaler)
177		}
178		text, err := tm.MarshalText()
179		return append(b, text...), err
180	}
181	switch v.Kind() {
182	case reflect.String:
183		return append(b, v.String()...), nil
184	case reflect.Bool:
185		if v.Bool() {
186			return append(b, "on"...), nil
187		}
188		return append(b, "off"...), nil
189	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
190		if f.duration {
191			return append(b, time.Duration(v.Int()).String()...), nil
192		}
193		return strconv.AppendInt(b, v.Int(), 10), nil
194	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
195		return strconv.AppendUint(b, v.Uint(), 10), nil
196	case reflect.Float32, reflect.Float64:
197		return strconv.AppendFloat(b, v.Float(), 'f', -1, v.Type().Bits()), nil
198	case reflect.Slice:
199		for i := range v.Len() {
200			if i > 0 {
201				b = append(b, f.sep...)
202			}
203			var err error
204			if b, err = appendValue(b, v.Index(i), f.item); err != nil {
205				return b, err
206			}
207		}
208		return b, nil
209	}
210	return b, fmt.Errorf("unsupported type %s", v.Type())
211}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close