1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
// SPDX-License-Identifier: GPL-3.0-or-later
package config
import (
"fmt"
"strings"
"krino/internal/sexp"
)
// PrintRule renders a rule the way krino.conf(5) and the examples write
// one: the name on the first line, then each item on its own line indented
// two spaces, conditions inside (when ...) aligned under the first, and the
// closing paren on the last line. A GUI that edits a rule through a form
// writes the result back with this and Splice, leaving the rest of the file
// untouched (GUI design §5.1).
//
// What it prints comes from the parsed rule, so anything the parser dropped
// - comments inside the form, the writer's own line breaks - is not in the
// output; Splice's caller warns about that before replacing a form that
// holds comments.
func PrintRule(r *Rule) string {
var b strings.Builder
b.WriteString("(rule " + sexp.Quote(r.Name) + "\n")
if r.HasWhen {
b.WriteString(" (when ")
for i, c := range r.When {
if i > 0 {
b.WriteString("\n ")
}
b.WriteString(printNode(c))
}
b.WriteString(")\n")
}
for _, line := range printSettings(r.Settings) {
b.WriteString(" " + line + "\n")
}
for _, a := range r.Actions {
b.WriteString(" " + printAction(a) + "\n")
}
if r.Stop {
b.WriteString(" (stop)\n")
}
// The closing paren belongs to the last line written, whichever it was.
out := strings.TrimRight(b.String(), "\n")
return out + ")\n"
}
// PrintExclude renders an (exclude ...) form on one line.
func PrintExclude(x *Exclude) string {
var b strings.Builder
b.WriteString("(exclude")
for _, c := range x.When {
b.WriteString(" " + printNode(c))
}
b.WriteString(")\n")
return b.String()
}
// Splice replaces the bytes of the form between start and end - a form's
// Pos and End - with text, and returns the new file contents. Every other
// byte of src, comments and layout included, is kept exactly. Offsets that
// do not belong to src - a form parsed from text that has since changed -
// are an error, not a panic (plan 13 review F6).
func Splice(src []byte, start, end sexp.Pos, text string) ([]byte, error) {
if start.Offset < 0 || end.Offset < start.Offset || end.Offset > len(src) {
return nil, fmt.Errorf("config: splice %d:%d is not inside %d bytes", start.Offset, end.Offset, len(src))
}
out := make([]byte, 0, len(src)-(end.Offset-start.Offset)+len(text))
out = append(out, src[:start.Offset]...)
out = append(out, text...)
return append(out, src[end.Offset:]...), nil
}
// printAction renders one action form.
func printAction(a Action) string {
switch a.Kind {
case Delete:
return "(delete)"
case DeletePermanent:
return "(delete permanent)"
}
return "(" + a.Kind.String() + " " + sexp.Quote(a.Arg) + ")"
}
// printSettings renders the settings a rule may carry (case, fold,
// on-conflict), in that order, leaving out those it does not set.
func printSettings(s Settings) []string {
var out []string
if s.Case != nil {
out = append(out, "(case "+s.Case.String()+")")
}
if s.Fold != nil {
word := "no"
if *s.Fold {
word = "yes"
}
out = append(out, "(fold "+word+")")
}
if s.OnConflict != nil {
out = append(out, "(on-conflict "+s.OnConflict.String()+")")
}
return out
}
// printNode renders one condition node: a symbol as written, a string
// quoted, a list as its head and arguments separated by single spaces.
func printNode(n *sexp.Node) string {
switch n.Kind {
case sexp.String:
return sexp.Quote(n.Text)
case sexp.Symbol:
return n.Text
}
parts := make([]string, 0, len(n.Children))
for _, c := range n.Children {
parts = append(parts, printNode(c))
}
return "(" + strings.Join(parts, " ") + ")"
}
|