"""Markdown → Gtk.TextBuffer renderer. Pure functions — no app state, just TextBuffer + TextIter operations. """ import re from gi.repository import Gtk def setup_tags(buf: Gtk.TextBuffer) -> None: """Create the standard set of text tags used by render().""" buf.create_tag("h1", weight=700, scale=1.4) buf.create_tag("h2", weight=700, scale=1.2) buf.create_tag("h3", weight=700, scale=1.05) buf.create_tag("bold", weight=700) buf.create_tag("italic", style=2) buf.create_tag("code", family="monospace", background="#2a2a2a", foreground="#e8e8e8") buf.create_tag("codeblock", family="monospace", background="#1e1e1e", foreground="#e8e8e8", left_margin=16, right_margin=16, pixels_above_lines=4, pixels_below_lines=4) buf.create_tag("bullet", left_margin=16) def render(buf: Gtk.TextBuffer, insert_iter: Gtk.TextIter, text: str) -> None: """Parse *text* as lightweight Markdown and insert into *buf* at *insert_iter*.""" lines = text.split("\n") i = 0 while i < len(lines): line = lines[i] if line.startswith("```"): lang = line[3:].strip() block_lines = [] i += 1 while i < len(lines) and not lines[i].startswith("```"): block_lines.append(lines[i]) i += 1 code = "\n".join(block_lines) + "\n" block_mark = buf.create_mark(None, insert_iter, True) _insert_highlighted_code(buf, insert_iter, code, lang) buf.apply_tag_by_name("codeblock", buf.get_iter_at_mark(block_mark), insert_iter) buf.delete_mark(block_mark) buf.insert(insert_iter, "\n") i += 1 continue header_match = re.match(r"^(#{1,3})\s+(.*)", line) if header_match: level = len(header_match.group(1)) content = header_match.group(2) tag = f"h{level}" mark = buf.create_mark(None, insert_iter, True) buf.insert(insert_iter, content + "\n") buf.apply_tag_by_name(tag, buf.get_iter_at_mark(mark), insert_iter) i += 1 continue bullet_match = re.match(r"^[\-\*]\s+(.*)", line) if bullet_match: _insert_inline(buf, insert_iter, "• " + bullet_match.group(1), "bullet") buf.insert(insert_iter, "\n") i += 1 continue _insert_inline_line(buf, insert_iter, line) buf.insert(insert_iter, "\n") i += 1 # -- private helpers -- _INLINE_RE = re.compile( r"(\*\*(.+?)\*\*|__(.+?)__|" r"\*(.+?)\*|_(.+?)_|" r"`([^`]+?)`)" ) def _insert_inline(buf, it, text: str, outer_tag: str | None = None): mark = buf.create_mark(None, it, True) _insert_inline_line(buf, it, text) if outer_tag: buf.apply_tag_by_name(outer_tag, buf.get_iter_at_mark(mark), it) def _insert_inline_line(buf, it, text: str): pos = 0 for m in _INLINE_RE.finditer(text): if m.start() > pos: buf.insert(it, text[pos:m.start()]) full = m.group(0) if full.startswith("**") or full.startswith("__"): inner = m.group(2) or m.group(3) mark = buf.create_mark(None, it, True) buf.insert(it, inner) buf.apply_tag_by_name("bold", buf.get_iter_at_mark(mark), it) elif full.startswith("*") or full.startswith("_"): inner = m.group(4) or m.group(5) mark = buf.create_mark(None, it, True) buf.insert(it, inner) buf.apply_tag_by_name("italic", buf.get_iter_at_mark(mark), it) elif full.startswith("`"): inner = m.group(6) mark = buf.create_mark(None, it, True) buf.insert(it, inner) buf.apply_tag_by_name("code", buf.get_iter_at_mark(mark), it) pos = m.end() if pos < len(text): buf.insert(it, text[pos:]) def _insert_highlighted_code(buf, it, code: str, lang: str): try: from pygments.lexers import get_lexer_by_name, guess_lexer from pygments.styles import get_style_by_name except ImportError: buf.insert(it, code) return try: lexer = get_lexer_by_name(lang, stripall=False) if lang else guess_lexer(code) except Exception: try: lexer = guess_lexer(code) except Exception: buf.insert(it, code) return try: style = get_style_by_name("monokai") except Exception: buf.insert(it, code) return tag_table = buf.get_tag_table() for ttype, value in lexer.get_tokens(code): tag_name = f"pyg_{ttype}" tag = tag_table.lookup(tag_name) if tag is None: sf = style.style_for_token(ttype) tag = buf.create_tag(tag_name, family="monospace") if sf.get("color"): tag.set_property("foreground", f"#{sf['color']}") if sf.get("bold"): tag.set_property("weight", 700) if sf.get("italic"): tag.set_property("style", 2) mark = buf.create_mark(None, it, True) buf.insert(it, value) buf.apply_tag(tag, buf.get_iter_at_mark(mark), it) buf.delete_mark(mark)