micron renderer
This commit is contained in:
parent
03cab97dac
commit
5e0e4b701e
5 changed files with 742 additions and 104 deletions
|
|
@ -5,7 +5,7 @@ This repository contains command line helpers for transforming Markdown into for
|
|||
## Tools
|
||||
|
||||
- `md2amb.py` – converts Markdown into Amber-screen formatted text (see script for details).
|
||||
- `md2txt.py` – converts Markdown into 80-column, DOS-compatible plain text with extensive formatting support. It ships with the default `markdown` parser and `text` renderer plugins and exposes the core pipeline so you can add your own parser or renderer modules:
|
||||
- `md2txt.py` – converts Markdown into 80-column, DOS-compatible plain text with extensive formatting support. It ships with the default `markdown` parser and `text` renderer plugins, registers an optional `micron` renderer for Micron-formatted output, and exposes the core pipeline so you can add your own parser or renderer modules:
|
||||
- FIGlet-rendered headings (H1–H3) driven by optional YAML frontmatter (`h1_font`, `h2_font`, `h3_font`).
|
||||
- H4+ headings rendered in uppercase with dashed underlines.
|
||||
- Emphasis styles converted to spaced or delimited characters, e.g. `**bold**` → `B O L D`, `__strong__` → `_s_t_r_o_n_g_`, `~~strike~~` → `~s~t~r~i~k~e~`.
|
||||
|
|
@ -29,7 +29,7 @@ This repository contains command line helpers for transforming Markdown into for
|
|||
python md2txt.py input.md -o output.txt # convert to DOS-friendly text
|
||||
python md2txt.py input.md # write result to stdout
|
||||
python md2txt.py input.md --width 72 # override column width
|
||||
python md2txt.py input.md --parser markdown --renderer text # explicitly select defaults
|
||||
python md2txt.py input.md --parser markdown --renderer micron # emit Micron-formatted output
|
||||
python md2txt.py input.md --renderer-option width=68 # pass KEY=VALUE to a renderer
|
||||
```
|
||||
|
||||
|
|
|
|||
237
loremipsum.mu
Normal file
237
loremipsum.mu
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
|
||||
> Salientia
|
||||
|
||||
>> Structa hinc vetitus factum
|
||||
|
||||
Lorem markdownum omnes tellus, in, Herculeis
|
||||
`=reciprocalPower`= neque promissa est Latiae caput
|
||||
umero talia fidissima, nymphae. Foedera et sagittas
|
||||
tenetur adde matri esse: talia manibus.
|
||||
|
||||
>> Fuit video longique dedisse
|
||||
|
||||
Ille stupet, ultra `!incunabula agendum urbes`! mirer
|
||||
`!aper`!, metu. Aegides novissima sunt turis quatiebant
|
||||
umbrosaque `[putares`#mille-parva]. Metuunt eruiturque
|
||||
maximus velit, ater Vesta vulnus rustica verba! Mei
|
||||
vincite nulla, in Iovem: fama exurunt vernum!
|
||||
|
||||
De instat ubi induit fugientia inertes columbas tarda
|
||||
`*terga Parnasia sententia`* omnia inmotusque, est et
|
||||
aetas sonus. Exhibuit coeperunt, canis. Suis minora,
|
||||
tempore fateor neve moderamine erit. Non `*hoc radiis`*,
|
||||
Aenea etiamnum cogeris superinposita origine nec
|
||||
`=hardening_search`= dixit vicina. Heros subito
|
||||
populifer adeunt quicquam cum `!ales`! Ixion angulus
|
||||
placet reddere.
|
||||
|
||||
>> Tractaque serpens arva quoque
|
||||
|
||||
> Include
|
||||
|
||||
<>=======()
|
||||
(/\___ /|\\ ()==========<>_
|
||||
\_/ | \\ //|\ ______/ \)
|
||||
\_| \\ // | \_/
|
||||
\|\/|\_ // /\/
|
||||
(oo)\ \_// /
|
||||
//_/\_\/ / |
|
||||
@@/ |=\ \ |
|
||||
\_=\_ \ |
|
||||
\==\ \|\_ snd
|
||||
__(\===\( )\
|
||||
(((~) __(_/ |
|
||||
(((~) \ /
|
||||
______/ /
|
||||
'------'
|
||||
<>=======()
|
||||
(/\___ /|\\ ()==========<>_
|
||||
\_/ | \\ //|\ ______/ \)
|
||||
\_| \\ // | \_/
|
||||
\|\/|\_ // /\/
|
||||
(oo)\ \_// /
|
||||
//_/\_\/ / |
|
||||
@@/ |=\ \ |
|
||||
\_=\_ \ |
|
||||
\==\ \|\_ snd
|
||||
__(\===\( )\
|
||||
(((~) __(_/ |
|
||||
(((~) \ /
|
||||
______/ /
|
||||
'------'
|
||||
<>=======()
|
||||
(/\___ /|\\ ()==========<>_
|
||||
\_/ | \\ //|\ ______/ \)
|
||||
\_| \\ // | \_/
|
||||
\|\/|\_ // /\/
|
||||
(oo)\ \_// /
|
||||
//_/\_\/ / |
|
||||
@@/ |=\ \ |
|
||||
\_=\_ \ |
|
||||
\==\ \|\_ snd
|
||||
__(\===\( )\
|
||||
(((~) __(_/ |
|
||||
(((~) \ /
|
||||
______/ /
|
||||
'------'
|
||||
|
||||
> 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
|
||||
|
||||
`=
|
||||
// Some comments
|
||||
line 1 of code
|
||||
line 2 of code
|
||||
line 3 of code
|
||||
`=
|
||||
|
||||
Block code "fences"
|
||||
|
||||
`=
|
||||
Sample text here...
|
||||
`=
|
||||
|
||||
Syntax highlighting
|
||||
|
||||
`=
|
||||
var foo = function (bar) {
|
||||
return bar++;
|
||||
};
|
||||
|
||||
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/stormt
|
||||
roopocat.jpg "The Stormtroopocat"]
|
||||
|
||||
Inprudens dum memor alma, casses dedi Sinuessa iam quid
|
||||
adicit `=gate_truncate_wave`=. `[Geratur de`#salientia-
|
||||
iungit-contra-et], insolida temptat `=open`= carentia;
|
||||
genitorem solet, potens. Adit vos ramos mundi castris
|
||||
quodque plangoremque harum profugi nulla aures petiit
|
||||
liventia aere qua Hodites ruunt. Achaidos dilexisse
|
||||
`=redundancy`= praesagia tenuatus necis `!Capys
|
||||
recordor`! tenebras actis signis, reliquit, placidissime
|
||||
iram liquefactis longe nervi famem. Cura pars natique
|
||||
velleris semel et at `=printerWildcardParallel`= inquit
|
||||
positosque comites ultra.
|
||||
|
||||
>> Quantaque augent
|
||||
|
||||
Acuti ignotis ignis, ostendit nostras terras nondum,
|
||||
`[petit utque utero`#structa-hinc-vetitus-factum]
|
||||
sepulcrales, pluma reseratis Ismario. Tria silentia duae
|
||||
`*nymphe`*: certe parva cernam et dixit. `[Verborum
|
||||
sedisti`#mille-parva].
|
||||
|
||||
`=
|
||||
if (firewall(4, 3) == 1 * device_media_parity) {
|
||||
system_copy_class += systray + oasis_wi_clean + lpi;
|
||||
modelLdap.search_secondary_socket *= bounce.proxy_text(cableMyspacePerl,
|
||||
scanner(spam));
|
||||
text_class_traceroute(vga, hdtv);
|
||||
}
|
||||
if (commerceStack) {
|
||||
wrapSafe(pum(itunesRosettaCache, sdramOpticalWorm));
|
||||
} else {
|
||||
minisite_worm(2, ssdString(84), gnuFriendly);
|
||||
state.qwerty(moduleJavascriptFloating + ugc, koffice,
|
||||
mail_system.prebinding_service_teraflops(mediaIcmpBoot));
|
||||
}
|
||||
memoryHexadecimalMultitasking += oop_android;
|
||||
var passive_vdu = bar;
|
||||
var ddrJava = leaf(unc_web);
|
||||
`=
|
||||
|
||||
>>> Mille parva
|
||||
|
||||
`!Minetur et`! aurum! Charaxi sanguine quo; vires
|
||||
subnectite `!pignora`!, omnique hostibus loquerentur
|
||||
aquae Solis ferox? `!Cum ne`!, putat utar ex ad aede
|
||||
mane. Vitalesque geminas exstimulat, iam vivunt dat
|
||||
artes et quem! Iam violas molire ipse, spoliabitur dente
|
||||
nervis regis, factos quae.
|
||||
|
||||
>>>>Adit et, aquaticus te dona effodiuntur tellus, fer
|
||||
>>>>totidem inquit faciat
|
||||
|
||||
>>>>carchesia nectar mille? Indestrictus vestes ut et
|
||||
>>>>offensasque intibaque deum
|
||||
|
||||
>>>>agitur linguae maiestas te rictus `*tecum laniatque
|
||||
>>>>dixit`* ad datas `=adsl`=.
|
||||
|
||||
>>>>Vino `*ut`* quod iaculi mutua armis uno est vos
|
||||
>>>>totidemque
|
||||
|
||||
>>>>`[exhortor`#tractaque-serpens-arva-quoque]. Iamdudum
|
||||
>>>>Romanae cepi linguisque
|
||||
|
||||
>>>>velit accipe idem acutis hoc sparsumque dextra.
|
||||
>>>>Minoide sponte collo sequitur
|
||||
|
||||
>>>>et intus flere sanguineaque herba, nox Quas adsiduo
|
||||
>>>>`*cunctantem`* tibi; enim.
|
||||
99
md2mu.py
99
md2mu.py
|
|
@ -1,99 +0,0 @@
|
|||
from mistune.core import BlockState
|
||||
from mistune.util import strip_end
|
||||
from mistune.renderers._list import render_list
|
||||
from mistune.renderers.markdown import MarkdownRenderer
|
||||
from typing import Dict, Any
|
||||
from textwrap import indent
|
||||
|
||||
class MicronRenderer(MarkdownRenderer):
|
||||
"""A renderer to format Micron text."""
|
||||
NAME = 'micron'
|
||||
|
||||
def __call__(self, tokens, state: BlockState):
|
||||
out = self.render_tokens(tokens, state)
|
||||
# special handle for line breaks
|
||||
out += '\n\n'.join(self.render_referrences(state)) + '\n'
|
||||
return strip_end(out)
|
||||
|
||||
def render_children(self, token, state: BlockState):
|
||||
children = token['children']
|
||||
return self.render_tokens(children, state)
|
||||
|
||||
def text(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return token['raw']
|
||||
|
||||
def emphasis(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return '`*' + self.render_children(token, state) + '`*'
|
||||
|
||||
def strong(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return '`!' + self.render_children(token, state) + '`!'
|
||||
|
||||
def link(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
label = token.get('label')
|
||||
text = self.render_children(token, state)
|
||||
out = '`[' + text + '`'
|
||||
if label:
|
||||
return out + '`[' + label + '`'
|
||||
attrs = token['attrs']
|
||||
url = attrs['url']
|
||||
if text == url:
|
||||
return '`[' + text + '`'
|
||||
elif 'mailto:' + text == url:
|
||||
return '`[' + text + '`'
|
||||
out += url
|
||||
return out + ']'
|
||||
|
||||
def image(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return self.link(token, state)
|
||||
|
||||
def codespan(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return '`=' + token['raw'] + '`='
|
||||
|
||||
def linebreak(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return ' \n'
|
||||
|
||||
def softbreak(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return '\n'
|
||||
|
||||
def blank_line(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return ''
|
||||
|
||||
def inline_html(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return ''
|
||||
|
||||
def paragraph(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
text = self.render_children(token, state)
|
||||
return text + '\n\n'
|
||||
|
||||
def heading(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
level = token['attrs']['level']
|
||||
if level > 3:
|
||||
level = 3
|
||||
marker = '>' * level
|
||||
text = self.render_children(token, state)
|
||||
return marker + ' ' + text + '\n\n'
|
||||
def thematic_break(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return '-\n\n'
|
||||
|
||||
def block_text(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return self.render_children(token, state) + '\n'
|
||||
|
||||
def block_code(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
code = token['raw']
|
||||
if code and code[-1] != '\n':
|
||||
code += '\n'
|
||||
marker = '`='
|
||||
return marker + '\n' + code + marker + '\n\n'
|
||||
|
||||
def block_quote(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
text = indent(self.render_children(token, state), '>>>>')
|
||||
return text + '\n\n'
|
||||
|
||||
def block_html(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return ''
|
||||
|
||||
def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return ''
|
||||
|
||||
def list(self, token: Dict[str, Any], state: BlockState) -> str:
|
||||
return render_list(self, token, state)
|
||||
10
md2txt.py
10
md2txt.py
|
|
@ -22,6 +22,8 @@ from plugins import (
|
|||
)
|
||||
from text_renderer import TextRenderer
|
||||
|
||||
import micron_renderer # noqa: F401 # register micron renderer plugin
|
||||
|
||||
|
||||
def _split_option(token: str) -> Tuple[str, str]:
|
||||
if "=" not in token:
|
||||
|
|
@ -77,9 +79,13 @@ def convert_markdown(
|
|||
renderer_options=effective_renderer_options,
|
||||
base_path=base_path,
|
||||
)
|
||||
if not isinstance(rendered, list):
|
||||
raise TypeError("Renderer plugin returned unsupported output for md2txt CLI.")
|
||||
if isinstance(rendered, list):
|
||||
return rendered
|
||||
if isinstance(rendered, tuple):
|
||||
return list(rendered)
|
||||
if isinstance(rendered, str):
|
||||
return rendered.splitlines()
|
||||
raise TypeError("Renderer plugin returned unsupported output for md2txt CLI.")
|
||||
|
||||
|
||||
def write_output(path: Optional[Path], lines: List[str]) -> None:
|
||||
|
|
|
|||
494
micron_renderer.py
Normal file
494
micron_renderer.py
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from md_types import (
|
||||
AsciiArtPayload,
|
||||
AsciiArtPiece,
|
||||
BlockEvent,
|
||||
BlockKind,
|
||||
BlockQuotePayload,
|
||||
BlockStyle,
|
||||
CodeBlockPayload,
|
||||
FrontMatter,
|
||||
HeadingPayload,
|
||||
ListItemPayload,
|
||||
ParagraphPayload,
|
||||
StyleSpec,
|
||||
StyleUpdateEvent,
|
||||
)
|
||||
from plugins import register_renderer
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StylableBlock:
|
||||
index: int
|
||||
render: Callable[[BlockStyle], str]
|
||||
style: BlockStyle
|
||||
|
||||
|
||||
class MicronRenderer:
|
||||
def __init__(self, frontmatter: FrontMatter, *, width: int = 80, **_: Any) -> None:
|
||||
self.frontmatter = frontmatter
|
||||
self.width = max(1, width)
|
||||
self._chunks: List[str] = []
|
||||
self._trailing_newlines = 0
|
||||
self._last_stylable_block: Optional[_StylableBlock] = None
|
||||
|
||||
def handle_event(self, event: BlockEvent | StyleUpdateEvent) -> None:
|
||||
if isinstance(event, BlockEvent):
|
||||
self._handle_block_event(event)
|
||||
else:
|
||||
self._apply_style_update(event.spec)
|
||||
|
||||
def finalize(self) -> str:
|
||||
return "".join(self._chunks).rstrip()
|
||||
|
||||
def _handle_block_event(self, event: BlockEvent) -> None:
|
||||
if event.kind is BlockKind.PARAGRAPH:
|
||||
self._render_paragraph(event.payload, event.style) # type: ignore[arg-type]
|
||||
elif event.kind is BlockKind.HEADING:
|
||||
self._render_heading(event.payload, event.style) # type: ignore[arg-type]
|
||||
elif event.kind is BlockKind.CODE_BLOCK:
|
||||
self._render_code_block(event.payload, event.style) # type: ignore[arg-type]
|
||||
elif event.kind is BlockKind.BLOCKQUOTE:
|
||||
self._render_blockquote(event.payload, event.style) # type: ignore[arg-type]
|
||||
elif event.kind is BlockKind.LIST_ITEM:
|
||||
self._render_list_item(event.payload, event.style) # type: ignore[arg-type]
|
||||
elif event.kind is BlockKind.HORIZONTAL_RULE:
|
||||
self._render_horizontal_rule(event.style)
|
||||
elif event.kind is BlockKind.BLANK_LINE:
|
||||
self._ensure_newlines(1)
|
||||
elif event.kind is BlockKind.CUSTOM_BLOCK:
|
||||
self._render_custom_block(event.payload, event.style) # type: ignore[arg-type]
|
||||
|
||||
def _render_paragraph(self, payload: ParagraphPayload, style: BlockStyle) -> None:
|
||||
content = self._render_inline(payload.text)
|
||||
if content:
|
||||
def render(target_style: BlockStyle) -> str:
|
||||
lines = self._wrap_text(content, target_style)
|
||||
return "\n".join(lines)
|
||||
|
||||
block = render(style)
|
||||
self._emit_block(block, stylable=True, render_fn=render, style=style)
|
||||
self._ensure_newlines(2)
|
||||
|
||||
def _render_heading(self, payload: HeadingPayload, style: BlockStyle) -> None:
|
||||
level = max(1, min(3, payload.level))
|
||||
marker = ">" * level + " "
|
||||
text = self._render_inline(payload.text)
|
||||
if text:
|
||||
def render(target_style: BlockStyle) -> str:
|
||||
lines = self._wrap_text(text, target_style, initial_prefix=marker)
|
||||
return "\n".join(lines)
|
||||
|
||||
block = render(style)
|
||||
self._emit_block(block, stylable=True, render_fn=render, style=style)
|
||||
self._ensure_newlines(2)
|
||||
|
||||
def _render_code_block(self, payload: CodeBlockPayload, style: BlockStyle) -> None:
|
||||
def render(target_style: BlockStyle) -> str:
|
||||
margin = " " * max(0, target_style.margin_left)
|
||||
fence = margin + "`="
|
||||
body_lines = [margin + line.rstrip("\n") for line in payload.lines]
|
||||
return "\n".join([fence, *body_lines, fence])
|
||||
|
||||
block = render(style)
|
||||
if block:
|
||||
self._emit_block(block, stylable=False)
|
||||
self._ensure_newlines(2)
|
||||
|
||||
def _render_blockquote(self, payload: BlockQuotePayload, style: BlockStyle) -> None:
|
||||
prefix = ">>>>" * max(1, payload.depth)
|
||||
text = self._render_inline(payload.text)
|
||||
if text:
|
||||
def render(target_style: BlockStyle) -> str:
|
||||
lines = self._wrap_text(
|
||||
text,
|
||||
target_style,
|
||||
initial_prefix=prefix,
|
||||
subsequent_prefix=prefix,
|
||||
align_blocks=False,
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
block = render(style)
|
||||
self._emit_block(block, stylable=False)
|
||||
self._ensure_newlines(2)
|
||||
|
||||
def _render_list_item(self, payload: ListItemPayload, style: BlockStyle) -> None:
|
||||
indent_text = payload.indent.replace("\t", " ")
|
||||
spacing = payload.spacing if payload.spacing else " "
|
||||
initial_prefix = f"{indent_text}{payload.marker}{spacing}"
|
||||
subsequent_prefix = f"{indent_text}{' ' * len(payload.marker)}{spacing}"
|
||||
text = self._render_inline(payload.text)
|
||||
if text:
|
||||
def render(target_style: BlockStyle) -> str:
|
||||
lines = self._wrap_text(
|
||||
text,
|
||||
target_style,
|
||||
initial_prefix=initial_prefix,
|
||||
subsequent_prefix=subsequent_prefix,
|
||||
align_blocks=False,
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
block = render(style)
|
||||
self._emit_block(block, stylable=False)
|
||||
self._ensure_newlines(1)
|
||||
|
||||
def _render_horizontal_rule(self, style: BlockStyle) -> None:
|
||||
margin = " " * max(0, style.margin_left)
|
||||
self._emit_block(margin + "-", stylable=False)
|
||||
self._ensure_newlines(2)
|
||||
|
||||
def _render_custom_block(self, payload: AsciiArtPayload, style: BlockStyle) -> None:
|
||||
def render(target_style: BlockStyle) -> str:
|
||||
lines = self._layout_ascii_pieces(payload.pieces, target_style)
|
||||
return "\n".join(lines)
|
||||
|
||||
block = render(style)
|
||||
if block:
|
||||
self._emit_block(block, stylable=True, render_fn=render, style=style)
|
||||
self._ensure_newlines(2)
|
||||
|
||||
def _render_inline(self, text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
code_segments: List[str] = []
|
||||
emphasis_segments: List[str] = []
|
||||
|
||||
def stash_code(match: re.Match[str]) -> str:
|
||||
placeholder = f"\u0000CODE{len(code_segments)}\u0000"
|
||||
code_segments.append(match.group(1))
|
||||
return placeholder
|
||||
|
||||
def stash_emphasis(content: str) -> str:
|
||||
placeholder = f"\u0000EMP{len(emphasis_segments)}\u0000"
|
||||
emphasis_segments.append(content)
|
||||
return placeholder
|
||||
|
||||
text = re.sub(r"`([^`]+)`", stash_code, text)
|
||||
|
||||
text = re.sub(
|
||||
r"\*\*(.+?)\*\*",
|
||||
lambda m: stash_emphasis(f"`!{m.group(1)}`!"),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"__(.+?)__",
|
||||
lambda m: stash_emphasis(f"`!{m.group(1)}`!"),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)",
|
||||
lambda m: stash_emphasis(f"`*{m.group(1)}`*"),
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"(?<!_)_(?!_)(.+?)(?<!_)_(?!_)",
|
||||
lambda m: stash_emphasis(f"`*{m.group(1)}`*"),
|
||||
text,
|
||||
)
|
||||
|
||||
text = re.sub(
|
||||
r"!\[([^\]]*)\]\(([^)]+)\)",
|
||||
self._replace_image,
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"(?<!\!)\[([^\]]+)\]\(([^)]+)\)",
|
||||
self._replace_link,
|
||||
text,
|
||||
)
|
||||
|
||||
for index, replacement in enumerate(emphasis_segments):
|
||||
text = text.replace(f"\u0000EMP{index}\u0000", replacement)
|
||||
for index, code in enumerate(code_segments):
|
||||
text = text.replace(f"\u0000CODE{index}\u0000", f"`={code}`=")
|
||||
|
||||
return text
|
||||
|
||||
def _wrap_text(
|
||||
self,
|
||||
text: str,
|
||||
style: BlockStyle,
|
||||
*,
|
||||
initial_prefix: str = "",
|
||||
subsequent_prefix: Optional[str] = None,
|
||||
align_blocks: bool = True,
|
||||
) -> List[str]:
|
||||
if not text:
|
||||
return []
|
||||
|
||||
margin_left = max(0, style.margin_left)
|
||||
margin_right = max(0, style.margin_right)
|
||||
available = max(1, self.width - margin_left - margin_right)
|
||||
prefix_first = initial_prefix
|
||||
prefix_rest = subsequent_prefix if subsequent_prefix is not None else initial_prefix
|
||||
|
||||
if len(prefix_first) >= available or len(prefix_rest) >= available:
|
||||
raw_lines = [f"{prefix_first}{text}".rstrip()]
|
||||
else:
|
||||
wrapper = textwrap.TextWrapper(
|
||||
width=available,
|
||||
expand_tabs=False,
|
||||
replace_whitespace=True,
|
||||
drop_whitespace=True,
|
||||
initial_indent=prefix_first,
|
||||
subsequent_indent=prefix_rest,
|
||||
)
|
||||
raw_lines = [line.rstrip() for line in wrapper.wrap(text)]
|
||||
|
||||
margin = " " * margin_left
|
||||
if not raw_lines:
|
||||
return []
|
||||
|
||||
if align_blocks:
|
||||
return [margin + self._apply_alignment(line, style, available) for line in raw_lines]
|
||||
return [margin + line for line in raw_lines]
|
||||
|
||||
@staticmethod
|
||||
def _apply_alignment(line: str, style: BlockStyle, available: int) -> str:
|
||||
align = (style.align or "left").lower()
|
||||
if align == "center":
|
||||
return line.strip().center(available).rstrip()
|
||||
if align == "right":
|
||||
return line.strip().rjust(available).rstrip()
|
||||
return line.rstrip()
|
||||
|
||||
def _layout_ascii_pieces(self, pieces: List[AsciiArtPiece], style: BlockStyle) -> List[str]:
|
||||
if not pieces:
|
||||
return []
|
||||
if len(pieces) == 1:
|
||||
piece = pieces[0]
|
||||
return self._align_preformatted_lines(piece.lines, style, piece.align)
|
||||
|
||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
||||
margin_right = max(style.margin_right, 0)
|
||||
available_width = max(1, self.width - margin_left - margin_right)
|
||||
|
||||
heights = [len(piece.lines) for piece in pieces]
|
||||
max_height = max(heights) if heights else 0
|
||||
widths = [self._ascii_piece_width(piece.lines) for piece in pieces]
|
||||
|
||||
positions = self._compute_ascii_positions(pieces, widths, available_width)
|
||||
if positions is None:
|
||||
stacked: List[str] = []
|
||||
for piece in pieces:
|
||||
stacked.extend(self._align_preformatted_lines(piece.lines, style, piece.align))
|
||||
return stacked
|
||||
|
||||
canvas = [list(" " * available_width) for _ in range(max_height)]
|
||||
for piece, pos, _width in positions:
|
||||
for row_index in range(max_height):
|
||||
if row_index >= len(piece.lines):
|
||||
continue
|
||||
line = piece.lines[row_index].rstrip("\n")
|
||||
for col_index, char in enumerate(line):
|
||||
target_index = pos + col_index
|
||||
if target_index >= available_width:
|
||||
break
|
||||
if char != " ":
|
||||
canvas[row_index][target_index] = char
|
||||
|
||||
raw_rows = ["".join(row).rstrip() for row in canvas]
|
||||
return self._align_preformatted_lines(raw_rows, style)
|
||||
|
||||
def _ascii_piece_width(self, lines: List[str]) -> int:
|
||||
return max((len(line.rstrip("\n")) for line in lines), default=0)
|
||||
|
||||
def _align_preformatted_lines(
|
||||
self,
|
||||
lines: List[str],
|
||||
style: BlockStyle,
|
||||
explicit_align: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
if not lines:
|
||||
return []
|
||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
||||
margin_right = max(style.margin_right, 0)
|
||||
available_width = max(1, self.width - margin_left - margin_right)
|
||||
processed = [line.rstrip("\n") for line in lines]
|
||||
block_width = max((len(line) for line in processed), default=0)
|
||||
extra_space = max(0, available_width - block_width)
|
||||
align = (explicit_align or style.align or "left").lower()
|
||||
if align in {"center", "centre"}:
|
||||
align_offset = extra_space // 2
|
||||
elif align == "right":
|
||||
align_offset = extra_space
|
||||
else:
|
||||
align_offset = 0
|
||||
max_indent = max(0, self.width - block_width)
|
||||
indent = min(margin_left + align_offset, max_indent)
|
||||
indent_str = " " * indent
|
||||
return [indent_str + line for line in processed]
|
||||
|
||||
def _compute_ascii_positions(
|
||||
self,
|
||||
pieces: List[AsciiArtPiece],
|
||||
widths: List[int],
|
||||
available_width: int,
|
||||
) -> Optional[List[Tuple[AsciiArtPiece, int, int]]]:
|
||||
if available_width <= 0:
|
||||
return None
|
||||
|
||||
gap = 4
|
||||
left_cursor = 0
|
||||
right_cursor = available_width
|
||||
placement_map: Dict[int, Tuple[int, int]] = {}
|
||||
|
||||
left_indices: List[int] = []
|
||||
center_indices: List[int] = []
|
||||
right_indices: List[int] = []
|
||||
|
||||
for index, piece in enumerate(pieces):
|
||||
align = (piece.align or "left").lower()
|
||||
if align == "right":
|
||||
right_indices.append(index)
|
||||
elif align == "center":
|
||||
center_indices.append(index)
|
||||
else:
|
||||
left_indices.append(index)
|
||||
|
||||
for index in left_indices:
|
||||
width = min(widths[index], available_width)
|
||||
pos = left_cursor
|
||||
left_cursor = min(available_width, pos + width + gap)
|
||||
placement_map[index] = (pos, width)
|
||||
|
||||
for index in reversed(right_indices):
|
||||
width = min(widths[index], available_width)
|
||||
pos = max(0, right_cursor - width)
|
||||
right_cursor = max(0, pos - gap)
|
||||
placement_map[index] = (pos, width)
|
||||
|
||||
center_used = False
|
||||
for index in center_indices:
|
||||
width = min(widths[index], available_width)
|
||||
pos = max(0, (available_width - width) // 2)
|
||||
if center_used:
|
||||
pos = left_cursor
|
||||
left_cursor = min(available_width, pos + width + gap)
|
||||
else:
|
||||
center_used = True
|
||||
placement_map[index] = (pos, width)
|
||||
|
||||
for i in range(len(pieces)):
|
||||
if i not in placement_map:
|
||||
continue
|
||||
pos_i, width_i = placement_map[i]
|
||||
end_i = pos_i + width_i
|
||||
for j in range(i + 1, len(pieces)):
|
||||
if j not in placement_map:
|
||||
continue
|
||||
pos_j, width_j = placement_map[j]
|
||||
if pos_i <= pos_j < end_i or pos_j <= pos_i < pos_j + width_j:
|
||||
return None
|
||||
|
||||
ordered_positions: List[Tuple[AsciiArtPiece, int, int]] = []
|
||||
for index, piece in enumerate(pieces):
|
||||
if index not in placement_map:
|
||||
continue
|
||||
pos, width = placement_map[index]
|
||||
ordered_positions.append((piece, pos, width))
|
||||
return ordered_positions
|
||||
|
||||
def _replace_link(self, match: re.Match[str]) -> 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}]"
|
||||
|
||||
def _replace_image(self, match: re.Match[str]) -> str:
|
||||
alt = match.group(1).strip()
|
||||
url = match.group(2).strip()
|
||||
if not alt:
|
||||
return f"`[{url}`"
|
||||
return f"`[{alt}`{url}]"
|
||||
|
||||
def _write(self, text: str) -> None:
|
||||
if not text:
|
||||
return
|
||||
self._chunks.append(text)
|
||||
if text.endswith("\n"):
|
||||
newline_count = len(text) - len(text.rstrip("\n"))
|
||||
prefix = text[:-newline_count]
|
||||
if prefix:
|
||||
self._trailing_newlines = newline_count
|
||||
else:
|
||||
self._trailing_newlines += newline_count
|
||||
else:
|
||||
self._trailing_newlines = 0
|
||||
|
||||
def _ensure_newlines(self, count: int) -> None:
|
||||
if self._trailing_newlines < count:
|
||||
needed = count - self._trailing_newlines
|
||||
self._write("\n" * needed)
|
||||
|
||||
def _emit_block(
|
||||
self,
|
||||
content: str,
|
||||
*,
|
||||
stylable: bool,
|
||||
render_fn: Optional[Callable[[BlockStyle], str]] = None,
|
||||
style: Optional[BlockStyle] = None,
|
||||
) -> None:
|
||||
if not content:
|
||||
if stylable:
|
||||
self._last_stylable_block = None
|
||||
return
|
||||
index = len(self._chunks)
|
||||
self._write(content)
|
||||
if stylable and render_fn is not None and style is not None:
|
||||
self._last_stylable_block = _StylableBlock(
|
||||
index=index,
|
||||
render=render_fn,
|
||||
style=BlockStyle(
|
||||
align=style.align,
|
||||
margin_left=style.margin_left,
|
||||
margin_right=style.margin_right,
|
||||
),
|
||||
)
|
||||
else:
|
||||
self._last_stylable_block = None
|
||||
|
||||
def _apply_style_update(self, spec: StyleSpec) -> None:
|
||||
if self._last_stylable_block is None:
|
||||
return
|
||||
block = self._last_stylable_block
|
||||
new_style = self._combine_styles(block.style, spec)
|
||||
updated = block.render(new_style)
|
||||
self._chunks[block.index] = updated
|
||||
block.style = new_style
|
||||
|
||||
@staticmethod
|
||||
def _combine_styles(base: BlockStyle, spec: StyleSpec) -> BlockStyle:
|
||||
if spec is None:
|
||||
return BlockStyle(
|
||||
align=base.align,
|
||||
margin_left=base.margin_left,
|
||||
margin_right=base.margin_right,
|
||||
)
|
||||
return BlockStyle(
|
||||
align=spec.align or base.align,
|
||||
margin_left=spec.margin_left if spec.margin_left is not None else base.margin_left,
|
||||
margin_right=spec.margin_right if spec.margin_right is not None else base.margin_right,
|
||||
)
|
||||
|
||||
|
||||
def _micron_renderer_factory(*, frontmatter: FrontMatter, **options: Any) -> MicronRenderer:
|
||||
return MicronRenderer(frontmatter, **options)
|
||||
|
||||
|
||||
try:
|
||||
register_renderer("micron", _micron_renderer_factory)
|
||||
except ValueError:
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue