nex renderer

This commit is contained in:
randogoth 2026-09-23 09:14:41 +03:00
parent e708dce8f7
commit 67a727ba24
2 changed files with 232 additions and 0 deletions

117
examples/markdown.nex Normal file
View file

@ -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")

115
src/md2txt/renderers/nex.py Normal file
View file

@ -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