diff --git a/examples/markdown.nex b/examples/markdown.nex new file mode 100644 index 0000000..7ae6848 --- /dev/null +++ b/examples/markdown.nex @@ -0,0 +1,117 @@ + + + h1 Heading 8-) + ============== + + + h2 Heading + ---------- + + + h3 Heading + ---------- + + + h4 Heading + ---------- + + + h5 Heading + ---------- + + + h6 Heading + ---------- + + + Horizontal Rules + ---------------- + + ---------------------------------------------------------------------------- + ---------------------------------------------------------------------------- + ---------------------------------------------------------------------------- + + + Emphasis + -------- + + This is bold text This is bold text This is italic text This is italic text + Strikethrough + + + Blockquotes + ----------- + + | Blockquotes can also be nested... + | | by using additional greater-than signs right next to each other... + | | | or with spaces between arrows. + + + Lists + ----- + + + Unordered + --------- + + + Create a list by starting a line with +, -, or * + - Marker character change forces new list start: + * Ac tristique libero volutpat at + + Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit + + Very easy + + + Ordered + ------- + + 1. Lorem ipsum dolor sit amet + 2. Consectetur adipiscing elit + 3. Integer molestie lorem at massa + + + Code + ---- + + Inline code + + + Indented code + + + 01 | // Some comments + 02 | line 1 of code + 03 | line 2 of code + 04 | line 3 of code + + Block code "fences" + + + 01 | Sample text here... + + Syntax highlighting + + + 01 | var foo = function (bar) { + 02 | return bar++; + 03 | }; + 04 | + 05 | console.log(foo(5)); + + + Links + ----- + + link text (http://dev.nodeca.com) + + + link with title (http://nodeca.github.io/pica/demo/ "title text!") + + + Images + ------ + + Minion (https://octodex.github.com/images/minion.png) Stormtroopocat + (https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + + diff --git a/src/md2txt/renderers/nex.py b/src/md2txt/renderers/nex.py new file mode 100644 index 0000000..ca13079 --- /dev/null +++ b/src/md2txt/renderers/nex.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from functools import partial +from typing import Any, List + +from ..models import BlockStyle, FrontMatter, HeadingPayload +from ..plugins import register_renderer +from .text import ( + BOLD_RE, + CODE_STASH_RE, + IMAGE_RE, + ITALIC_RE, + LINK_RE, + STRIKETHROUGH_RE, + TextRenderer, + UNDERLINE_EM_RE, + UNDERLINE_STRONG_RE, +) + + +class NexRenderer(TextRenderer): + """Render Markdown events as plain text for the Nex protocol. + + Nex has no hypertext markup for hand-authored documents at all (its one + special syntax, `=> URL`, is spec'd for server-auto-generated directory + listings, not document bodies) -- so this renderer's whole job is to + flatten Markdown down to plain, readable text. It reuses TextRenderer's + wrapping/margin/hyphenation/ASCII-art/include machinery unchanged (same + approach as AmaRenderer/MicronRenderer) and only overrides headings (no + FIGlet -- a plain setext-style underline instead, which is a readability + convention borrowed from Markdown itself, not a Nex spec requirement) + and inline formatting (emphasis markers are stripped rather than + translated, and links/images are inlined as "label (url)" plain prose + rather than footnoted or turned into `=> ` lines). + """ + + def __init__(self, frontmatter: FrontMatter, *, width: int = 80, **_: Any) -> None: + super().__init__(width=width, frontmatter=frontmatter) + # Links are inlined by `_replace_link` below, not footnoted. + self.links.clear() + self.link_indices.clear() + + def _render_heading(self, payload: HeadingPayload, style: BlockStyle) -> None: + self._ensure_header_spacing() + processed_text = self._process_inline(payload.text) + underline_char = "=" if payload.level == 1 else "-" + self._emit_render( + partial(self._render_underlined_heading, processed_text, underline_char), + style, + stylable=True, + ) + + def _render_underlined_heading(self, text: str, underline_char: str, style: BlockStyle) -> List[str]: + wrapped = self._wrap_text(text, style=style) + output: List[str] = [] + for line in wrapped: + line = line.rstrip() + if not line.strip(): + output.append("") + continue + leading = len(line) - len(line.lstrip(" ")) + underline = " " * leading + underline_char * len(line.lstrip(" ")) + output.append(line) + output.append(underline) + output.append("") + return output + + # Inline ------------------------------------------------------------- + + def _process_inline(self, text: str) -> str: + code_segments: List[str] = [] + + def stash_code(match) -> str: + code_segments.append(match.group(0)[1:-1]) + return f"\u0000CODE{len(code_segments) - 1}\u0000" + + text = CODE_STASH_RE.sub(stash_code, text) + text = STRIKETHROUGH_RE.sub(lambda m: m.group(1), text) + text = BOLD_RE.sub(lambda m: m.group(1), text) + text = UNDERLINE_STRONG_RE.sub(lambda m: m.group(1), text) + text = ITALIC_RE.sub(lambda m: m.group(1), text) + text = UNDERLINE_EM_RE.sub(lambda m: 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", code) + return text + + @staticmethod + def _replace_link(match) -> str: + label = match.group(1).strip() + url = match.group(2).strip() + if not label or label == url: + return url + return f"{label} ({url})" + + @staticmethod + def _replace_image(match) -> str: + alt = match.group(1).strip() + url = match.group(2).strip() + if not alt: + return url + return f"{alt} ({url})" + + +def _nex_renderer_factory(*, frontmatter: FrontMatter, **options: Any) -> NexRenderer: + width = int(options.get("width", 80)) + return NexRenderer(frontmatter, width=width) + + +try: + register_renderer("nex", _nex_renderer_factory) +except ValueError: + pass