from __future__ import annotations import re from collections.abc import Awaitable, Callable from dataclasses import dataclass from enum import StrEnum DEFAULT_SILENT_TOKEN = "🔇" _REPLY_TARGET_RE = re.compile(r"\[\[([^\[\]]+)\]\]") _MEDIA_RE = re.compile(r"MEDIA:(\S+)") _MULTI_SPACE_RE = re.compile(r" {2,}") class DirectiveType(StrEnum): REPLY_TARGET = "reply_target" MEDIA = "media" SILENT = "silent" @dataclass class StreamingDirective: type: DirectiveType value: str raw: str index: int = 0 DirectiveHandler = Callable[[StreamingDirective], Awaitable[None]] class StreamingDirectiveParser: def __init__(self, silent_token: str = DEFAULT_SILENT_TOKEN): self._silent_re = re.compile(re.escape(silent_token)) self._emitted: set[tuple[str, str]] = set() def feed(self, text: str) -> tuple[str, list[StreamingDirective]]: """Parse directives from a single text chunk. Designed for incremental chunk-by-chunk processing in streaming scenarios. Each call scans only the provided *text* (a stream delta), not the accumulated full content. Directives are deduplicated across calls by (type, value) — the same directive value of the same type is emitted at most once per parser lifetime. """ directives = self._find_all(text) new_directives: list[StreamingDirective] = [] for d in directives: key = (d.type.value, d.value) if key not in self._emitted: self._emitted.add(key) new_directives.append(d) clean = self._remove_directives(text, directives) clean = _MULTI_SPACE_RE.sub(" ", clean).strip() return clean, new_directives def reset(self) -> None: self._emitted.clear() @property def emitted_count(self) -> int: return len(self._emitted) def _find_all(self, text: str) -> list[StreamingDirective]: directives: list[StreamingDirective] = [] for match in _REPLY_TARGET_RE.finditer(text): value = match.group(1).strip() if value: directives.append( StreamingDirective( type=DirectiveType.REPLY_TARGET, value=value, raw=match.group(0), index=match.start(), ) ) for match in _MEDIA_RE.finditer(text): directives.append( StreamingDirective( type=DirectiveType.MEDIA, value=match.group(1), raw=match.group(0), index=match.start(), ) ) for match in self._silent_re.finditer(text): directives.append( StreamingDirective( type=DirectiveType.SILENT, value="", raw=match.group(0), index=match.start(), ) ) return directives @staticmethod def _remove_directives(text: str, directives: list[StreamingDirective]) -> str: if not directives: return text result = text for d in sorted(directives, key=lambda x: -x.index): if d.index < 0 or d.index > len(result): continue if d.index + len(d.raw) > len(result): continue result = result[: d.index] + result[d.index + len(d.raw):] return result