233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
package parser
|
||
|
||
import (
|
||
"fmt"
|
||
"math/rand"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
func init() {
|
||
rand.Seed(time.Now().UnixNano())
|
||
}
|
||
|
||
// ParseTemplate возвращает текст, список звуков и количество минут для таймаута (0 если нет)
|
||
func ParseTemplate(template string, username string, args string, aiResult string, getRandomUsername func() string) (result string, soundFiles []string, timeoutMinutes int, err error) {
|
||
// Замена простых переменных
|
||
result = strings.ReplaceAll(template, "<USERNAME/>", "@"+username)
|
||
result = strings.ReplaceAll(result, "<RANDOMUSER/>", getRandomUsername())
|
||
result = strings.ReplaceAll(result, "<ARG/>", args)
|
||
result = strings.ReplaceAll(result, "<AI/>", aiResult)
|
||
|
||
// Обработка <random>, <song>, <timeout>
|
||
result, soundFiles, timeoutMinutes = processTags(result)
|
||
|
||
// Рекурсивная обработка всех <group>
|
||
result, err = processGroups(result)
|
||
if err != nil {
|
||
return "", nil, 0, err
|
||
}
|
||
result = strings.ReplaceAll(result, "\n", " ")
|
||
result = strings.ReplaceAll(result, "\r", " ")
|
||
result = regexp.MustCompile(`\s+`).ReplaceAllString(result, " ")
|
||
result = strings.TrimSpace(result)
|
||
return result, soundFiles, timeoutMinutes, nil
|
||
}
|
||
|
||
func getRandomViewer() string {
|
||
names := []string{"зрителя", "кого-то", "случайного пользователя", "незнакомца"}
|
||
return names[rand.Intn(len(names))]
|
||
}
|
||
|
||
// processTags обрабатывает теги <random>, <song>, <timeout>
|
||
func processTags(text string) (string, []string, int) {
|
||
// <random>
|
||
randomRe := regexp.MustCompile(`<random\s+s=([-0-9]+)\s+e=([-0-9]+)\s*/>`)
|
||
text = randomRe.ReplaceAllStringFunc(text, func(m string) string {
|
||
sub := randomRe.FindStringSubmatch(m)
|
||
s, _ := strconv.Atoi(sub[1])
|
||
e, _ := strconv.Atoi(sub[2])
|
||
if s > e {
|
||
s, e = e, s
|
||
}
|
||
val := rand.Intn(e-s+1) + s
|
||
return strconv.Itoa(val)
|
||
})
|
||
|
||
// <song>
|
||
songRe := regexp.MustCompile(`<song\s+([^>]+)\s*/>`)
|
||
soundFiles := make([]string, 0)
|
||
text = songRe.ReplaceAllStringFunc(text, func(m string) string {
|
||
fRe := regexp.MustCompile(`f="([^"]+)"`)
|
||
matches := fRe.FindStringSubmatch(m)
|
||
if len(matches) > 1 {
|
||
soundFiles = append(soundFiles, matches[1])
|
||
}
|
||
return ""
|
||
})
|
||
|
||
// <timeout minutes="X"/>
|
||
timeoutMinutes := 0
|
||
timeoutRe := regexp.MustCompile(`<timeout\s+minutes="?([0-9]+)"?\s*/>`)
|
||
text = timeoutRe.ReplaceAllStringFunc(text, func(m string) string {
|
||
sub := timeoutRe.FindStringSubmatch(m)
|
||
if len(sub) > 1 {
|
||
minutes, _ := strconv.Atoi(sub[1])
|
||
if minutes > 0 {
|
||
timeoutMinutes = minutes
|
||
}
|
||
}
|
||
return ""
|
||
})
|
||
|
||
return text, soundFiles, timeoutMinutes
|
||
}
|
||
|
||
func processRandomAndSong(text string) (string, []string) {
|
||
// Обработка <random>
|
||
randomRe := regexp.MustCompile(`<random\s+s=([-0-9]+)\s+e=([-0-9]+)\s*/>`)
|
||
text = randomRe.ReplaceAllStringFunc(text, func(m string) string {
|
||
sub := randomRe.FindStringSubmatch(m)
|
||
s, _ := strconv.Atoi(sub[1])
|
||
e, _ := strconv.Atoi(sub[2])
|
||
if s > e {
|
||
s, e = e, s
|
||
}
|
||
val := rand.Intn(e-s+1) + s
|
||
return strconv.Itoa(val)
|
||
})
|
||
|
||
// Обработка <song> с любыми атрибутами
|
||
songRe := regexp.MustCompile(`<song\s+([^>]+)\s*/>`)
|
||
soundFiles := make([]string, 0)
|
||
text = songRe.ReplaceAllStringFunc(text, func(m string) string {
|
||
// Извлекаем значение атрибута f="..."
|
||
fRe := regexp.MustCompile(`f="([^"]+)"`)
|
||
matches := fRe.FindStringSubmatch(m)
|
||
if len(matches) > 1 {
|
||
soundFiles = append(soundFiles, matches[1])
|
||
}
|
||
return "" // заменяем тег на пустую строку
|
||
})
|
||
return text, soundFiles
|
||
}
|
||
|
||
// processGroups и остальные функции без изменений ...
|
||
func processGroups(text string) (string, error) {
|
||
var err error
|
||
for {
|
||
text, err = processOneGroup(text)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if !strings.Contains(text, "<group>") {
|
||
break
|
||
}
|
||
}
|
||
return text, nil
|
||
}
|
||
|
||
func processOneGroup(text string) (string, error) {
|
||
start := strings.Index(text, "<group>")
|
||
if start == -1 {
|
||
return text, nil
|
||
}
|
||
end := findMatchingClosingTag(text, start)
|
||
if end == -1 {
|
||
return "", fmt.Errorf("unclosed <group> at position %d", start)
|
||
}
|
||
inner := text[start+7 : end]
|
||
sections := extractGSections(inner)
|
||
if len(sections) == 0 {
|
||
return "", fmt.Errorf("no <g> sections inside <group> at position %d", start)
|
||
}
|
||
chosen := sections[rand.Intn(len(sections))]
|
||
newText := text[:start] + chosen + text[end+8:]
|
||
return newText, nil
|
||
}
|
||
|
||
// findMatchingClosingTag ищет позицию закрывающего </group> с учётом вложенности
|
||
func findMatchingClosingTag(text string, start int) int {
|
||
depth := 1
|
||
i := start + 7
|
||
for i < len(text) {
|
||
if strings.HasPrefix(text[i:], "<group>") {
|
||
depth++
|
||
i += 7
|
||
} else if strings.HasPrefix(text[i:], "</group>") {
|
||
depth--
|
||
if depth == 0 {
|
||
return i
|
||
}
|
||
i += 8
|
||
} else {
|
||
i++
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
// extractGSections извлекает содержимое всех <g>...</g> на верхнем уровне (не внутри вложенных групп)
|
||
func extractGSections(s string) []string {
|
||
var result []string
|
||
i := 0
|
||
for i < len(s) {
|
||
if strings.HasPrefix(s[i:], "<g>") {
|
||
startContent := i + 3
|
||
j := startContent
|
||
depth := 0
|
||
for j < len(s) {
|
||
if strings.HasPrefix(s[j:], "<group>") {
|
||
depth++
|
||
j += 7
|
||
} else if strings.HasPrefix(s[j:], "</group>") {
|
||
if depth > 0 {
|
||
depth--
|
||
}
|
||
j += 8
|
||
} else if strings.HasPrefix(s[j:], "</g>") && depth == 0 {
|
||
content := s[startContent:j]
|
||
result = append(result, content)
|
||
i = j + 4
|
||
break
|
||
} else {
|
||
j++
|
||
}
|
||
}
|
||
if j >= len(s) {
|
||
break
|
||
}
|
||
} else if strings.HasPrefix(s[i:], "<group>") {
|
||
// Пропускаем вложенную группу целиком
|
||
groupEnd := findMatchingClosingTag(s, i)
|
||
if groupEnd == -1 {
|
||
break
|
||
}
|
||
i = groupEnd + 8
|
||
} else {
|
||
i++
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// ValidateTemplate проверяет баланс тегов
|
||
func ValidateTemplate(template string) error {
|
||
balance := 0
|
||
for i := 0; i < len(template); i++ {
|
||
if strings.HasPrefix(template[i:], "<group>") {
|
||
balance++
|
||
i += 6
|
||
} else if strings.HasPrefix(template[i:], "</group>") {
|
||
balance--
|
||
i += 7
|
||
}
|
||
}
|
||
if balance != 0 {
|
||
return fmt.Errorf("unbalanced <group> tags")
|
||
}
|
||
return nil
|
||
}
|