This commit is contained in:
2026-04-03 04:56:32 -03:00
parent 68c640e3ab
commit f7613c9030
2 changed files with 148 additions and 130 deletions

145
cht/ui/markdown.py Normal file
View File

@@ -0,0 +1,145 @@
"""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)