micron renderer derivative
This commit is contained in:
parent
937930ef5c
commit
c9af956d0a
2 changed files with 174 additions and 446 deletions
103
micron.mu
Normal file
103
micron.mu
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
> 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/]
|
||||||
|
|
||||||
|
>> Images
|
||||||
|
|
||||||
|
`[Minion`https://octodex.github.com/images/minion.png]
|
||||||
|
`[Stormtroopocat`https://octodex.github.com/images/stormtroopocat.jpg]
|
||||||
|
|
@ -1,404 +1,98 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
from functools import partial
|
||||||
import textwrap
|
from typing import Any, List
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
from md_types import (
|
from md_types import AsciiArtPayload, BlockQuotePayload, BlockStyle, CodeBlockPayload, FrontMatter, HeadingPayload, ListItemPayload, ParagraphPayload
|
||||||
AsciiArtPayload,
|
|
||||||
AsciiArtPiece,
|
|
||||||
BlockEvent,
|
|
||||||
BlockKind,
|
|
||||||
BlockQuotePayload,
|
|
||||||
BlockStyle,
|
|
||||||
CodeBlockPayload,
|
|
||||||
FrontMatter,
|
|
||||||
HeadingPayload,
|
|
||||||
ListItemPayload,
|
|
||||||
ParagraphPayload,
|
|
||||||
StyleSpec,
|
|
||||||
StyleUpdateEvent,
|
|
||||||
)
|
|
||||||
from plugins import register_renderer
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class MicronRenderer(TextRenderer):
|
||||||
class _StylableBlock:
|
|
||||||
index: int
|
|
||||||
render: Callable[[BlockStyle], str]
|
|
||||||
style: BlockStyle
|
|
||||||
|
|
||||||
|
|
||||||
class MicronRenderer:
|
|
||||||
def __init__(self, frontmatter: FrontMatter, *, width: int = 80, **_: Any) -> None:
|
def __init__(self, frontmatter: FrontMatter, *, width: int = 80, **_: Any) -> None:
|
||||||
self.frontmatter = frontmatter
|
super().__init__(width, frontmatter)
|
||||||
self.width = max(1, width)
|
# Micron format inlines links directly, so suppress link collection overhead.
|
||||||
self._chunks: List[str] = []
|
self.links.clear()
|
||||||
self._trailing_newlines = 0
|
self.link_indices.clear()
|
||||||
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:
|
def _render_paragraph(self, payload: ParagraphPayload, style: BlockStyle) -> None:
|
||||||
content = self._render_inline(payload.text)
|
processed = self._process_inline(payload.text)
|
||||||
if content:
|
self._wrap_emit(processed, style, stylable=True, hyphenate=self.hyphenate)
|
||||||
def render(target_style: BlockStyle) -> str:
|
if self.paragraph_spacing > 0:
|
||||||
lines = self._wrap_text(content, target_style)
|
self.output.extend([""] * self.paragraph_spacing)
|
||||||
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:
|
def _render_heading(self, payload: HeadingPayload, style: BlockStyle) -> None:
|
||||||
|
self._ensure_header_spacing()
|
||||||
level = max(1, min(3, payload.level))
|
level = max(1, min(3, payload.level))
|
||||||
marker = ">" * level + " "
|
marker = ">" * level
|
||||||
text = self._render_inline(payload.text)
|
line = f"{marker} {self._process_inline(payload.text)}".rstrip()
|
||||||
if text:
|
self._emit_block([line, ""], stylable=False)
|
||||||
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_code_block(self, payload: CodeBlockPayload, style: BlockStyle) -> None:
|
||||||
def render(target_style: BlockStyle) -> str:
|
margin_left, _, _ = self._margins(style)
|
||||||
margin = " " * max(0, target_style.margin_left)
|
indent = " " * margin_left
|
||||||
fence = margin + "`="
|
body = [f"{indent}`=", *[f"{indent}{line.rstrip()}" for line in payload.lines], f"{indent}`="]
|
||||||
body_lines = [margin + line.rstrip("\n") for line in payload.lines]
|
self._emit_block(body, stylable=False)
|
||||||
return "\n".join([fence, *body_lines, fence])
|
self.output.append("")
|
||||||
|
|
||||||
block = render(style)
|
|
||||||
if block:
|
|
||||||
self._emit_block(block, stylable=False)
|
|
||||||
self._ensure_newlines(2)
|
|
||||||
|
|
||||||
def _render_blockquote(self, payload: BlockQuotePayload, style: BlockStyle) -> None:
|
def _render_blockquote(self, payload: BlockQuotePayload, style: BlockStyle) -> None:
|
||||||
prefix = ">>>>" * max(1, payload.depth)
|
processed = self._process_inline(payload.text)
|
||||||
text = self._render_inline(payload.text)
|
indent = ">>>>" * max(1, payload.depth)
|
||||||
if text:
|
self._wrap_emit(processed, style, initial_indent=indent, subsequent_indent=indent, hyphenate=self.hyphenate)
|
||||||
def render(target_style: BlockStyle) -> str:
|
self.output.append("")
|
||||||
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:
|
def _render_list_item(self, payload: ListItemPayload, style: BlockStyle) -> None:
|
||||||
indent_text = payload.indent.replace("\t", " ")
|
base_indent = payload.indent.replace("\t", " ")
|
||||||
spacing = payload.spacing if payload.spacing else " "
|
marker_indent = " " * self.list_marker_indent
|
||||||
initial_prefix = f"{indent_text}{payload.marker}{spacing}"
|
marker = payload.marker
|
||||||
subsequent_prefix = f"{indent_text}{' ' * len(payload.marker)}{spacing}"
|
spacing = " " * self.list_text_spacing
|
||||||
text = self._render_inline(payload.text)
|
initial = f"{base_indent}{marker_indent}{marker}{spacing}"
|
||||||
if text:
|
subsequent = f"{base_indent}{marker_indent}{' ' * len(marker)}{spacing}"
|
||||||
def render(target_style: BlockStyle) -> str:
|
processed = self._process_inline(payload.text)
|
||||||
lines = self._wrap_text(
|
self._wrap_emit(processed, style, initial_indent=initial, subsequent_indent=subsequent, hyphenate=self.hyphenate)
|
||||||
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_custom_block(self, payload: AsciiArtPayload, style: BlockStyle) -> None:
|
||||||
def render(target_style: BlockStyle) -> str:
|
render = partial(self._layout_ascii_pieces, payload.pieces)
|
||||||
lines = self._layout_ascii_pieces(payload.pieces, target_style)
|
self._emit_block(render(style), stylable=True, render_fn=render, style=style)
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
block = render(style)
|
def _render_horizontal_rule(self, _payload: object, style: BlockStyle) -> None:
|
||||||
if block:
|
margin_left, _, available = self._margins(style)
|
||||||
self._emit_block(block, stylable=True, render_fn=render, style=style)
|
self._emit_block([" " * margin_left + "-" * available], stylable=False)
|
||||||
self._ensure_newlines(2)
|
|
||||||
|
|
||||||
def _render_inline(self, text: str) -> str:
|
|
||||||
if not text:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
# Inline transformations -------------------------------------------------
|
||||||
|
def _process_inline(self, text: str) -> str:
|
||||||
code_segments: List[str] = []
|
code_segments: List[str] = []
|
||||||
emphasis_segments: List[str] = []
|
|
||||||
|
|
||||||
def stash_code(match: re.Match[str]) -> str:
|
def stash_code(match):
|
||||||
placeholder = f"\u0000CODE{len(code_segments)}\u0000"
|
segment = match.group(0)
|
||||||
code_segments.append(match.group(1))
|
code_segments.append(segment[1:-1])
|
||||||
return placeholder
|
return f"\u0000CODE{len(code_segments) - 1}\u0000"
|
||||||
|
|
||||||
def stash_emphasis(content: str) -> str:
|
text = CODE_STASH_RE.sub(stash_code, text)
|
||||||
placeholder = f"\u0000EMP{len(emphasis_segments)}\u0000"
|
text = STRIKETHROUGH_RE.sub(lambda m: f"~~{m.group(1)}~~", text) # keep strikethrough literal for now
|
||||||
emphasis_segments.append(content)
|
text = BOLD_RE.sub(lambda m: self._apply_emphasis_spacing(m.string, m.start(), m.end(), f"`!{m.group(1)}`!"), text)
|
||||||
return placeholder
|
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 = re.sub(r"`([^`]+)`", stash_code, 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 = re.sub(
|
text = IMAGE_RE.sub(self._replace_image, text)
|
||||||
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}`=")
|
|
||||||
|
|
||||||
|
for idx, code in enumerate(code_segments):
|
||||||
|
text = text.replace(f"\u0000CODE{idx}\u0000", f"`={code}`=")
|
||||||
return text
|
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
|
@staticmethod
|
||||||
def _apply_alignment(line: str, style: BlockStyle, available: int) -> str:
|
def _replace_link(match) -> 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()
|
label = match.group(1).strip()
|
||||||
url = match.group(2).strip()
|
url = match.group(2).strip()
|
||||||
if not label:
|
if not label:
|
||||||
|
|
@ -407,85 +101,16 @@ class MicronRenderer:
|
||||||
return f"`[{label}`"
|
return f"`[{label}`"
|
||||||
return f"`[{label}`{url}]"
|
return f"`[{label}`{url}]"
|
||||||
|
|
||||||
def _replace_image(self, match: re.Match[str]) -> str:
|
@staticmethod
|
||||||
|
def _replace_image(match) -> str:
|
||||||
alt = match.group(1).strip()
|
alt = match.group(1).strip()
|
||||||
url = match.group(2).strip()
|
url = match.group(2).strip()
|
||||||
if not alt:
|
return f"`[{alt or url}`{url}]"
|
||||||
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:
|
def _micron_renderer_factory(*, frontmatter: FrontMatter, **options: Any) -> MicronRenderer:
|
||||||
return MicronRenderer(frontmatter, **options)
|
width = int(options.get("width", 80))
|
||||||
|
return MicronRenderer(frontmatter, width=width)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue