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) result = strings.ReplaceAll(result, "", getRandomUsername()) result = strings.ReplaceAll(result, "", args) result = strings.ReplaceAll(result, "", aiResult) // Обработка , , result, soundFiles, timeoutMinutes = processTags(result) // Рекурсивная обработка всех 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 обрабатывает теги , , func processTags(text string) (string, []string, int) { // randomRe := regexp.MustCompile(``) 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) }) // songRe := regexp.MustCompile(`]+)\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 "" }) // timeoutMinutes := 0 timeoutRe := regexp.MustCompile(``) 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) { // Обработка randomRe := regexp.MustCompile(``) 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) }) // Обработка с любыми атрибутами songRe := regexp.MustCompile(`]+)\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, "") { break } } return text, nil } func processOneGroup(text string) (string, error) { start := strings.Index(text, "") if start == -1 { return text, nil } end := findMatchingClosingTag(text, start) if end == -1 { return "", fmt.Errorf("unclosed at position %d", start) } inner := text[start+7 : end] sections := extractGSections(inner) if len(sections) == 0 { return "", fmt.Errorf("no sections inside at position %d", start) } chosen := sections[rand.Intn(len(sections))] newText := text[:start] + chosen + text[end+8:] return newText, nil } // findMatchingClosingTag ищет позицию закрывающего с учётом вложенности func findMatchingClosingTag(text string, start int) int { depth := 1 i := start + 7 for i < len(text) { if strings.HasPrefix(text[i:], "") { depth++ i += 7 } else if strings.HasPrefix(text[i:], "") { depth-- if depth == 0 { return i } i += 8 } else { i++ } } return -1 } // extractGSections извлекает содержимое всех ... на верхнем уровне (не внутри вложенных групп) func extractGSections(s string) []string { var result []string i := 0 for i < len(s) { if strings.HasPrefix(s[i:], "") { startContent := i + 3 j := startContent depth := 0 for j < len(s) { if strings.HasPrefix(s[j:], "") { depth++ j += 7 } else if strings.HasPrefix(s[j:], "") { if depth > 0 { depth-- } j += 8 } else if strings.HasPrefix(s[j:], "") && 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:], "") { // Пропускаем вложенную группу целиком 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:], "") { balance++ i += 6 } else if strings.HasPrefix(template[i:], "") { balance-- i += 7 } } if balance != 0 { return fmt.Errorf("unbalanced tags") } return nil }