block decorations and wrap
This commit is contained in:
parent
de6e99c24a
commit
c57cfc4747
4 changed files with 152 additions and 4 deletions
|
|
@ -5,6 +5,9 @@ h3_font: caps
|
||||||
margin_left: 12
|
margin_left: 12
|
||||||
margin_right: 12
|
margin_right: 12
|
||||||
paragraph_spacing: 2
|
paragraph_spacing: 2
|
||||||
|
code_block_line_numbers: false
|
||||||
|
code_block_wrap: 4
|
||||||
|
blockquote_bars: false
|
||||||
hyphenate: true
|
hyphenate: true
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
21
md2txt.py
21
md2txt.py
|
|
@ -79,6 +79,22 @@ def parse_frontmatter(lines: List[str]) -> Tuple[FrontMatter, List[str]]:
|
||||||
paragraph_spacing_value = frontmatter.get("lines_between_paragraphs")
|
paragraph_spacing_value = frontmatter.get("lines_between_paragraphs")
|
||||||
if paragraph_spacing_value is None:
|
if paragraph_spacing_value is None:
|
||||||
paragraph_spacing_value = frontmatter.get("paragraph_lines")
|
paragraph_spacing_value = frontmatter.get("paragraph_lines")
|
||||||
|
default_wrap_indent = 2
|
||||||
|
wrap_code_blocks = _parse_bool(frontmatter.get("wrap_code_blocks"), False)
|
||||||
|
code_block_wrap_indent = default_wrap_indent if wrap_code_blocks else 0
|
||||||
|
code_block_wrap_value = frontmatter.get("code_block_wrap")
|
||||||
|
if code_block_wrap_value is not None:
|
||||||
|
normalized_wrap = code_block_wrap_value.strip()
|
||||||
|
if normalized_wrap:
|
||||||
|
if re.fullmatch(r"-?\d+", normalized_wrap):
|
||||||
|
wrap_code_blocks = True
|
||||||
|
code_block_wrap_indent = max(0, _parse_int(normalized_wrap, default_wrap_indent))
|
||||||
|
else:
|
||||||
|
wrap_flag = _parse_bool(normalized_wrap, wrap_code_blocks)
|
||||||
|
wrap_code_blocks = wrap_flag
|
||||||
|
code_block_wrap_indent = default_wrap_indent if wrap_flag else 0
|
||||||
|
code_block_line_numbers = _parse_bool(frontmatter.get("code_block_line_numbers"), True)
|
||||||
|
blockquote_bars = _parse_bool(frontmatter.get("blockquote_bars"), True)
|
||||||
fm = FrontMatter(
|
fm = FrontMatter(
|
||||||
h1_font=frontmatter.get("h1_font", "standard").strip() or "standard",
|
h1_font=frontmatter.get("h1_font", "standard").strip() or "standard",
|
||||||
h2_font=frontmatter.get("h2_font", "standard").strip() or "standard",
|
h2_font=frontmatter.get("h2_font", "standard").strip() or "standard",
|
||||||
|
|
@ -90,6 +106,10 @@ def parse_frontmatter(lines: List[str]) -> Tuple[FrontMatter, List[str]]:
|
||||||
hyphen_lang=(frontmatter.get("hyphen_lang") or "en_US").strip() or "en_US",
|
hyphen_lang=(frontmatter.get("hyphen_lang") or "en_US").strip() or "en_US",
|
||||||
figlet_fallback=_parse_bool(frontmatter.get("figlet_fallback"), False),
|
figlet_fallback=_parse_bool(frontmatter.get("figlet_fallback"), False),
|
||||||
header_spacing=max(0, _parse_int(frontmatter.get("header_spacing"), 2)),
|
header_spacing=max(0, _parse_int(frontmatter.get("header_spacing"), 2)),
|
||||||
|
wrap_code_blocks=wrap_code_blocks,
|
||||||
|
code_block_wrap_indent=code_block_wrap_indent,
|
||||||
|
code_block_line_numbers=code_block_line_numbers,
|
||||||
|
blockquote_bars=blockquote_bars,
|
||||||
)
|
)
|
||||||
return fm, remaining
|
return fm, remaining
|
||||||
|
|
||||||
|
|
@ -128,4 +148,3 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ class FrontMatter:
|
||||||
hyphen_lang: str = "en_US"
|
hyphen_lang: str = "en_US"
|
||||||
figlet_fallback: bool = False
|
figlet_fallback: bool = False
|
||||||
header_spacing: int = 2
|
header_spacing: int = 2
|
||||||
|
wrap_code_blocks: bool = False
|
||||||
|
code_block_wrap_indent: int = 2
|
||||||
|
code_block_line_numbers: bool = True
|
||||||
|
blockquote_bars: bool = True
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
128
text_renderer.py
128
text_renderer.py
|
|
@ -73,6 +73,12 @@ class TextRenderer:
|
||||||
self.hyphen_lang = frontmatter.hyphen_lang or "en_US"
|
self.hyphen_lang = frontmatter.hyphen_lang or "en_US"
|
||||||
self.figlet_fallback = frontmatter.figlet_fallback
|
self.figlet_fallback = frontmatter.figlet_fallback
|
||||||
self.header_spacing = max(0, frontmatter.header_spacing)
|
self.header_spacing = max(0, frontmatter.header_spacing)
|
||||||
|
self.wrap_code_blocks = frontmatter.wrap_code_blocks
|
||||||
|
self.code_block_line_numbers = frontmatter.code_block_line_numbers
|
||||||
|
self.blockquote_bars = frontmatter.blockquote_bars
|
||||||
|
self.wrap_code_blocks_indent = (
|
||||||
|
max(0, frontmatter.code_block_wrap_indent) if self.wrap_code_blocks else 0
|
||||||
|
)
|
||||||
self._base_style = BlockStyle(
|
self._base_style = BlockStyle(
|
||||||
align="left",
|
align="left",
|
||||||
margin_left=max(0, frontmatter.margin_left),
|
margin_left=max(0, frontmatter.margin_left),
|
||||||
|
|
@ -157,7 +163,7 @@ class TextRenderer:
|
||||||
|
|
||||||
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 = " | "
|
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(
|
wrapped = self._wrap_text(
|
||||||
processed,
|
processed,
|
||||||
|
|
@ -321,14 +327,131 @@ class TextRenderer:
|
||||||
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 []
|
||||||
width = max(2, len(str(len(lines))))
|
if self.wrap_code_blocks:
|
||||||
|
if self.code_block_line_numbers:
|
||||||
|
return self._format_wrapped_code_block_numbered(lines, style)
|
||||||
|
return self._format_wrapped_code_block_plain(lines, style)
|
||||||
|
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]:
|
||||||
margin_indent = " " * max(0, style.margin_left)
|
margin_indent = " " * max(0, style.margin_left)
|
||||||
|
width = max(2, len(str(len(lines))))
|
||||||
formatted: List[str] = []
|
formatted: List[str] = []
|
||||||
for idx, line in enumerate(lines, start=1):
|
for idx, line in enumerate(lines, start=1):
|
||||||
formatted.append(f"{margin_indent}{idx:0{width}d} | {line}")
|
formatted.append(f"{margin_indent}{idx:0{width}d} | {line}")
|
||||||
formatted.append(margin_indent if margin_indent else "")
|
formatted.append(margin_indent if margin_indent else "")
|
||||||
return formatted
|
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:
|
||||||
|
formatted.append(base_prefix)
|
||||||
|
formatted.append(margin_indent if margin_indent else "")
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
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] = []
|
||||||
|
for idx, line in enumerate(lines, start=1):
|
||||||
|
if not line:
|
||||||
|
formatted.append(prefix_template.format(num=idx))
|
||||||
|
continue
|
||||||
|
wrapped_segments = self._wrap_code_line_segments(line, content_width)
|
||||||
|
if not wrapped_segments:
|
||||||
|
formatted.append(prefix_template.format(num=idx))
|
||||||
|
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
|
||||||
|
|
||||||
|
def _wrap_code_line_segments(self, line: str, content_width: int) -> List[str]:
|
||||||
|
if not line:
|
||||||
|
return []
|
||||||
|
segments: List[str] = []
|
||||||
|
remaining = line
|
||||||
|
current_indent = 0
|
||||||
|
max_indent_allowed = max(0, content_width - 1)
|
||||||
|
while remaining:
|
||||||
|
available = max(1, content_width - current_indent)
|
||||||
|
segment, remaining = self._split_code_segment(remaining, available)
|
||||||
|
segments.append(" " * current_indent + segment)
|
||||||
|
leading = self._leading_space_count(segment)
|
||||||
|
total_indent = current_indent + leading
|
||||||
|
desired_indent = total_indent + self.wrap_code_blocks_indent
|
||||||
|
current_indent = min(max_indent_allowed, desired_indent)
|
||||||
|
return segments
|
||||||
|
|
||||||
|
def _split_code_segment(self, text: str, max_width: int) -> tuple[str, str]:
|
||||||
|
if max_width <= 0:
|
||||||
|
max_width = 1
|
||||||
|
if len(text) <= max_width:
|
||||||
|
return text, ""
|
||||||
|
slice_text = text[:max_width]
|
||||||
|
break_pos = -1
|
||||||
|
for idx, char in enumerate(slice_text):
|
||||||
|
if char in {" ", "\t"}:
|
||||||
|
break_pos = idx
|
||||||
|
if break_pos <= 0:
|
||||||
|
segment = slice_text
|
||||||
|
remainder = text[len(segment) :]
|
||||||
|
else:
|
||||||
|
segment = text[:break_pos]
|
||||||
|
remainder = text[break_pos:]
|
||||||
|
if not segment:
|
||||||
|
segment = slice_text
|
||||||
|
remainder = text[len(segment) :]
|
||||||
|
return segment, remainder
|
||||||
|
|
||||||
|
def _leading_space_count(self, text: str) -> int:
|
||||||
|
count = 0
|
||||||
|
for char in text:
|
||||||
|
if char == " ":
|
||||||
|
count += 1
|
||||||
|
elif char == "\t":
|
||||||
|
count += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return count
|
||||||
|
|
||||||
def _process_inline(self, text: str) -> str:
|
def _process_inline(self, text: str) -> str:
|
||||||
code_segments: List[str] = []
|
code_segments: List[str] = []
|
||||||
|
|
||||||
|
|
@ -759,4 +882,3 @@ class TextRenderer:
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
self.output.extend([""] * (self.header_spacing - existing))
|
self.output.extend([""] * (self.header_spacing - existing))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue