diff --git a/bot/utils/formatters.py b/bot/utils/formatters.py index 69ce7ca..8d78fcf 100644 --- a/bot/utils/formatters.py +++ b/bot/utils/formatters.py @@ -73,13 +73,18 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple 2. Стараемся не разрывать блоки кода 3. Если блок кода не влезает — отправляем отдельным сообщением без Markdown """ + def calc_code_flags(txt: str) -> Tuple[bool, bool, bool]: + """Вычислить флаги code для данного текста.""" + has_code = '```' in txt + backtick_count = txt.count('```') + # code_opened: есть хотя бы один ``` + code_opened = backtick_count >= 1 + # code_closed: есть хотя бы 2 ``` (пара) или нечётное количество (открыт и закрыт в одном) + code_closed = backtick_count >= 2 + return has_code, code_opened, code_closed + if len(text) <= max_length: - has_code = '```' in text - # Считаем количество ``` для определения открыт/закрыт - backtick_count = text.count('```') - # Если 1 или нечётное количество — блок открыт но не закрыт (или наоборот) - code_opened = backtick_count >= 1 and (backtick_count % 2 == 1) - code_closed = backtick_count >= 2 and (backtick_count % 2 == 0) + has_code, code_opened, code_closed = calc_code_flags(text) return [(text, has_code, code_opened, code_closed)] parts = [] @@ -88,9 +93,6 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple # Текущая позиция в тексте pos = 0 current = "" - current_has_code = False - current_code_opened = False - current_code_closed = False for block_start, block_end in code_blocks: # Текст до блока кода @@ -103,11 +105,9 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple for line in before_code.split('\n'): if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER: if current: - parts.append((current, current_has_code, current_code_opened, current_code_closed)) + has_code, code_opened, code_closed = calc_code_flags(current) + parts.append((current, has_code, code_opened, code_closed)) current = line - current_has_code = False - current_code_opened = False - current_code_closed = False else: current += ('\n' if current else '') + line @@ -115,33 +115,51 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple if len(current) + len(code_block) + 1 > max_length - RESERVED_FOR_HEADER: # Отправляем что накопилось if current: - parts.append((current, current_has_code, current_code_opened, current_code_closed)) + has_code, code_opened, code_closed = calc_code_flags(current) + parts.append((current, has_code, code_opened, code_closed)) # Если блок кода слишком длинный — режем его на части if len(code_block) > max_length - RESERVED_FOR_HEADER: - # Отправляем блок кода частями без Markdown - for i in range(0, len(code_block), max_length - RESERVED_FOR_HEADER): - chunk = code_block[i:i + max_length - RESERVED_FOR_HEADER] - # Первая часть имеет открывающий ```, последняя — закрывающий - first_chunk = (i == 0) - last_chunk = (i + max_length - RESERVED_FOR_HEADER >= len(code_block)) - parts.append((chunk, True, first_chunk, last_chunk)) + # Блок кода включает ```, нужно резать правильно + # block_start и block_end включают оба ``` + # Находим позицию открывающего и закрывающего ``` + open_end = code_block.find('```', 3) + 3 # Конец открывающего ``` + close_start = code_block.rfind('```') # Начало закрывающего ``` + + # Содержимое блока без ``` + code_content = code_block[3:close_start] # Только содержимое + + # Режем содержимое на части + content_max_len = max_length - RESERVED_FOR_HEADER - 3 # -3 для ``` + chunks = [] + for i in range(0, len(code_content), content_max_len): + chunks.append(code_content[i:i + content_max_len]) + + # Создаём части с правильными ``` + for i, chunk in enumerate(chunks): + if i == 0 and len(chunks) == 1: + # Один чанк — полный блок + full_block = f"```{chunk}```" + parts.append((full_block, True, True, True)) + elif i == 0: + # Первая часть — только открывающий ``` + first_part = f"```{chunk}" + parts.append((first_part, True, True, False)) + elif i == len(chunks) - 1: + # Последняя часть — только закрывающий ``` + last_part = f"{chunk}```" + parts.append((last_part, True, False, True)) + else: + # Средняя часть — без ``` + parts.append((chunk, False, False, False)) + current = "" - current_has_code = False - current_code_opened = False - current_code_closed = False else: # Блок влезает в следующее сообщение current = code_block - current_has_code = True - current_code_opened = True # Блок начинается с ``` - current_code_closed = True # Блок заканчивается на ``` else: # Блок кода влезает в текущее сообщение current += ('\n' if current else '') + code_block - current_has_code = True - current_code_opened = True # Блок содержит открывающий ``` - current_code_closed = True # Блок содержит закрывающий ``` pos = block_end @@ -151,16 +169,15 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple for line in remaining.split('\n'): if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER: if current: - parts.append((current, current_has_code, current_code_opened, current_code_closed)) + has_code, code_opened, code_closed = calc_code_flags(current) + parts.append((current, has_code, code_opened, code_closed)) current = line - current_has_code = False - current_code_opened = False - current_code_closed = False else: current += ('\n' if current else '') + line if current: - parts.append((current, current_has_code, current_code_opened, current_code_closed)) + has_code, code_opened, code_closed = calc_code_flags(current) + parts.append((current, has_code, code_opened, code_closed)) return parts @@ -191,19 +208,31 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p for i, (part, has_code, code_opened, code_closed) in enumerate(parts): # Определяем parse_mode для этого сообщения - actual_parse_mode = parse_mode if parse_mode and has_code else None + actual_parse_mode = parse_mode if parse_mode and (has_code or code_opened or code_closed) else None # Логика работы с блоками кода между сообщениями: - # - Если предыдущее сообщение не закрыло блок — открываем в этом - # - Если текущее не закрывает блок и следующее не имеет кода — закрываем - if total > 1 and actual_parse_mode and has_code: + need_prepend = False + need_append = False + + if total > 1 and actual_parse_mode: # Проверяем нужно ли открыть блок в начале этого сообщения if i > 0 and not parts[i-1][3]: # предыдущее не закрыло блок (parts[i-1][3] = code_closed) - part = "```\n" + part + need_prepend = True # Проверяем нужно ли закрыть блок в конце этого сообщения - if i < total - 1 and not code_closed and not parts[i+1][1]: # следующее не имеет кода - part = part + "\n```" + # Если текущее не закрыло блок и следующее не имеет кода/не закроет + if i < total - 1 and not code_closed: + next_has = parts[i+1][1] + next_opened = parts[i+1][2] + # Закрываем если следующее не имеет кода или не открывает свой блок + if not next_has: + need_append = True + + # Применяем модификации + if need_prepend: + part = "```\n" + part + if need_append: + part = part + "\n```" # Добавляем номер части ПОСЛЕ работы с блоками кода if total > 1: