// 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, " ") + ")" }