feat: добавлена поддержка шаблонов $0 и \$0 для наград в задачах (v3.1.1)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 44s

This commit is contained in:
poignatov
2026-01-06 15:00:42 +03:00
parent 0ea531889d
commit 7df258da15
4 changed files with 74 additions and 20 deletions

View File

@@ -1 +1 @@
3.1.0 3.1.1

View File

@@ -7290,17 +7290,47 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
rewardStrings[reward.Position] = rewardStr rewardStrings[reward.Position] = rewardStr
} }
// Подставляем в reward_message основной задачи // Функция для замены плейсхолдеров в сообщении награды
var mainTaskMessage string replaceRewardPlaceholders := func(message string, rewardStrings map[int]string) string {
if task.RewardMessage != nil && *task.RewardMessage != "" { result := message
mainTaskMessage = *task.RewardMessage // Сначала сохраняем экранированные плейсхолдеры \$0, \$1 и т.д. во временные маркеры
// Заменяем плейсхолдеры ${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 плейсхолдеров for i := 0; i < 100; i++ { // Максимум 100 плейсхолдеров
placeholder := fmt.Sprintf("${%d}", i) placeholder := fmt.Sprintf("${%d}", i)
if rewardStr, ok := rewardStrings[i]; ok { 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 { } else {
// Если reward_message пустой, используем имя задачи // Если reward_message пустой, используем имя задачи
mainTaskMessage = task.Name mainTaskMessage = task.Name
@@ -7394,13 +7424,7 @@ func (a *App) completeTaskHandler(w http.ResponseWriter, r *http.Request) {
} }
// Подставляем в reward_message подзадачи // Подставляем в reward_message подзадачи
subtaskMessage := subtaskRewardMessage.String subtaskMessage := replaceRewardPlaceholders(subtaskRewardMessage.String, subtaskRewardStrings)
for i := 0; i < 100; i++ {
placeholder := fmt.Sprintf("${%d}", i)
if rewardStr, ok := subtaskRewardStrings[i]; ok {
subtaskMessage = strings.ReplaceAll(subtaskMessage, placeholder, rewardStr)
}
}
subtaskMessages = append(subtaskMessages, subtaskMessage) subtaskMessages = append(subtaskMessages, subtaskMessage)
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "3.0.1", "version": "3.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -268,10 +268,40 @@ function TaskForm({ onNavigate, taskId }) {
const findMaxPlaceholderIndex = (message) => { const findMaxPlaceholderIndex = (message) => {
if (!message) return -1 if (!message) return -1
const matches = message.match(/\$\{(\d+)\}/g) // Находим все варианты плейсхолдеров: ${0}, $0, но не \$0
if (!matches) return -1 const indices = []
const indices = matches.map(m => parseInt(m.match(/\d+/)[0]))
return Math.max(...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" id="reward_message"
value={rewardMessage} value={rewardMessage}
onChange={(e) => setRewardMessage(e.target.value)} onChange={(e) => setRewardMessage(e.target.value)}
placeholder="Используйте ${0}, ${1} для указания проектов" placeholder="Используйте ${0}, $0 для указания проектов (\\$0 для экранирования)"
className="form-textarea" className="form-textarea"
rows={3} rows={3}
/> />