ci / module boundaries / boundaries (push) Successful in 7s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 55s
ci / ts / ts (packages + web) (push) Failing after 4m4s
ci / api / api (push) Successful in 55s
ci / db / db (push) Failing after 1m14s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m11s
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
package meta
|
|
|
|
import (
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// TemplateSuggestion compares candidate layouts against the same sample of stored paths.
|
|
type TemplateSuggestion struct {
|
|
Template string
|
|
Matched int
|
|
Total int
|
|
Examples []string
|
|
}
|
|
|
|
// SuggestFromSamples also works without configured path regexes, using catalog metadata.
|
|
func SuggestFromSamples(exprs []string, samples []TemplateSample) (TemplateSuggestion, error) {
|
|
candidates := map[string]bool{}
|
|
paths := make([]string, 0, len(samples))
|
|
for _, s := range samples {
|
|
paths = append(paths, s.RelPath)
|
|
if tpl := inferTemplate(s); tpl != "" {
|
|
candidates[tpl] = true
|
|
}
|
|
}
|
|
for _, expr := range exprs {
|
|
if tpl, err := SuggestTemplate([]string{expr}, paths); err == nil {
|
|
candidates[tpl] = true
|
|
}
|
|
}
|
|
keys := make([]string, 0, len(candidates))
|
|
for tpl := range candidates {
|
|
keys = append(keys, tpl)
|
|
}
|
|
sort.Strings(keys)
|
|
best := TemplateSuggestion{Total: len(samples), Examples: []string{}}
|
|
for _, tpl := range keys {
|
|
t, err := ParseTemplate(tpl)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
count := 0
|
|
var examples []string
|
|
for _, s := range samples {
|
|
if rendered, err := t.Render(s.Input); err == nil && rendered == norm.NFC.String(s.RelPath) {
|
|
count++
|
|
if len(examples) < 3 {
|
|
examples = append(examples, s.RelPath)
|
|
}
|
|
}
|
|
}
|
|
if count > best.Matched {
|
|
best.Template, best.Matched, best.Examples = tpl, count, examples
|
|
}
|
|
}
|
|
if best.Matched == 0 {
|
|
return best, ErrNoTemplate
|
|
}
|
|
return best, nil
|
|
}
|
|
|
|
func inferTemplate(s TemplateSample) string {
|
|
type token struct{ value, placeholder string }
|
|
segments := strings.Split(norm.NFC.String(s.RelPath), "/")
|
|
for i, segment := range segments {
|
|
last := i == len(segments)-1
|
|
fields := []string{"albumartist", "artist", "album", "year", "genre", "disc"}
|
|
if last {
|
|
ext := path.Ext(segment)
|
|
if ext == "" {
|
|
return ""
|
|
}
|
|
segment = strings.TrimSuffix(segment, ext)
|
|
fields = []string{"title", "albumartist", "artist", "album", "year", "track", "disc"}
|
|
}
|
|
var tokens []token
|
|
for _, field := range fields {
|
|
v := cleanValue(fieldValue(s.Input, tnode{field: field}))
|
|
if v == "" {
|
|
continue
|
|
}
|
|
tokens = append(tokens, token{v, "{" + field + "}"})
|
|
if field == "track" || field == "disc" {
|
|
n, _ := strconv.Atoi(v)
|
|
for width := 2; width <= 3; width++ {
|
|
padded := strings.Repeat("0", max(0, width-len(v))) + strconv.Itoa(n)
|
|
if padded != v {
|
|
tokens = append(tokens, token{padded, "{" + field + ":0" + strconv.Itoa(width) + "}"})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Longest value wins; stable ties prefer album artist in folders and title in filenames.
|
|
sort.SliceStable(tokens, func(a, b int) bool { return len(tokens[a].value) > len(tokens[b].value) })
|
|
var out strings.Builder
|
|
for at := 0; at < len(segment); {
|
|
matched := false
|
|
for _, tok := range tokens {
|
|
if !strings.HasPrefix(segment[at:], tok.value) {
|
|
continue
|
|
}
|
|
end := at + len(tok.value)
|
|
if tok.value[0] >= '0' && tok.value[0] <= '9' && ((at > 0 && segment[at-1] >= '0' && segment[at-1] <= '9') || (end < len(segment) && segment[end] >= '0' && segment[end] <= '9')) {
|
|
continue
|
|
}
|
|
out.WriteString(tok.placeholder)
|
|
at = end
|
|
matched = true
|
|
break
|
|
}
|
|
if !matched {
|
|
if strings.ContainsRune("{}[]", rune(segment[at])) {
|
|
return ""
|
|
}
|
|
out.WriteByte(segment[at])
|
|
at++
|
|
}
|
|
}
|
|
segments[i] = out.String()
|
|
if last {
|
|
segments[i] += ".{ext}"
|
|
}
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|