141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
// Package siteyaml changes one top-level block of a YAML file (site.yaml)
|
|
// and leaves every other line exactly as it was written.
|
|
package siteyaml
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func emptyValue(v any) bool {
|
|
switch x := v.(type) {
|
|
case nil:
|
|
return true
|
|
case map[string]any:
|
|
return len(x) == 0
|
|
case []any:
|
|
return len(x) == 0
|
|
case []map[string]string:
|
|
return len(x) == 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
var topKey = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]*\s*:`)
|
|
|
|
// SetTopLevel replaces one top-level key of a YAML file (removing it when
|
|
// value is empty), leaving every other line exactly as written: site.yaml is
|
|
// a file people read and comment, and a diff should show only the change.
|
|
func SetTopLevel(text, key string, value any) (string, error) {
|
|
var block string
|
|
if !emptyValue(value) {
|
|
var buf bytes.Buffer
|
|
enc := yaml.NewEncoder(&buf)
|
|
enc.SetIndent(2)
|
|
if err := enc.Encode(map[string]any{key: value}); err != nil {
|
|
return "", err
|
|
}
|
|
block = buf.String()
|
|
}
|
|
lines := strings.SplitAfter(text, "\n")
|
|
start, end := -1, len(lines)
|
|
for i, l := range lines {
|
|
if start < 0 {
|
|
if strings.HasPrefix(l, key+":") {
|
|
start = i
|
|
}
|
|
continue
|
|
}
|
|
// The block ends at the next top-level key or comment.
|
|
if topKey.MatchString(l) || strings.HasPrefix(l, "#") {
|
|
end = i
|
|
break
|
|
}
|
|
}
|
|
if start < 0 {
|
|
if block == "" {
|
|
return text, nil
|
|
}
|
|
if text != "" && !strings.HasSuffix(text, "\n") {
|
|
text += "\n"
|
|
}
|
|
return text + block, nil
|
|
}
|
|
if strings.HasPrefix(strings.TrimSpace(strings.TrimPrefix(lines[start], key+":")), "{") {
|
|
return "", fmt.Errorf("site.yaml writes %s: on one line; split it over several lines first", key)
|
|
}
|
|
out := strings.Join(lines[:start], "") + block + strings.Join(lines[end:], "")
|
|
var check map[string]any
|
|
if err := yaml.Unmarshal([]byte(out), &check); err != nil {
|
|
return "", fmt.Errorf("site.yaml wouldn't be valid YAML after the change: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// AddEntry adds child: value to the top-level mapping key: (collections:,
|
|
// say), after its last entry, leaving every other line as written. The
|
|
// caller makes sure child isn't there already. If key: doesn't exist, it's
|
|
// added at the end.
|
|
func AddEntry(text, key, child string, value any) (string, error) {
|
|
var buf bytes.Buffer
|
|
enc := yaml.NewEncoder(&buf)
|
|
enc.SetIndent(2)
|
|
if err := enc.Encode(map[string]any{child: value}); err != nil {
|
|
return "", err
|
|
}
|
|
lines := strings.SplitAfter(text, "\n")
|
|
start := -1
|
|
for i, l := range lines {
|
|
if strings.HasPrefix(l, key+":") {
|
|
start = i
|
|
break
|
|
}
|
|
}
|
|
if start < 0 {
|
|
return SetTopLevel(text, key, map[string]any{child: value})
|
|
}
|
|
if rest := strings.TrimSpace(strings.TrimPrefix(lines[start], key+":")); rest != "" && !strings.HasPrefix(rest, "#") {
|
|
return "", fmt.Errorf("site.yaml writes %s: on one line; split it over several lines first", key)
|
|
}
|
|
// The block's entries are the indented lines after key:; new ones go
|
|
// after the last of them, at the same indent.
|
|
last, indent := start, " "
|
|
found := false
|
|
for i := start + 1; i < len(lines); i++ {
|
|
l := lines[i]
|
|
t := strings.TrimSpace(l)
|
|
if t == "" {
|
|
continue
|
|
}
|
|
if l[0] != ' ' && l[0] != '\t' {
|
|
break // the next top-level key or comment
|
|
}
|
|
if !found && !strings.HasPrefix(t, "#") {
|
|
indent = l[:len(l)-len(strings.TrimLeft(l, " \t"))]
|
|
found = true
|
|
}
|
|
last = i
|
|
}
|
|
var add strings.Builder
|
|
for _, l := range strings.SplitAfter(buf.String(), "\n") {
|
|
if l != "" {
|
|
add.WriteString(indent + l)
|
|
}
|
|
}
|
|
tail := strings.Join(lines[last+1:], "")
|
|
head := strings.Join(lines[:last+1], "")
|
|
if !strings.HasSuffix(head, "\n") {
|
|
head += "\n"
|
|
}
|
|
out := head + add.String() + tail
|
|
var check map[string]any
|
|
if err := yaml.Unmarshal([]byte(out), &check); err != nil {
|
|
return "", fmt.Errorf("site.yaml wouldn't be valid YAML after the change: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|