залил
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user