md2txt/micron_renderer.py
2025-10-20 15:55:47 +03:00

119 lines
5.1 KiB
Python

from __future__ import annotations
from functools import partial
from typing import Any, List
from md_types import AsciiArtPayload, BlockQuotePayload, BlockStyle, CodeBlockPayload, FrontMatter, HeadingPayload, ListItemPayload, ParagraphPayload
from plugins import register_renderer
from text_renderer import (
BOLD_RE,
CODE_STASH_RE,
IMAGE_RE,
ITALIC_RE,
LINK_RE,
STRIKETHROUGH_RE,
TextRenderer,
UNDERLINE_EM_RE,
UNDERLINE_STRONG_RE,
)
class MicronRenderer(TextRenderer):
def __init__(self, frontmatter: FrontMatter, *, width: int = 80, **_: Any) -> None:
super().__init__(width, frontmatter)
# Micron format inlines links directly, so suppress link collection overhead.
self.links.clear()
self.link_indices.clear()
def _render_paragraph(self, payload: ParagraphPayload, style: BlockStyle) -> None:
processed = self._process_inline(payload.text)
self._wrap_emit(processed, style, stylable=True, hyphenate=self.hyphenate)
if self.paragraph_spacing > 0:
self.output.extend([""] * self.paragraph_spacing)
def _render_heading(self, payload: HeadingPayload, style: BlockStyle) -> None:
self._ensure_header_spacing()
level = max(1, min(3, payload.level))
marker = ">" * level
line = f"{marker} {self._process_inline(payload.text)}".rstrip()
self._emit_block([line, ""], stylable=False)
def _render_code_block(self, payload: CodeBlockPayload, style: BlockStyle) -> None:
margin_left, _, _ = self._margins(style)
indent = " " * margin_left
body = [f"{indent}`=", *[f"{indent}{line.rstrip()}" for line in payload.lines], f"{indent}`="]
self._emit_block(body, stylable=False)
self.output.append("")
def _render_blockquote(self, payload: BlockQuotePayload, style: BlockStyle) -> None:
processed = self._process_inline(payload.text)
indent = ">>>>" * max(1, payload.depth)
self._wrap_emit(processed, style, initial_indent=indent, subsequent_indent=indent, hyphenate=self.hyphenate)
self.output.append("")
def _render_list_item(self, payload: ListItemPayload, style: BlockStyle) -> None:
base_indent = payload.indent.replace("\t", " ")
marker_indent = " " * self.list_marker_indent
marker = payload.marker
spacing = " " * self.list_text_spacing
initial = f"{base_indent}{marker_indent}{marker}{spacing}"
subsequent = f"{base_indent}{marker_indent}{' ' * len(marker)}{spacing}"
processed = self._process_inline(payload.text)
self._wrap_emit(processed, style, initial_indent=initial, subsequent_indent=subsequent, hyphenate=self.hyphenate)
def _render_custom_block(self, payload: AsciiArtPayload, style: BlockStyle) -> None:
render = partial(self._layout_ascii_pieces, payload.pieces)
self._emit_block(render(style), stylable=True, render_fn=render, style=style)
def _render_horizontal_rule(self, _payload: object, style: BlockStyle) -> None:
margin_left, _, available = self._margins(style)
self._emit_block([" " * margin_left + "-" * available], stylable=False)
# Inline transformations -------------------------------------------------
def _process_inline(self, text: str) -> str:
code_segments: List[str] = []
def stash_code(match):
segment = match.group(0)
code_segments.append(segment[1:-1])
return f"\u0000CODE{len(code_segments) - 1}\u0000"
text = CODE_STASH_RE.sub(stash_code, text)
text = STRIKETHROUGH_RE.sub(lambda m: f"~~{m.group(1)}~~", text) # keep strikethrough literal for now
text = BOLD_RE.sub(lambda m: self._apply_emphasis_spacing(m.string, m.start(), m.end(), f"`!{m.group(1)}`!"), text)
text = ITALIC_RE.sub(lambda m: self._apply_emphasis_spacing(m.string, m.start(), m.end(), f"`*{m.group(1)}`*"), text)
text = UNDERLINE_STRONG_RE.sub(lambda m: self._apply_emphasis_spacing(m.string, m.start(), m.end(), f"`!{m.group(1)}`!"), text)
text = UNDERLINE_EM_RE.sub(lambda m: self._apply_emphasis_spacing(m.string, m.start(), m.end(), f"`*{m.group(1)}`*"), text)
text = LINK_RE.sub(self._replace_link, text)
text = IMAGE_RE.sub(self._replace_image, text)
for idx, code in enumerate(code_segments):
text = text.replace(f"\u0000CODE{idx}\u0000", f"`={code}`=")
return text
@staticmethod
def _replace_link(match) -> str:
label = match.group(1).strip()
url = match.group(2).strip()
if not label:
return url
if label == url or url == f"mailto:{label}":
return f"`[{label}`"
return f"`[{label}`{url}]"
@staticmethod
def _replace_image(match) -> str:
alt = match.group(1).strip()
url = match.group(2).strip()
return f"`[{alt or url}`{url}]"
def _micron_renderer_factory(*, frontmatter: FrontMatter, **options: Any) -> MicronRenderer:
width = int(options.get("width", 80))
return MicronRenderer(frontmatter, width=width)
try:
register_renderer("micron", _micron_renderer_factory)
except ValueError:
pass