diff --git a/VERSION b/VERSION index fd2a018..94ff29c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 +3.1.1 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 3020301..596a010 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -7290,17 +7290,47 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) { rewardStrings[reward.Position] = rewardStr } - // Подставляем в reward_message основной задачи - var mainTaskMessage string - if task.RewardMessage != nil && *task.RewardMessage != "" { - mainTaskMessage = *task.RewardMessage - // Заменяем плейсхолдеры ${0}, ${1}, и т.д. + // Функция для замены плейсхолдеров в сообщении награды + replaceRewardPlaceholders := func(message string, rewardStrings map[int]string) string { + result := message + // Сначала сохраняем экранированные плейсхолдеры \$0, \$1 и т.д. во временные маркеры + escapedMarkers := make(map[string]string) + for i := 0; i < 100; i++ { + escaped := fmt.Sprintf(`\$%d`, i) + marker := fmt.Sprintf(`__ESCAPED_DOLLAR_%d__`, i) + if strings.Contains(result, escaped) { + escapedMarkers[marker] = escaped + result = strings.ReplaceAll(result, escaped, marker) + } + } + // Заменяем ${0}, ${1}, и т.д. for i := 0; i < 100; i++ { // Максимум 100 плейсхолдеров placeholder := fmt.Sprintf("${%d}", i) if rewardStr, ok := rewardStrings[i]; ok { - mainTaskMessage = strings.ReplaceAll(mainTaskMessage, placeholder, rewardStr) + result = strings.ReplaceAll(result, placeholder, rewardStr) } } + // Затем заменяем $0, $1, и т.д. (экранированные уже защищены маркерами) + // Используем регулярное выражение для поиска $N, где после N не идет еще одна цифра + for i := 0; i < 100; i++ { + if rewardStr, ok := rewardStrings[i]; ok { + // Паттерн: $N, где после N не идет еще одна цифра (чтобы не заменить $10 при поиске $1) + pattern := fmt.Sprintf(`\$%d(?!\d)`, i) + re := regexp.MustCompile(pattern) + result = re.ReplaceAllString(result, rewardStr) + } + } + // Восстанавливаем экранированные доллары из временных маркеров + for marker, escaped := range escapedMarkers { + result = strings.ReplaceAll(result, marker, escaped) + } + return result + } + + // Подставляем в reward_message основной задачи + var mainTaskMessage string + if task.RewardMessage != nil && *task.RewardMessage != "" { + mainTaskMessage = replaceRewardPlaceholders(*task.RewardMessage, rewardStrings) } else { // Если reward_message пустой, используем имя задачи mainTaskMessage = task.Name @@ -7394,13 +7424,7 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) { } // Подставляем в reward_message подзадачи - subtaskMessage := subtaskRewardMessage.String - for i := 0; i < 100; i++ { - placeholder := fmt.Sprintf("${%d}", i) - if rewardStr, ok := subtaskRewardStrings[i]; ok { - subtaskMessage = strings.ReplaceAll(subtaskMessage, placeholder, rewardStr) - } - } + subtaskMessage := replaceRewardPlaceholders(subtaskRewardMessage.String, subtaskRewardStrings) subtaskMessages = append(subtaskMessages, subtaskMessage) } diff --git a/play-life-web/package.json b/play-life-web/package.json index aa19d81..e90fa4d 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "3.0.1", + "version": "3.1.1", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/TaskForm.jsx b/play-life-web/src/components/TaskForm.jsx index 292e751..e0bc39e 100644 --- a/play-life-web/src/components/TaskForm.jsx +++ b/play-life-web/src/components/TaskForm.jsx @@ -268,10 +268,40 @@ function TaskForm({ onNavigate, taskId }) { const findMaxPlaceholderIndex = (message) => { if (!message) return -1 - const matches = message.match(/\$\{(\d+)\}/g) - if (!matches) return -1 - const indices = matches.map(m => parseInt(m.match(/\d+/)[0])) - return Math.max(...indices) + // Находим все варианты плейсхолдеров: ${0}, $0, но не \$0 + const indices = [] + + // Ищем ${N} + const matchesCurly = message.match(/\$\{(\d+)\}/g) || [] + matchesCurly.forEach(match => { + const numMatch = match.match(/\d+/) + if (numMatch) { + indices.push(parseInt(numMatch[0])) + } + }) + + // Ищем $N (но не \$N) + // Используем глобальный поиск и проверяем, что перед $ нет обратного слэша + let searchIndex = 0 + while (true) { + const index = message.indexOf('$', searchIndex) + if (index === -1) break + + // Проверяем, что перед $ нет обратного слэша + if (index === 0 || message[index - 1] !== '\\') { + // Проверяем, что после $ идет цифра + const afterDollar = message.substring(index + 1) + const digitMatch = afterDollar.match(/^(\d+)/) + if (digitMatch) { + // Проверяем, что после цифры не идет еще одна цифра (чтобы не захватить $10 при поиске $1) + const num = parseInt(digitMatch[0]) + indices.push(num) + } + } + searchIndex = index + 1 + } + + return indices.length > 0 ? Math.max(...indices) : -1 } @@ -551,7 +581,7 @@ function TaskForm({ onNavigate, taskId }) { id="reward_message" value={rewardMessage} onChange={(e) => setRewardMessage(e.target.value)} - placeholder="Используйте ${0}, ${1} для указания проектов" + placeholder="Используйте ${0}, $0 для указания проектов (\\$0 для экранирования)" className="form-textarea" rows={3} />