tightening
This commit is contained in:
parent
5e0e4b701e
commit
937930ef5c
1 changed files with 136 additions and 175 deletions
305
text_renderer.py
305
text_renderer.py
|
|
@ -4,6 +4,7 @@ import re
|
||||||
import string
|
import string
|
||||||
import textwrap
|
import textwrap
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from functools import partial
|
||||||
from typing import Callable, Dict, List, Optional, Tuple
|
from typing import Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from md_types import (
|
from md_types import (
|
||||||
|
|
@ -54,6 +55,16 @@ except ImportError: # pragma: no cover - emit a helpful error at runtime instea
|
||||||
FontNotFound = ValueError # type: ignore[assignment]
|
FontNotFound = ValueError # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
|
CODE_STASH_RE = re.compile(r"`[^`]*`")
|
||||||
|
STRIKETHROUGH_RE = re.compile(r"~~(.*?)~~")
|
||||||
|
BOLD_RE = re.compile(r"\*\*(.*?)\*\*")
|
||||||
|
ITALIC_RE = re.compile(r"(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)")
|
||||||
|
UNDERLINE_STRONG_RE = re.compile(r"__(.*?)__")
|
||||||
|
UNDERLINE_EM_RE = re.compile(r"(?<!_)_(?!_)(.*?)(?<!_)_(?!_)")
|
||||||
|
IMAGE_RE = re.compile(r"!\[([^\]]*)\]\(([^)]+)\)")
|
||||||
|
LINK_RE = re.compile(r"(?<!\!)\[([^\]]+)\]\(([^)]+)\)")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BlockRecord:
|
class BlockRecord:
|
||||||
start: int
|
start: int
|
||||||
|
|
@ -99,6 +110,16 @@ class TextRenderer:
|
||||||
raise RuntimeError(f"Failed to initialise hyphenator for language '{self.hyphen_lang}': {exc}") from exc
|
raise RuntimeError(f"Failed to initialise hyphenator for language '{self.hyphen_lang}': {exc}") from exc
|
||||||
else:
|
else:
|
||||||
self.hyphenator = None
|
self.hyphenator = None
|
||||||
|
self._handlers: Dict[BlockKind, Callable[[object, BlockStyle], None]] = {
|
||||||
|
BlockKind.PARAGRAPH: self._render_paragraph,
|
||||||
|
BlockKind.HEADING: self._render_heading,
|
||||||
|
BlockKind.CODE_BLOCK: self._render_code_block,
|
||||||
|
BlockKind.BLOCKQUOTE: self._render_blockquote,
|
||||||
|
BlockKind.LIST_ITEM: self._render_list_item,
|
||||||
|
BlockKind.HORIZONTAL_RULE: self._render_horizontal_rule,
|
||||||
|
BlockKind.BLANK_LINE: self._render_blank_line,
|
||||||
|
BlockKind.CUSTOM_BLOCK: self._render_custom_block,
|
||||||
|
}
|
||||||
|
|
||||||
def handle_event(self, event: BlockEvent | StyleUpdateEvent) -> None:
|
def handle_event(self, event: BlockEvent | StyleUpdateEvent) -> None:
|
||||||
if isinstance(event, BlockEvent):
|
if isinstance(event, BlockEvent):
|
||||||
|
|
@ -124,61 +145,37 @@ class TextRenderer:
|
||||||
return self.output
|
return self.output
|
||||||
|
|
||||||
def _handle_block_event(self, event: BlockEvent) -> None:
|
def _handle_block_event(self, event: BlockEvent) -> None:
|
||||||
if event.kind is BlockKind.PARAGRAPH:
|
handler = self._handlers.get(event.kind)
|
||||||
self._render_paragraph(event.payload, event.style)
|
if handler:
|
||||||
elif event.kind is BlockKind.HEADING:
|
handler(event.payload, event.style)
|
||||||
self._render_heading(event.payload, event.style)
|
else:
|
||||||
elif event.kind is BlockKind.CODE_BLOCK:
|
|
||||||
self._render_code_block(event.payload, event.style)
|
|
||||||
elif event.kind is BlockKind.BLOCKQUOTE:
|
|
||||||
self._render_blockquote(event.payload, event.style)
|
|
||||||
elif event.kind is BlockKind.LIST_ITEM:
|
|
||||||
self._render_list_item(event.payload, event.style)
|
|
||||||
elif event.kind is BlockKind.HORIZONTAL_RULE:
|
|
||||||
self._render_horizontal_rule(event.style)
|
|
||||||
elif event.kind is BlockKind.BLANK_LINE:
|
|
||||||
self._render_blank_line()
|
|
||||||
elif event.kind is BlockKind.CUSTOM_BLOCK:
|
|
||||||
self._render_custom_block(event.payload, event.style)
|
|
||||||
else: # pragma: no cover - defensive default
|
|
||||||
self._last_stylable_block = None
|
self._last_stylable_block = None
|
||||||
|
|
||||||
def _render_paragraph(self, payload: ParagraphPayload, style: BlockStyle) -> None:
|
def _render_paragraph(self, payload: ParagraphPayload, style: BlockStyle) -> None:
|
||||||
processed = self._process_inline(payload.text)
|
processed = self._process_inline(payload.text)
|
||||||
|
self._wrap_emit(processed, style, stylable=True, hyphenate=self.hyphenate)
|
||||||
def render(target_style: BlockStyle) -> List[str]:
|
|
||||||
return self._wrap_text(processed, style=target_style, hyphenate=self.hyphenate)
|
|
||||||
|
|
||||||
lines = render(style)
|
|
||||||
self._emit_block(lines, stylable=True, render_fn=render, style=style)
|
|
||||||
if self.paragraph_spacing > 0:
|
if self.paragraph_spacing > 0:
|
||||||
self.output.extend([""] * self.paragraph_spacing)
|
self.output.extend([""] * self.paragraph_spacing)
|
||||||
|
|
||||||
def _render_heading(self, payload: HeadingPayload, style: BlockStyle) -> None:
|
def _render_heading(self, payload: HeadingPayload, style: BlockStyle) -> None:
|
||||||
self._ensure_header_spacing()
|
self._ensure_header_spacing()
|
||||||
|
|
||||||
def render(target_style: BlockStyle) -> List[str]:
|
self._emit_render(partial(self._render_heading_lines, payload.level, payload.text), style, stylable=True)
|
||||||
return self._render_heading_lines(payload.level, payload.text, target_style)
|
|
||||||
|
|
||||||
lines = render(style)
|
|
||||||
self._emit_block(lines, stylable=True, render_fn=render, style=style)
|
|
||||||
|
|
||||||
def _render_code_block(self, payload: CodeBlockPayload, style: BlockStyle) -> None:
|
def _render_code_block(self, payload: CodeBlockPayload, style: BlockStyle) -> None:
|
||||||
lines = self._format_code_block(payload.lines, style)
|
self._emit_render(partial(self._format_code_block, payload.lines), style)
|
||||||
self._emit_block(lines, stylable=False)
|
|
||||||
|
|
||||||
def _render_blockquote(self, payload: BlockQuotePayload, style: BlockStyle) -> None:
|
def _render_blockquote(self, payload: BlockQuotePayload, style: BlockStyle) -> None:
|
||||||
processed = self._process_inline(payload.text)
|
processed = self._process_inline(payload.text)
|
||||||
indent_unit = " | " if self.blockquote_bars else " "
|
indent_unit = " | " if self.blockquote_bars else " "
|
||||||
indent = indent_unit * max(1, payload.depth)
|
indent = indent_unit * max(1, payload.depth)
|
||||||
wrapped = self._wrap_text(
|
self._wrap_emit(
|
||||||
processed,
|
processed,
|
||||||
|
style,
|
||||||
initial_indent=indent,
|
initial_indent=indent,
|
||||||
subsequent_indent=indent,
|
subsequent_indent=indent,
|
||||||
style=style,
|
|
||||||
hyphenate=self.hyphenate,
|
hyphenate=self.hyphenate,
|
||||||
)
|
)
|
||||||
self._emit_block(wrapped, stylable=False)
|
|
||||||
|
|
||||||
def _render_list_item(self, payload: ListItemPayload, style: BlockStyle) -> None:
|
def _render_list_item(self, payload: ListItemPayload, style: BlockStyle) -> None:
|
||||||
base_indent = payload.indent.replace("\t", " ")
|
base_indent = payload.indent.replace("\t", " ")
|
||||||
|
|
@ -188,30 +185,25 @@ class TextRenderer:
|
||||||
initial_indent = f"{base_indent}{marker_indent}{marker}{text_spacing}"
|
initial_indent = f"{base_indent}{marker_indent}{marker}{text_spacing}"
|
||||||
subsequent_indent = f"{base_indent}{marker_indent}{' ' * len(marker)}{text_spacing}"
|
subsequent_indent = f"{base_indent}{marker_indent}{' ' * len(marker)}{text_spacing}"
|
||||||
processed = self._process_inline(payload.text)
|
processed = self._process_inline(payload.text)
|
||||||
wrapped = self._wrap_text(
|
self._wrap_emit(
|
||||||
processed,
|
processed,
|
||||||
|
style,
|
||||||
initial_indent=initial_indent,
|
initial_indent=initial_indent,
|
||||||
subsequent_indent=subsequent_indent,
|
subsequent_indent=subsequent_indent,
|
||||||
style=style,
|
|
||||||
hyphenate=self.hyphenate,
|
hyphenate=self.hyphenate,
|
||||||
)
|
)
|
||||||
self._emit_block(wrapped, stylable=False)
|
|
||||||
|
|
||||||
def _render_horizontal_rule(self, style: BlockStyle) -> None:
|
def _render_horizontal_rule(self, _payload: object, style: BlockStyle) -> None:
|
||||||
line = self._render_horizontal_rule_line(style)
|
line = self._render_horizontal_rule_line(style)
|
||||||
self._emit_block([line], stylable=False)
|
self._emit_block([line], stylable=False)
|
||||||
|
|
||||||
def _render_blank_line(self) -> None:
|
def _render_blank_line(self, *_: object) -> None:
|
||||||
if self.paragraph_spacing == 0:
|
if self.paragraph_spacing == 0:
|
||||||
self.output.append("")
|
self.output.append("")
|
||||||
|
|
||||||
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) -> List[str]:
|
self._emit_render(partial(self._layout_ascii_pieces, payload.pieces), style, stylable=True)
|
||||||
return self._layout_ascii_pieces(payload.pieces, target_style)
|
|
||||||
|
|
||||||
lines = render(style)
|
|
||||||
self._emit_block(lines, stylable=True, render_fn=render, style=style)
|
|
||||||
|
|
||||||
def _layout_ascii_pieces(self, pieces: List[AsciiArtPiece], style: BlockStyle) -> List[str]:
|
def _layout_ascii_pieces(self, pieces: List[AsciiArtPiece], style: BlockStyle) -> List[str]:
|
||||||
if not pieces:
|
if not pieces:
|
||||||
|
|
@ -220,9 +212,7 @@ class TextRenderer:
|
||||||
piece = pieces[0]
|
piece = pieces[0]
|
||||||
return self._align_preformatted_lines(piece.lines, style, piece.align)
|
return self._align_preformatted_lines(piece.lines, style, piece.align)
|
||||||
|
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
margin_left, _, available_width = self._margins(style)
|
||||||
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]
|
heights = [len(piece.lines) for piece in pieces]
|
||||||
max_height = max(heights) if heights else 0
|
max_height = max(heights) if heights else 0
|
||||||
|
|
@ -230,11 +220,11 @@ class TextRenderer:
|
||||||
|
|
||||||
positions = self._compute_ascii_positions(pieces, widths, available_width)
|
positions = self._compute_ascii_positions(pieces, widths, available_width)
|
||||||
if positions is None:
|
if positions is None:
|
||||||
# fall back to vertical stacking by aligning individually
|
return [
|
||||||
result: List[str] = []
|
aligned_line
|
||||||
for piece in pieces:
|
for piece in pieces
|
||||||
result.extend(self._align_preformatted_lines(piece.lines, style, piece.align))
|
for aligned_line in self._align_preformatted_lines(piece.lines, style, piece.align)
|
||||||
return result
|
]
|
||||||
|
|
||||||
canvas = [list(" " * available_width) for _ in range(max_height)]
|
canvas = [list(" " * available_width) for _ in range(max_height)]
|
||||||
for piece, pos, width in positions:
|
for piece, pos, width in positions:
|
||||||
|
|
@ -252,11 +242,7 @@ class TextRenderer:
|
||||||
canvas[row_index][target_index] = char
|
canvas[row_index][target_index] = char
|
||||||
|
|
||||||
prefix = " " * margin_left
|
prefix = " " * margin_left
|
||||||
result_lines: List[str] = []
|
return [prefix + "".join(row).rstrip() for row in canvas]
|
||||||
for row in canvas:
|
|
||||||
content = "".join(row).rstrip()
|
|
||||||
result_lines.append(prefix + content)
|
|
||||||
return result_lines
|
|
||||||
|
|
||||||
def _compute_ascii_positions(
|
def _compute_ascii_positions(
|
||||||
self,
|
self,
|
||||||
|
|
@ -272,34 +258,24 @@ class TextRenderer:
|
||||||
right_cursor = available_width
|
right_cursor = available_width
|
||||||
placement_map: Dict[int, Tuple[int, int]] = {}
|
placement_map: Dict[int, Tuple[int, int]] = {}
|
||||||
|
|
||||||
# Categorise pieces by alignment preference
|
groups: Dict[str, List[int]] = {"left": [], "center": [], "right": []}
|
||||||
left_indices: List[int] = []
|
|
||||||
center_indices: List[int] = []
|
|
||||||
right_indices: List[int] = []
|
|
||||||
|
|
||||||
for index, piece in enumerate(pieces):
|
for index, piece in enumerate(pieces):
|
||||||
align = (piece.align or "left").lower()
|
groups.get((piece.align or "left").lower(), groups["left"]).append(index)
|
||||||
if align == "right":
|
|
||||||
right_indices.append(index)
|
|
||||||
elif align == "center":
|
|
||||||
center_indices.append(index)
|
|
||||||
else:
|
|
||||||
left_indices.append(index)
|
|
||||||
|
|
||||||
for index in left_indices:
|
for index in groups["left"]:
|
||||||
width = min(widths[index], available_width)
|
width = min(widths[index], available_width)
|
||||||
pos = left_cursor
|
pos = left_cursor
|
||||||
left_cursor = min(available_width, pos + width + gap)
|
left_cursor = min(available_width, pos + width + gap)
|
||||||
placement_map[index] = (pos, width)
|
placement_map[index] = (pos, width)
|
||||||
|
|
||||||
for index in reversed(right_indices):
|
for index in reversed(groups["right"]):
|
||||||
width = min(widths[index], available_width)
|
width = min(widths[index], available_width)
|
||||||
pos = max(0, right_cursor - width)
|
pos = max(0, right_cursor - width)
|
||||||
right_cursor = max(0, pos - gap)
|
right_cursor = max(0, pos - gap)
|
||||||
placement_map[index] = (pos, width)
|
placement_map[index] = (pos, width)
|
||||||
|
|
||||||
center_used = False
|
center_used = False
|
||||||
for index in center_indices:
|
for index in groups["center"]:
|
||||||
width = min(widths[index], available_width)
|
width = min(widths[index], available_width)
|
||||||
pos = max(0, (available_width - width) // 2)
|
pos = max(0, (available_width - width) // 2)
|
||||||
if center_used:
|
if center_used:
|
||||||
|
|
@ -342,9 +318,7 @@ class TextRenderer:
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
if not lines:
|
if not lines:
|
||||||
return []
|
return []
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
margin_left, _, available_width = self._margins(style)
|
||||||
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]
|
processed = [line.rstrip("\n") for line in lines]
|
||||||
block_width = max((len(line) for line in processed), default=0)
|
block_width = max((len(line) for line in processed), default=0)
|
||||||
extra_space = max(0, available_width - block_width)
|
extra_space = max(0, available_width - block_width)
|
||||||
|
|
@ -360,6 +334,52 @@ class TextRenderer:
|
||||||
indent_str = " " * indent
|
indent_str = " " * indent
|
||||||
return [indent_str + line for line in processed]
|
return [indent_str + line for line in processed]
|
||||||
|
|
||||||
|
def _margins(self, style: BlockStyle) -> Tuple[int, int, int]:
|
||||||
|
margin_left = max(0, min(style.margin_left, self.width - 1))
|
||||||
|
remaining = self.width - margin_left
|
||||||
|
margin_right = max(0, min(style.margin_right, max(0, remaining - 1)))
|
||||||
|
available = max(1, self.width - margin_left - margin_right)
|
||||||
|
return margin_left, margin_right, available
|
||||||
|
|
||||||
|
def _wrap_emit(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
style: BlockStyle,
|
||||||
|
*,
|
||||||
|
initial_indent: str = "",
|
||||||
|
subsequent_indent: Optional[str] = None,
|
||||||
|
hyphenate: bool,
|
||||||
|
stylable: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._emit_render(
|
||||||
|
lambda target_style: self._wrap_text(
|
||||||
|
text,
|
||||||
|
initial_indent=initial_indent,
|
||||||
|
subsequent_indent=subsequent_indent if subsequent_indent is not None else initial_indent,
|
||||||
|
style=target_style,
|
||||||
|
hyphenate=hyphenate,
|
||||||
|
),
|
||||||
|
style,
|
||||||
|
stylable=stylable,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _emit_render(
|
||||||
|
self,
|
||||||
|
render_fn: Callable[[BlockStyle], List[str]],
|
||||||
|
style: BlockStyle,
|
||||||
|
*,
|
||||||
|
stylable: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self._emit_block(
|
||||||
|
render_fn(style),
|
||||||
|
stylable=stylable,
|
||||||
|
render_fn=render_fn if stylable else None,
|
||||||
|
style=style if stylable else None,
|
||||||
|
)
|
||||||
|
|
||||||
def _emit_block(
|
def _emit_block(
|
||||||
self,
|
self,
|
||||||
lines: List[str],
|
lines: List[str],
|
||||||
|
|
@ -449,7 +469,7 @@ class TextRenderer:
|
||||||
if overflow_detected and self.figlet_fallback:
|
if overflow_detected and self.figlet_fallback:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
margin_left, _, _ = self._margins(style)
|
||||||
indent_str = " " * margin_left
|
indent_str = " " * margin_left
|
||||||
return [indent_str + line.rstrip() for line in lines]
|
return [indent_str + line.rstrip() for line in lines]
|
||||||
|
|
||||||
|
|
@ -486,87 +506,45 @@ class TextRenderer:
|
||||||
|
|
||||||
def _render_horizontal_rule_line(self, style: BlockStyle) -> str:
|
def _render_horizontal_rule_line(self, style: BlockStyle) -> str:
|
||||||
|
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
margin_left, _, available_width = self._margins(style)
|
||||||
available_width = max(1, self._effective_width(style))
|
|
||||||
return " " * margin_left + "-" * available_width
|
return " " * margin_left + "-" * available_width
|
||||||
|
|
||||||
def _format_code_block(self, lines: List[str], style: BlockStyle) -> List[str]:
|
def _format_code_block(self, lines: List[str], style: BlockStyle) -> List[str]:
|
||||||
if not lines:
|
if not lines:
|
||||||
return []
|
return []
|
||||||
if self.wrap_code_blocks:
|
margin_left, _, available = self._margins(style)
|
||||||
if self.code_block_line_numbers:
|
indent = " " * margin_left
|
||||||
return self._format_wrapped_code_block_numbered(lines, style)
|
numbered = self.code_block_line_numbers
|
||||||
return self._format_wrapped_code_block_plain(lines, style)
|
wrapped = self.wrap_code_blocks
|
||||||
if self.code_block_line_numbers:
|
|
||||||
return self._format_code_block_numbered(lines, style)
|
|
||||||
return self._format_code_block_plain(lines, style)
|
|
||||||
|
|
||||||
def _format_code_block_numbered(self, lines: List[str], style: BlockStyle) -> List[str]:
|
if numbered:
|
||||||
margin_indent = " " * max(0, style.margin_left)
|
|
||||||
width = max(2, len(str(len(lines))))
|
width = max(2, len(str(len(lines))))
|
||||||
formatted: List[str] = []
|
lead_fmt = f"{indent}{{:0{width}d}} | "
|
||||||
for idx, line in enumerate(lines, start=1):
|
cont_prefix = f"{indent}{' ' * width} | "
|
||||||
formatted.append(f"{margin_indent}{idx:0{width}d} | {line}")
|
content_width = max(1, available - (len(lead_fmt.format(0)) - len(indent)))
|
||||||
formatted.append(margin_indent if margin_indent else "")
|
|
||||||
return formatted
|
|
||||||
|
|
||||||
def _format_code_block_plain(self, lines: List[str], style: BlockStyle) -> List[str]:
|
|
||||||
margin_indent = " " * max(0, style.margin_left)
|
|
||||||
base_prefix = margin_indent + " "
|
|
||||||
formatted: List[str] = []
|
|
||||||
for line in lines:
|
|
||||||
if line:
|
|
||||||
formatted.append(base_prefix + line)
|
|
||||||
else:
|
else:
|
||||||
formatted.append(base_prefix)
|
base = indent + " "
|
||||||
formatted.append(margin_indent if margin_indent else "")
|
lead_fmt = None
|
||||||
return formatted
|
cont_prefix = base
|
||||||
|
content_width = max(1, available - 3)
|
||||||
|
|
||||||
|
def lead(idx: int) -> str:
|
||||||
|
return lead_fmt.format(idx) if numbered else cont_prefix
|
||||||
|
|
||||||
def _format_wrapped_code_block_numbered(self, lines: List[str], style: BlockStyle) -> List[str]:
|
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
|
||||||
margin_indent = " " * margin_left
|
|
||||||
effective_width = self._effective_width(style)
|
|
||||||
number_width = max(2, len(str(len(lines))))
|
|
||||||
prefix_template = f"{margin_indent}{{num:0{number_width}d}} | "
|
|
||||||
continuation_prefix = f"{margin_indent}{' ' * number_width} | "
|
|
||||||
prefix_len_without_margin = len(prefix_template.format(num=0)) - len(margin_indent)
|
|
||||||
content_width = max(1, effective_width - prefix_len_without_margin)
|
|
||||||
formatted: List[str] = []
|
formatted: List[str] = []
|
||||||
for idx, line in enumerate(lines, start=1):
|
for idx, line in enumerate(lines, start=1):
|
||||||
if not line:
|
prefix = lead(idx)
|
||||||
formatted.append(prefix_template.format(num=idx))
|
segments = (
|
||||||
|
self._wrap_code_line_segments(line, content_width)
|
||||||
|
if wrapped and line
|
||||||
|
else ([line] if line else [])
|
||||||
|
)
|
||||||
|
if not segments:
|
||||||
|
formatted.append(prefix)
|
||||||
continue
|
continue
|
||||||
wrapped_segments = self._wrap_code_line_segments(line, content_width)
|
formatted.append(prefix + segments[0])
|
||||||
if not wrapped_segments:
|
formatted.extend(cont_prefix + segment for segment in segments[1:])
|
||||||
formatted.append(prefix_template.format(num=idx))
|
formatted.append(indent if indent else "")
|
||||||
continue
|
|
||||||
first_segment, *rest_segments = wrapped_segments
|
|
||||||
formatted.append(prefix_template.format(num=idx) + first_segment)
|
|
||||||
for segment in rest_segments:
|
|
||||||
formatted.append(continuation_prefix + segment)
|
|
||||||
formatted.append(margin_indent if margin_indent else "")
|
|
||||||
return formatted
|
|
||||||
|
|
||||||
def _format_wrapped_code_block_plain(self, lines: List[str], style: BlockStyle) -> List[str]:
|
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
|
||||||
margin_indent = " " * margin_left
|
|
||||||
effective_width = self._effective_width(style)
|
|
||||||
content_prefix = margin_indent + " "
|
|
||||||
content_width = max(1, effective_width - 3)
|
|
||||||
formatted: List[str] = []
|
|
||||||
for line in lines:
|
|
||||||
if not line:
|
|
||||||
formatted.append(content_prefix)
|
|
||||||
continue
|
|
||||||
wrapped_segments = self._wrap_code_line_segments(line, content_width)
|
|
||||||
if not wrapped_segments:
|
|
||||||
formatted.append(content_prefix)
|
|
||||||
continue
|
|
||||||
first_segment, *rest_segments = wrapped_segments
|
|
||||||
formatted.append(content_prefix + first_segment)
|
|
||||||
for segment in rest_segments:
|
|
||||||
formatted.append(content_prefix + segment)
|
|
||||||
formatted.append(margin_indent if margin_indent else "")
|
|
||||||
return formatted
|
return formatted
|
||||||
|
|
||||||
def _wrap_code_line_segments(self, line: str, content_width: int) -> List[str]:
|
def _wrap_code_line_segments(self, line: str, content_width: int) -> List[str]:
|
||||||
|
|
@ -625,23 +603,11 @@ class TextRenderer:
|
||||||
code_segments.append(match.group(0))
|
code_segments.append(match.group(0))
|
||||||
return f"\u0000CODE{len(code_segments) - 1}\u0000"
|
return f"\u0000CODE{len(code_segments) - 1}\u0000"
|
||||||
|
|
||||||
text = re.sub(r"`[^`]*`", stash_code, text)
|
text = CODE_STASH_RE.sub(stash_code, text)
|
||||||
|
|
||||||
text = self._apply_pattern(
|
text = self._apply_pattern(text, STRIKETHROUGH_RE, lambda m, src: self._stylize_delimited(m.group(1), "-", transform="preserve"))
|
||||||
text,
|
text = self._apply_pattern(text, BOLD_RE, lambda m, src: self._replace_spaced_emphasis(src, m, transform="upper"))
|
||||||
re.compile(r"~~(.*?)~~"),
|
text = self._apply_pattern(text, ITALIC_RE, lambda m, src: self._replace_spaced_emphasis(src, m, transform="preserve"))
|
||||||
lambda m, src: self._stylize_delimited(m.group(1), "-", transform="preserve"),
|
|
||||||
)
|
|
||||||
text = self._apply_pattern(
|
|
||||||
text,
|
|
||||||
re.compile(r"\*\*(.*?)\*\*"),
|
|
||||||
lambda m, src: self._replace_spaced_emphasis(src, m, transform="upper"),
|
|
||||||
)
|
|
||||||
text = self._apply_pattern(
|
|
||||||
text,
|
|
||||||
re.compile(r"(?<!\*)\*(?!\*)(.*?)(?<!\*)\*(?!\*)"),
|
|
||||||
lambda m, src: self._replace_spaced_emphasis(src, m, transform="preserve"),
|
|
||||||
)
|
|
||||||
|
|
||||||
emphasis_segments: List[str] = []
|
emphasis_segments: List[str] = []
|
||||||
|
|
||||||
|
|
@ -660,7 +626,7 @@ class TextRenderer:
|
||||||
|
|
||||||
text = apply_with_placeholder(
|
text = apply_with_placeholder(
|
||||||
text,
|
text,
|
||||||
re.compile(r"__(.*?)__"),
|
UNDERLINE_STRONG_RE,
|
||||||
lambda m: self._apply_emphasis_spacing(
|
lambda m: self._apply_emphasis_spacing(
|
||||||
m.string,
|
m.string,
|
||||||
m.start(),
|
m.start(),
|
||||||
|
|
@ -670,7 +636,7 @@ class TextRenderer:
|
||||||
)
|
)
|
||||||
text = apply_with_placeholder(
|
text = apply_with_placeholder(
|
||||||
text,
|
text,
|
||||||
re.compile(r"(?<!_)_(?!_)(.*?)(?<!_)_(?!_)"),
|
UNDERLINE_EM_RE,
|
||||||
lambda m: self._apply_emphasis_spacing(
|
lambda m: self._apply_emphasis_spacing(
|
||||||
m.string,
|
m.string,
|
||||||
m.start(),
|
m.start(),
|
||||||
|
|
@ -679,8 +645,8 @@ class TextRenderer:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
text = re.sub(r"(?<!\!)\[([^\]]+)\]\(([^)]+)\)", self._handle_link, text)
|
text = LINK_RE.sub(self._handle_link, text)
|
||||||
text = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", self._handle_image, text)
|
text = IMAGE_RE.sub(self._handle_image, text)
|
||||||
|
|
||||||
for index, replacement in enumerate(emphasis_segments):
|
for index, replacement in enumerate(emphasis_segments):
|
||||||
placeholder = f"\u0000EMP{index}\u0000"
|
placeholder = f"\u0000EMP{index}\u0000"
|
||||||
|
|
@ -859,8 +825,7 @@ class TextRenderer:
|
||||||
hyphenate: bool = False,
|
hyphenate: bool = False,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
style = style or BlockStyle()
|
style = style or BlockStyle()
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
margin_left, _, available_width = self._margins(style)
|
||||||
available_width = max(1, self._effective_width(style))
|
|
||||||
|
|
||||||
subsequent = initial_indent if subsequent_indent is None else subsequent_indent
|
subsequent = initial_indent if subsequent_indent is None else subsequent_indent
|
||||||
|
|
||||||
|
|
@ -991,8 +956,7 @@ class TextRenderer:
|
||||||
lines.append(initial_indent.rstrip())
|
lines.append(initial_indent.rstrip())
|
||||||
|
|
||||||
result: List[str] = []
|
result: List[str] = []
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
margin_left, _, width = self._margins(style)
|
||||||
width = available_width
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.rstrip()
|
stripped = line.rstrip()
|
||||||
line_len = len(stripped)
|
line_len = len(stripped)
|
||||||
|
|
@ -1031,10 +995,7 @@ class TextRenderer:
|
||||||
return segments
|
return segments
|
||||||
|
|
||||||
def _effective_width(self, style: BlockStyle) -> int:
|
def _effective_width(self, style: BlockStyle) -> int:
|
||||||
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
return self._margins(style)[2]
|
||||||
remaining = self.width - margin_left
|
|
||||||
margin_right = min(max(style.margin_right, 0), max(0, remaining - 1))
|
|
||||||
return max(1, self.width - margin_left - margin_right)
|
|
||||||
|
|
||||||
def _ensure_header_spacing(self) -> None:
|
def _ensure_header_spacing(self) -> None:
|
||||||
if self.header_spacing <= 0:
|
if self.header_spacing <= 0:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue