Files
sanspie 2744cff481
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m57s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
Stage 9.6c1: project refs and refactorings without the editor (D298)
2026-10-20 12:12:18 +03:00

175 lines
3.4 KiB
Go

package rename
import (
"regexp"
"strconv"
"strings"
"unicode"
"aego/core/errs"
)
type Case uint8
const (
CaseKeep Case = iota
CaseLower
CaseUpper
CaseTitle
)
type Rule struct {
Pattern string
Find string
Replace string
Regex bool
Case Case
Start int
Step int
}
type Item struct {
Name string
Ext string
Parent string
}
type Row struct {
From string
To string
Conflict string
}
func (r Row) Changed() bool {
return r.From != r.To
}
var token = regexp.MustCompile(`\{(name|n|parent|ext)(?::(0?\d+))?\}`)
func title(s string) string {
rs := []rune(s)
up := true
for i, r := range rs {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
if up {
rs[i] = unicode.ToUpper(r)
} else {
rs[i] = unicode.ToLower(r)
}
up = false
continue
}
up = true
}
return string(rs)
}
func applyCase(s string, c Case) string {
switch c {
case CaseLower:
return strings.ToLower(s)
case CaseUpper:
return strings.ToUpper(s)
case CaseTitle:
return title(s)
}
return s
}
func number(n int, width string) string {
s := strconv.Itoa(n)
if width == "" {
return s
}
w, _ := strconv.Atoi(width)
if neg := n < 0; neg {
return "-" + strings.Repeat("0", max(0, w-len(s))) + s[1:]
}
return strings.Repeat("0", max(0, w-len(s))) + s
}
func Plan(rule Rule, items []Item, valid func(string) bool, taken func(parent, name string) bool, fold bool) ([]Row, error) {
var re *regexp.Regexp
if rule.Find != "" && rule.Regex {
var err error
if re, err = regexp.Compile(rule.Find); err != nil {
return nil, errs.New(errs.EditorCommand, "the find pattern is not a valid regular expression: "+err.Error())
}
}
pattern := rule.Pattern
if strings.TrimSpace(pattern) == "" {
pattern = "{name}"
}
step := rule.Step
if step == 0 {
step = 1
}
key := func(parent, name string) string {
if fold {
name = strings.ToLower(name)
}
return parent + "\x00" + name
}
own := map[string]bool{}
for _, it := range items {
own[key(it.Parent, it.Name+it.Ext)] = true
}
rows := make([]Row, len(items))
seen := map[string]int{}
for i, it := range items {
base := it.Name
if rule.Find != "" {
if re != nil {
base = re.ReplaceAllString(base, rule.Replace)
} else {
base = strings.ReplaceAll(base, rule.Find, rule.Replace)
}
}
n := rule.Start + i*step
out := token.ReplaceAllStringFunc(pattern, func(m string) string {
sub := token.FindStringSubmatch(m)
switch sub[1] {
case "name":
return base
case "n":
return number(n, sub[2])
case "parent":
return it.Parent
case "ext":
return strings.TrimPrefix(it.Ext, ".")
}
return m
})
out = strings.TrimSpace(applyCase(out, rule.Case))
rows[i] = Row{From: it.Name, To: out}
switch {
case out == "":
rows[i].Conflict = "empty name"
case valid != nil && !valid(out):
rows[i].Conflict = "not a valid name"
}
k := key(it.Parent, out+it.Ext)
if j, dup := seen[k]; dup {
rows[i].Conflict = "same name as " + items[j].Name
if rows[j].Conflict == "" {
rows[j].Conflict = "same name as " + it.Name
}
} else {
seen[k] = i
}
if rows[i].Conflict == "" && rows[i].Changed() && !own[k] && taken != nil && taken(it.Parent, out+it.Ext) {
rows[i].Conflict = "already exists"
}
}
return rows, nil
}
func Blocked(rows []Row) bool {
for _, r := range rows {
if r.Conflict != "" {
return true
}
}
return false
}