from __future__ import annotations import re from html import escape DEFAULT_STATUS_LABELS = { "open": "исполняет роль", "backstage": "в закулисье", "delay": "задержки", "rest": "антракт", } ACTOR_PLACEHOLDER_RE = re.compile(r"\{\{\s*actor\s*:\s*([a-z0-9_\-]+)\s*\}\}", re.IGNORECASE) def build_hidden_link(config: dict) -> str: url = config.get("hidden_link_url", "").strip() if not url: return "" invisible = config.get("hidden_link_char", "​") return f'{invisible}' def build_actor_line(actor: dict, state: dict, config: dict) -> str: actor_state = state.get("actors", {}) status_labels = {**DEFAULT_STATUS_LABELS, **config.get("status_labels", {})} current = actor_state.get(actor["key"], {}) status = current.get("status", actor.get("default_status", "backstage")) phrase = current.get("phrase", actor.get("phrases", {}).get(status, "")) label = status_labels.get(status, status) display_name = actor.get("display_html", escape(actor["display_name"])) meta = actor.get("meta_html", escape(actor["pronouns"])) emoji = actor.get("emoji_html", escape(actor.get("emoji", ""))) line = ( f'{emoji} ' f'{display_name}' f"{meta}{escape(label)}." ) if phrase: line = f"{line}\n {escape(phrase)}" return line def build_actor_fragment(actor: dict, state: dict, config: dict) -> str: actor_state = state.get("actors", {}) status_labels = {**DEFAULT_STATUS_LABELS, **config.get("status_labels", {})} current = actor_state.get(actor["key"], {}) status = current.get("status", actor.get("default_status", "backstage")) phrase = current.get("phrase", actor.get("phrases", {}).get(status, "")) label = status_labels.get(status, status) fragment = f"{escape(label)}." if phrase: fragment = f"{fragment}\n {escape(phrase)}" return fragment def build_actor_lines(config: dict, state: dict, skip_keys: set[str] | None = None) -> str: actor_lines: list[str] = [] skip_keys = skip_keys or set() for actor in config["actors"]: if actor["key"] in skip_keys: continue actor_lines.append(build_actor_line(actor, state, config)) actor_lines.extend(config.get("static_actor_lines_html", [])) return "\n".join(actor_lines) def replace_actor_placeholders(template: str, config: dict, state: dict) -> tuple[str, set[str]]: used_keys: set[str] = set() actors_by_key = {actor["key"].lower(): actor for actor in config["actors"]} def repl(match: re.Match[str]) -> str: actor_key = match.group(1).lower() actor = actors_by_key.get(actor_key) if actor is None: return match.group(0) used_keys.add(actor["key"]) line_start = template.rfind("\n", 0, match.start()) + 1 line_end = template.find("\n", match.end()) if line_end == -1: line_end = len(template) line_text = template[line_start:line_end] if line_text.strip().lower() == match.group(0).strip().lower(): return build_actor_line(actor, state, config) return build_actor_fragment(actor, state, config) return ACTOR_PLACEHOLDER_RE.sub(repl, template), used_keys def build_default_template(config: dict) -> str: blocks = [] hidden = build_hidden_link(config) if hidden: blocks.append(hidden) for key in ("header_html", "intro_links_html", "projects_block_html", "actors_title_html"): value = config.get(key, "").strip() if value: blocks.append(value) blocks.append("{{actors}}") for key in ("legend_html", "footer_html"): value = config.get(key, "").strip() if value: blocks.append(value) return "\n\n".join(blocks) def build_channel_text(config: dict, state: dict) -> str: template = state.get("template", {}).get("text") or config.get("template_text") or build_default_template(config) template, used_keys = replace_actor_placeholders(template, config, state) actors_block = build_actor_lines(config, state, skip_keys=used_keys) hidden_link = build_hidden_link(config) text = template.replace("{{actors}}", actors_block) text = text.replace("{{hidden_link}}", hidden_link) if "{{hidden_link}}" not in template and hidden_link: text = f"{hidden_link}\n{text}" return text