Files
magpie/internal/edit/text.go
T
Zhizhen He de2530826d edit: preserve YAML entries when updating top-level keys (#452)
Changing Goose's model or effort could insert a key inside a multiline flow
collection or append a duplicate when the existing key was quoted. Locate
entries through parsed keys, keep complete values together, and validate the
result before writing it. Read and delete use decoded key names too.

Keep other entries, document markers, comments and block-scalar blank lines
in place. Reject invalid input, multiple documents and layouts that cannot
be edited safely as whole lines, with the file path in the error.

Regression tests reproduce the flow-value and quoted-key failures and cover
multiline values, unchanged surrounding content and failed writes.
2026-10-02 00:57:49 +08:00

108 lines
2.3 KiB
Go

package edit
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// TOML and top-level YAML entries are located by their parsers (toml.go and
// yaml_top.go), then edited without reformatting the rest of the file.
var (
// tomlTable and tomlKV only read a TOML file its parser refuses; see
// GetTOMLTop.
tomlTable = regexp.MustCompile(`^\s*\[`)
tomlKV = regexp.MustCompile(`^\s*([A-Za-z0-9_.-]+|"[^"]*")\s*=\s*(.*?)\s*$`)
)
// setLine replaces the line whose key matches, or inserts newLine after the
// last key line in the header section (before the first line matching stop).
func setLine(lines []string, key, newLine string, stop *regexp.Regexp, keyOf func(string) (string, bool)) []string {
lastKey := -1
for i, line := range lines {
if stop != nil && stop.MatchString(line) {
break
}
k, ok := keyOf(line)
if !ok {
continue
}
if k == key {
lines[i] = newLine
return lines
}
lastKey = i
}
at := lastKey + 1
out := make([]string, 0, len(lines)+1)
out = append(out, lines[:at]...)
out = append(out, newLine)
out = append(out, lines[at:]...)
return out
}
// unquote strips a leading quoted scalar (and anything after it, such as an
// inline comment); ok is false when v does not start with a quote.
func unquote(v string) (string, bool) {
if v == "" || (v[0] != '"' && v[0] != '\'') {
return "", false
}
q := v[0]
for i := 1; i < len(v); i++ {
if q == '"' && v[i] == '\\' {
i++
continue
}
if v[i] == q {
if q == '"' {
if s, err := strconv.Unquote(v[:i+1]); err == nil {
return s, true
}
}
return v[1:i], true
}
}
return v[1:], true
}
func tomlValue(v string) string {
if s, ok := unquote(v); ok {
return s
}
if i := strings.Index(v, "#"); i >= 0 {
v = strings.TrimSpace(v[:i])
}
return v
}
func toString(v any) string {
switch x := v.(type) {
case string:
return x
case bool:
return strconv.FormatBool(x)
case int:
return strconv.Itoa(x)
}
return fmt.Sprint(v)
}
// splitLines splits on '\n' but keeps a trailing newline as an empty final
// element so joinLines can restore the file exactly.
func splitLines(s string) []string {
if s == "" {
return []string{""}
}
return strings.Split(s, "\n")
}
func joinLines(lines []string) string {
s := strings.Join(lines, "\n")
if !strings.HasSuffix(s, "\n") {
s += "\n"
}
return s
}