diff --git a/app.py b/app.py index 5f49eee..1c5a915 100644 --- a/app.py +++ b/app.py @@ -1958,12 +1958,107 @@ def _format_update_date(value: str | None) -> str: return value -def _paragraphs_html(value: str | None) -> str: - lines = _split_lines(value) +def _inline_update_markup(value: str | None) -> str: + """Безопасное мини-форматирование для текста обновлений. + + Поддерживаем в админке: + - `F5` или `1` — выделенная клавиша/команда; + - **важный текст** — зелёное выделение. + Остальной текст обязательно экранируется, чтобы не сломать HTML. + """ + source = _clean_update_text(value) + if not source: + return "" + + token_re = re.compile(r"(`[^`]+`|\*\*.+?\*\*)") + result: list[str] = [] + last_pos = 0 + + for match in token_re.finditer(source): + result.append(html.escape(source[last_pos:match.start()], quote=True)) + token = match.group(0) + + if token.startswith("`") and token.endswith("`"): + inner = token[1:-1].strip() + if inner: + result.append(f'{html.escape(inner, quote=True)}') + elif token.startswith("**") and token.endswith("**"): + inner = token[2:-2].strip() + if inner: + result.append(f'{html.escape(inner, quote=True)}') + + last_pos = match.end() + + result.append(html.escape(source[last_pos:], quote=True)) + return "".join(result) + + +def _flush_update_list(buffer: list[str], list_type: str) -> str: + if not buffer: + return "" + + tag = "ol" if list_type == "numbered" else "ul" + items = "\n".join(f"
Описание не заполнено.
" - return "\n".join(f"{_escape_text(line)}
" for line in lines) + html_parts: list[str] = [] + list_buffer: list[str] = [] + current_list_type = "" + + def close_list() -> None: + nonlocal list_buffer, current_list_type + if list_buffer: + html_parts.append(_flush_update_list(list_buffer, current_list_type)) + list_buffer = [] + current_list_type = "" + + for line in lines: + numbered_match = re.match(r"^\d+[\.)]\s+(.+)$", line) + bullet_match = re.match(r"^[-*•]\s+(.+)$", line) + + if numbered_match: + if current_list_type and current_list_type != "numbered": + close_list() + current_list_type = "numbered" + list_buffer.append(_inline_update_markup(numbered_match.group(1))) + continue + + if bullet_match: + if current_list_type and current_list_type != "bullet": + close_list() + current_list_type = "bullet" + list_buffer.append(_inline_update_markup(bullet_match.group(1))) + continue + + close_list() + html_parts.append(f"{_inline_update_markup(line)}
") + + close_list() + return "\n".join(html_parts) + + +def _paragraphs_html(value: str | None) -> str: + # Оставлено как совместимый алиас: теперь описание умеет списки, клавиши и выделения. + return _rich_update_text_html(value) def _normalize_update_image_path(value: str | None) -> str: @@ -2012,7 +2107,7 @@ def _build_update_block( actions = _split_lines(operator_actions) if actions: - actions_html = "\n".join(f"`F5` — клавиша/команда, **важно** — выделенное слово.
+ 1. Первый пункт или - Первый пункт`F5`, `1`. Выделение слов: **важно**.
+ - Теперь рядом с игроком отображается, если у него нет фотографии для расстановки. + Теперь рядом с игроком отображается у кого нет фотографии для расстановки.
+