diff --git a/examples/loremipsum.md b/examples/loremipsum.md
index 9d82d30..48af7f4 100644
--- a/examples/loremipsum.md
+++ b/examples/loremipsum.md
@@ -41,20 +41,11 @@ populifer adeunt quicquam cum **ales** Ixion angulus placet reddere.
## Tractaque serpens arva quoque
{:.center}
-### Unordered
+# Include
-+ Create a list by starting a line with `+`, `-`, or `*`
-- Marker character change forces new list start:
-* Ac tristique libero volutpat at jesrh hjsrh sejr hhsjehv sjtrhg jrdth
-+ Facilisis in pretium nisl aliquet
-- Nulla volutpat aliquam velit
-+ Very easy
+#[d1 :left](dragon.txt) #[d1 :center :mirror](dragon.txt) #[d1 :right](dragon.txt)
-### Ordered
-
-1. Lorem ipsum dolor sit amet
-2. Consectetur adipiscing elit
-3. Integer molestie lorem at massa
+![[markdown.md]]
Inprudens dum memor alma, casses dedi Sinuessa iam quid adicit
`gate_truncate_wave`. [Geratur de](#salientia-iungit-contra-et), insolida
diff --git a/markdown_parser.py b/markdown_parser.py
index 6ed08be..a49086f 100644
--- a/markdown_parser.py
+++ b/markdown_parser.py
@@ -1,9 +1,13 @@
from __future__ import annotations
+import json
import re
-from typing import Iterable, Iterator, List, Optional, Union
+from pathlib import Path
+from typing import Dict, Iterable, Iterator, List, Optional, Union
from md_types import (
+ AsciiArtPayload,
+ AsciiArtPiece,
BlockEvent,
BlockKind,
BlockQuotePayload,
@@ -27,6 +31,7 @@ PARA_OPEN_RE = re.compile(r"^\s*
]*)>\s*$", re.IGNORECASE)
PARA_CLOSE_RE = re.compile(r"^\s*
\s*$", re.IGNORECASE)
MMD_ATTR_LINE_RE = re.compile(r"^\{\s*:(.+)\}\s*$")
MMD_ATTR_TAIL_RE = re.compile(r"(.*?)\s*\{\s*:(.+?)\}\s*$")
+ASCII_SENTINEL_PREFIX = "\u0000ASCII:"
class MarkdownParser:
@@ -36,6 +41,7 @@ class MarkdownParser:
self._paragraph_style_spec: Optional[StyleSpec] = None
self._pending_block_style_spec: Optional[StyleSpec] = None
self._last_stylable_block: bool = False
+ self._ascii_cache: Dict[str, List[str]] = {}
def parse(self, lines: Iterable[str]) -> Iterator[Union[BlockEvent, StyleUpdateEvent]]:
self._reset_state()
@@ -48,6 +54,23 @@ class MarkdownParser:
for raw_line in iterator:
line = raw_line.rstrip("\n")
+ if line.startswith(ASCII_SENTINEL_PREFIX):
+ event = self._flush_paragraph(current_paragraph)
+ if event is not None:
+ yield event
+ current_paragraph = []
+ payload = self._build_ascii_payload(line)
+ style = self._combine_styles(self._current_style(), self._pending_block_style_spec)
+ self._pending_block_style_spec = None
+ self._last_stylable_block = True
+ yield BlockEvent(
+ kind=BlockKind.CUSTOM_BLOCK,
+ payload=payload,
+ style=style,
+ stylable=True,
+ )
+ continue
+
if in_code_block:
if line.strip().startswith("```"):
event = self._flush_code_block(code_lines)
@@ -297,6 +320,69 @@ class MarkdownParser:
stylable=False,
)
+ def _build_ascii_payload(self, sentinel_line: str) -> AsciiArtPayload:
+ entries = self._decode_ascii_sentinel(sentinel_line)
+ pieces: List[AsciiArtPiece] = []
+ for entry in entries:
+ lines = self._load_ascii_art_lines(entry["path"])
+ pieces.append(
+ AsciiArtPiece(
+ block_type=entry["type"],
+ name=entry["name"],
+ path=entry["path"],
+ lines=lines,
+ align=entry["align"],
+ )
+ )
+ return AsciiArtPayload(pieces=pieces)
+
+ def _decode_ascii_sentinel(self, line: str) -> List[Dict[str, str]]:
+ payload = line[len(ASCII_SENTINEL_PREFIX) :]
+ try:
+ data = json.loads(payload)
+ except json.JSONDecodeError as exc:
+ raise ValueError(f"Invalid ASCII sentinel payload: {payload}") from exc
+ if "pieces" in data and isinstance(data["pieces"], list):
+ raw_pieces = data["pieces"]
+ else:
+ raw_pieces = [data]
+ pieces: List[Dict[str, str]] = []
+ for entry in raw_pieces:
+ for key in ("type", "name", "path"):
+ if key not in entry:
+ raise ValueError(f"ASCII sentinel missing '{key}' field: {payload}")
+ align = entry.get("align")
+ align_str = str(align) if align is not None else None
+ if align_str is not None:
+ align_lower = align_str.strip().lower()
+ if align_lower == "centre":
+ align_lower = "center"
+ if align_lower not in {"left", "center", "right"}:
+ align_lower = None
+ else:
+ align_lower = None
+ pieces.append(
+ {
+ "type": str(entry["type"]),
+ "name": str(entry["name"]),
+ "path": str(entry["path"]),
+ "align": align_lower,
+ }
+ )
+ return pieces
+
+ def _load_ascii_art_lines(self, path_str: str) -> List[str]:
+ cached = self._ascii_cache.get(path_str)
+ if cached is not None:
+ return list(cached)
+ path = Path(path_str)
+ if not path.exists():
+ raise FileNotFoundError(f"ASCII art file '{path}' was not found.")
+ text = path.read_text(encoding="utf-8")
+ lines = text.splitlines()
+ self._ascii_cache[path_str] = lines
+ return list(lines)
+
def _make_base_style(self) -> BlockStyle:
return BlockStyle(
align=self._base_style.align,
diff --git a/md2txt.py b/md2txt.py
index 010170e..e115f91 100644
--- a/md2txt.py
+++ b/md2txt.py
@@ -5,10 +5,11 @@ Convert Markdown into 80-column DOS-compatible plain text.
from __future__ import annotations
import argparse
+import json
import re
import sys
from pathlib import Path
-from typing import Iterable, List, Optional, Tuple
+from typing import Iterable, List, Optional, Set, Tuple
from md_types import BlockStyle, FrontMatter
from markdown_parser import MarkdownParser
@@ -16,6 +17,14 @@ from text_renderer import TextRenderer
FRONTMATTER_PATTERN = re.compile(r"^---\s*$")
+INCLUDE_WIKILINK_PATTERN = re.compile(r"^\s*!\[\[(.+?)\]\]\s*$")
+INCLUDE_DIRECTIVE_PATTERN = re.compile(r"^\s*\{\s*\.include\s+(.+?)\s*\}\s*$")
+ASCII_BLOCK_PATTERN = re.compile(
+ r"^\s*#\[(?P