from marko.block import BlockElement from marko.source import Source from re import Match, compile as re_compile _LISTING_FLAG_RE = re_compile(r"(?i)(?:^|\s)\+?listing\b") _LANDSCAPE_FLAG_RE = re_compile(r"(?i)(?:^|\s)\+?landscape\b") def strip_caption_flags(raw: str) -> tuple[str | None, bool, bool]: """Return (clean_text, with_listing, landscape) from caption / image-title tail.""" text = (raw or "").strip() with_listing = bool(_LISTING_FLAG_RE.search(text)) landscape = bool(_LANDSCAPE_FLAG_RE.search(text)) if with_listing: text = _LISTING_FLAG_RE.sub(" ", text) if landscape: text = _LANDSCAPE_FLAG_RE.sub(" ", text) text = " ".join(text.split()).strip() return (text or None), with_listing, landscape class Caption(BlockElement): """Represents caption element Syntax: %label Caption text [+listing] [+landscape] """ # Interrupt an open paragraph so «текст:\\n%id …» still becomes a Caption # (otherwise marko treats the % line as paragraph continuation / plain text). breaks_paragraph = True priority = 6 pattern = r"\%(\w+)( (.+))?" def __init__(self, match: Match[str]): self.unique_name = match.group(1) raw = (match.group(3) or "").strip() self.text, self.with_listing, self.landscape = strip_caption_flags(raw) @classmethod def match(cls, source: Source) -> Match[str] | None: return source.expect_re(cls.pattern) @classmethod def parse(cls, source: Source) -> Match[str] | None: m = source.match source.consume() return m