Files
renamer/internal/regex/template.go
2025-10-31 10:12:02 +08:00

110 lines
2.6 KiB
Go

package regex
import (
"fmt"
"strconv"
"strings"
)
type templateSegment struct {
literal string
group int
}
const literalSegment = -1
// template represents a parsed replacement template with capture placeholders.
type template struct {
segments []templateSegment
}
// parseTemplate converts a string containing literal text, numbered placeholders (@0, @1, ...),
// and escaped @@ sequences into a template structure. It returns the template, the highest
// placeholder index encountered, or an error when syntax is invalid.
func parseTemplate(input string) (template, int, error) {
segments := make([]templateSegment, 0)
var literal strings.Builder
maxGroup := 0
i := 0
for i < len(input) {
ch := input[i]
if ch != '@' {
literal.WriteByte(ch)
i++
continue
}
// Flush any buffered literal before handling placeholder/escape.
flushLiteral := func() {
if literal.Len() == 0 {
return
}
segments = append(segments, templateSegment{literal: literal.String(), group: literalSegment})
literal.Reset()
}
if i+1 >= len(input) {
return template{}, 0, fmt.Errorf("dangling @ at end of template")
}
next := input[i+1]
if next == '@' {
flushLiteral()
literal.WriteByte('@')
i += 2
continue
}
j := i + 1
for j < len(input) && input[j] >= '0' && input[j] <= '9' {
j++
}
if j == i+1 {
return template{}, 0, fmt.Errorf("invalid placeholder at offset %d", i)
}
indexStr := input[i+1 : j]
index, err := strconv.Atoi(indexStr)
if err != nil {
return template{}, 0, fmt.Errorf("invalid placeholder index @%s", indexStr)
}
flushLiteral()
segments = append(segments, templateSegment{group: index})
if index > maxGroup {
maxGroup = index
}
i = j
}
if literal.Len() > 0 {
segments = append(segments, templateSegment{literal: literal.String(), group: literalSegment})
}
return template{segments: segments}, maxGroup, nil
}
// render produces the output string for a given set of submatches. The slice must contain the
// full match at index 0 followed by capture groups. Missing groups (e.g., optional matches)
// expand to empty strings. Referencing a group index beyond the available matches returns an error.
func (t template) render(submatches []string) (string, error) {
var builder strings.Builder
for _, segment := range t.segments {
if segment.group == literalSegment {
builder.WriteString(segment.literal)
continue
}
if segment.group >= len(submatches) {
return "", ErrUndefinedPlaceholder{Index: segment.group}
}
builder.WriteString(submatches[segment.group])
}
return builder.String(), nil
}