init
This commit is contained in:
commit
3d889ec117
14 changed files with 1722 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
69
README.md
Normal file
69
README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# md2amb utilities
|
||||||
|
|
||||||
|
This repository contains command line helpers for transforming Markdown into formats that work well on retro hardware or constrained text viewers.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
- 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~`.
|
||||||
|
- Blockquotes prefixed by `| `, lists preserved, inline code untouched.
|
||||||
|
- Code blocks numbered (`01 | line`), with both fenced and indented fences supported.
|
||||||
|
- Links transformed into `[label](n)` references, with footnote-style URL list at the end.
|
||||||
|
- Optional alignment and margin controls via HTML `<p>` attributes or MultiMarkdown attribute blocks (e.g. `{:.center margin=20px}`) applied as leading spaces.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.9+.
|
||||||
|
- Optional: `pyfiglet` for FIGlet headings (`pip install pyfiglet`). When unavailable, headings fall back to the H4-style renderer automatically.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Both scripts accept `--help` for the full option list.
|
||||||
|
|
||||||
|
## FIGlet Fonts via Frontmatter
|
||||||
|
|
||||||
|
You can set FIGlet fonts for H1–H3 per document using YAML frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
h1_font: slant
|
||||||
|
h2_font: standard
|
||||||
|
h3_font: small
|
||||||
|
---
|
||||||
|
|
||||||
|
# Main Title
|
||||||
|
## Section Heading
|
||||||
|
### Subsection
|
||||||
|
```
|
||||||
|
|
||||||
|
If the rendered FIGlet text would exceed the configured width, the converter automatically falls back to the H4-style uppercase heading with a dashed underline.
|
||||||
|
|
||||||
|
## Styling via Attribute Blocks
|
||||||
|
|
||||||
|
You can influence alignment and margins in the source Markdown using either HTML paragraphs or MultiMarkdown attribute blocks:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<p align="center" style="margin: 10px 20px;">
|
||||||
|
Centered paragraph with custom margins.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Paragraph styled via MMD attributes.
|
||||||
|
{: .right margin=12px }
|
||||||
|
|
||||||
|
## Centered Heading {: .center margin=8px }
|
||||||
|
```
|
||||||
|
|
||||||
|
Margins are interpreted as spaces; values using `px` are rounded to the nearest integer, and `margin: 0 auto;` centers content.
|
||||||
|
|
||||||
|
## Output Line Endings
|
||||||
|
|
||||||
|
Generated text uses CRLF line endings to maintain DOS compatibility.
|
||||||
79
loremipsum.md
Normal file
79
loremipsum.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
---
|
||||||
|
h1_font: colossal
|
||||||
|
h2_font: big
|
||||||
|
h3_font: small
|
||||||
|
---
|
||||||
|
|
||||||
|
# Salientia
|
||||||
|
{:.center}
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
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.
|
||||||
167
loremipsum.txt
Normal file
167
loremipsum.txt
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
|
||||||
|
.d8888b. 888d8b 888 d8b
|
||||||
|
d88P Y88b 888Y8P 888 Y8P
|
||||||
|
Y88b. 888 888
|
||||||
|
"Y888b. 8888b. 888888 .d88b. 88888b. 888888888 8888b.
|
||||||
|
"Y88b. "88b888888d8P Y8b888 "88b888 888 "88b
|
||||||
|
"888.d88888888888888888888888 888888 888.d888888
|
||||||
|
Y88b d88P888 888888888Y8b. 888 888Y88b. 888888 888
|
||||||
|
"Y8888P" "Y888888888888 "Y8888 888 888 "Y888888"Y888888
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_____ _ _ _ _
|
||||||
|
/ ____| | | | | | (_)
|
||||||
|
| (___ | |_ _ __ _ _ ___| |_ __ _ | |__ _ _ __ ___
|
||||||
|
\___ \| __| '__| | | |/ __| __/ _` | | '_ \| | '_ \ / __|
|
||||||
|
____) | |_| | | |_| | (__| || (_| | | | | | | | | | (__
|
||||||
|
|_____/ \__|_| \__,_|\___|\__\__,_| |_| |_|_|_| |_|\___|
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ __ _
|
||||||
|
| | (_) | / _| | |
|
||||||
|
__ _____| |_ _| |_ _ _ ___ | |_ __ _ ___| |_ _ _ _ __ ___
|
||||||
|
\ \ / / _ \ __| | __| | | / __| | _/ _` |/ __| __| | | | '_ ` _ \
|
||||||
|
\ V / __/ |_| | |_| |_| \__ \ | || (_| | (__| |_| |_| | | | | | |
|
||||||
|
\_/ \___|\__|_|\__|\__,_|___/ |_| \__,_|\___|\__|\__,_|_| |_| |_|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
______ _ _ _ _
|
||||||
|
| ____| (_) | (_) | |
|
||||||
|
| |__ _ _ _| |_ __ ___ __| | ___ ___
|
||||||
|
| __| | | | | __| \ \ / / |/ _` |/ _ \/ _ \
|
||||||
|
| | | |_| | | |_ \ V /| | (_| | __/ (_) |
|
||||||
|
|_| \__,_|_|\__| \_/ |_|\__,_|\___|\___/
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ _ _
|
||||||
|
| | (_) | | | (_)
|
||||||
|
| | ___ _ __ __ _ _ __ _ _ _ ___ __| | ___ __| |_ ___ ___ ___
|
||||||
|
| |/ _ \| '_ \ / _` | |/ _` | | | |/ _ \ / _` |/ _ \/ _` | / __/ __|/ _ \
|
||||||
|
| | (_) | | | | (_| | | (_| | |_| | __/ | (_| | __/ (_| | \__ \__ \ __/
|
||||||
|
|_|\___/|_| |_|\__, |_|\__, |\__,_|\___| \__,_|\___|\__,_|_|___/___/\___|
|
||||||
|
__/ | | |
|
||||||
|
|___/ |_|
|
||||||
|
|
||||||
|
|
||||||
|
Ille stupet, ultra I N C U N A B U L A A G E N D U M U R B E S mirer A P E
|
||||||
|
R, metu. Aegides novissima sunt turis quatiebant umbrosaque [putares](1).
|
||||||
|
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 t e r g a P a r n a s i
|
||||||
|
a s e n t e n t i a omnia inmotusque, est et aetas sonus. Exhibuit coeperunt,
|
||||||
|
canis. Suis minora, tempore fateor neve moderamine erit. Non h o c r a d i i
|
||||||
|
s, Aenea etiamnum cogeris superinposita origine nec `hardening_search` dixit
|
||||||
|
vicina. Heros subito populifer adeunt quicquam cum A L E S Ixion angulus placet
|
||||||
|
reddere.
|
||||||
|
|
||||||
|
_______ _
|
||||||
|
|__ __| | |
|
||||||
|
| |_ __ __ _ ___| |_ __ _ __ _ _ _ ___
|
||||||
|
| | '__/ _` |/ __| __/ _` |/ _` | | | |/ _ \
|
||||||
|
| | | | (_| | (__| || (_| | (_| | |_| | __/
|
||||||
|
|_|_| \__,_|\___|\__\__,_|\__, |\__,_|\___|
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
___ ___ _ __ _ __ ___ _ __ ___ __ _ _ ____ ____ _
|
||||||
|
/ __|/ _ \ '__| '_ \ / _ \ '_ \/ __| / _` | '__\ \ / / _` |
|
||||||
|
\__ \ __/ | | |_) | __/ | | \__ \ | (_| | | \ V / (_| |
|
||||||
|
|___/\___|_| | .__/ \___|_| |_|___/ \__,_|_| \_/ \__,_|
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
__ _ _ _ ___ __ _ _ _ ___
|
||||||
|
/ _` | | | |/ _ \ / _` | | | |/ _ \
|
||||||
|
| (_| | |_| | (_) | (_| | |_| | __/
|
||||||
|
\__, |\__,_|\___/ \__, |\__,_|\___|
|
||||||
|
| | | |
|
||||||
|
|_| |_|
|
||||||
|
|
||||||
|
|
||||||
|
Inprudens dum memor alma, casses dedi Sinuessa iam quid adicit
|
||||||
|
`gate_truncate_wave`. [Geratur de](2), 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 C A P Y S R E C O R D O R tenebras actis
|
||||||
|
signis, reliquit, placidissime iram liquefactis longe nervi famem. Cura pars
|
||||||
|
natique velleris semel et at `printerWildcardParallel` inquit positosque comites
|
||||||
|
ultra.
|
||||||
|
|
||||||
|
____ _
|
||||||
|
/ __ \ | |
|
||||||
|
| | | |_ _ __ _ _ __ | |_ __ _ __ _ _ _ ___
|
||||||
|
| | | | | | |/ _` | '_ \| __/ _` |/ _` | | | |/ _ \
|
||||||
|
| |__| | |_| | (_| | | | | || (_| | (_| | |_| | __/
|
||||||
|
\___\_\\__,_|\__,_|_| |_|\__\__,_|\__, |\__,_|\___|
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
_
|
||||||
|
| |
|
||||||
|
__ _ _ _ __ _ ___ _ __ | |_
|
||||||
|
/ _` | | | |/ _` |/ _ \ '_ \| __|
|
||||||
|
| (_| | |_| | (_| | __/ | | | |_
|
||||||
|
\__,_|\__,_|\__, |\___|_| |_|\__|
|
||||||
|
__/ |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
|
||||||
|
Acuti ignotis ignis, ostendit nostras terras nondum, [petit utque utero](3)
|
||||||
|
sepulcrales, pluma reseratis Ismario. Tria silentia duae n y m p h e: certe
|
||||||
|
parva cernam et dixit. [Verborum sedisti](1).
|
||||||
|
|
||||||
|
01 | if (firewall(4, 3) == 1 * device_media_parity) {
|
||||||
|
02 | system_copy_class += systray + oasis_wi_clean + lpi;
|
||||||
|
03 | modelLdap.search_secondary_socket *= bounce.proxy_text(cableMyspacePerl,
|
||||||
|
04 | scanner(spam));
|
||||||
|
05 | text_class_traceroute(vga, hdtv);
|
||||||
|
06 | }
|
||||||
|
07 | if (commerceStack) {
|
||||||
|
08 | wrapSafe(pum(itunesRosettaCache, sdramOpticalWorm));
|
||||||
|
09 | } else {
|
||||||
|
10 | minisite_worm(2, ssdString(84), gnuFriendly);
|
||||||
|
11 | state.qwerty(moduleJavascriptFloating + ugc, koffice,
|
||||||
|
12 | mail_system.prebinding_service_teraflops(mediaIcmpBoot));
|
||||||
|
13 | }
|
||||||
|
14 | memoryHexadecimalMultitasking += oop_android;
|
||||||
|
15 | var passive_vdu = bar;
|
||||||
|
16 | var ddrJava = leaf(unc_web);
|
||||||
|
|
||||||
|
|
||||||
|
__ __ _ _ _
|
||||||
|
| \/ (_) | |___ _ __ __ _ _ ___ ____ _
|
||||||
|
| |\/| | | | / -_) | '_ \/ _` | '_\ V / _` |
|
||||||
|
|_| |_|_|_|_\___| | .__/\__,_|_| \_/\__,_|
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
M I N E T U R E T aurum! Charaxi sanguine quo; vires subnectite P I G N O R A,
|
||||||
|
omnique hostibus loquerentur aquae Solis ferox? C U M N E, 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 t e c u m l a n i a t q u e d i x i t
|
||||||
|
| ad datas `adsl`.
|
||||||
|
| Vino u t quod iaculi mutua armis uno est vos totidemque
|
||||||
|
| [exhortor](4). Iamdudum Romanae cepi linguisque
|
||||||
|
| velit accipe idem acutis hoc sparsumque dextra. Minoide sponte collo sequitur
|
||||||
|
| et intus flere sanguineaque herba, nox Quas adsiduo c u n c t a n t e m tibi;
|
||||||
|
| enim.
|
||||||
|
|
||||||
|
[1] #mille-parva
|
||||||
|
[2] #salientia-iungit-contra-et
|
||||||
|
[3] #structa-hinc-vetitus-factum
|
||||||
|
[4] #tractaque-serpens-arva-quoque
|
||||||
152
loremipsum_60.txt
Normal file
152
loremipsum_60.txt
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
|
||||||
|
.d8888b. 888d8b 888 d8b
|
||||||
|
d88P Y88b 888Y8P 888 Y8P
|
||||||
|
Y88b. 888 888
|
||||||
|
"Y888b. 8888b. 888888 .d88b. 88888b. 888888888 8888b.
|
||||||
|
"Y88b. "88b888888d8P Y8b888 "88b888 888 "88b
|
||||||
|
"888.d88888888888888888888888 888888 888.d888888
|
||||||
|
Y88b d88P888 888888888Y8b. 888 888Y88b. 888888 888
|
||||||
|
"Y8888P" "Y888888888888 "Y8888 888 888 "Y888888"Y888888
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 I N C U N A B U L A A G E N D U M U
|
||||||
|
R B E S mirer A P E R , metu. Aegides novissima sunt
|
||||||
|
turis quatiebant umbrosaque [putares](1). 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 t e
|
||||||
|
r g a P a r n a s i a s e n t e n t i a omnia
|
||||||
|
inmotusque, est et aetas sonus. Exhibuit coeperunt, canis.
|
||||||
|
Suis minora, tempore fateor neve moderamine erit. Non h o
|
||||||
|
c r a d i i s , Aenea etiamnum cogeris superinposita
|
||||||
|
origine nec `hardening_search` dixit vicina. Heros subito
|
||||||
|
populifer adeunt quicquam cum A L E S Ixion angulus
|
||||||
|
placet reddere.
|
||||||
|
|
||||||
|
_______ _
|
||||||
|
|__ __| | |
|
||||||
|
| |_ __ __ _ ___| |_ __ _ __ _ _ _ ___
|
||||||
|
| | '__/ _` |/ __| __/ _` |/ _` | | | |/ _ \
|
||||||
|
| | | | (_| | (__| || (_| | (_| | |_| | __/
|
||||||
|
|_|_| \__,_|\___|\__\__,_|\__, |\__,_|\___|
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
___ ___ _ __ _ __ ___ _ __ ___ __ _ _ ____ ____ _
|
||||||
|
/ __|/ _ \ '__| '_ \ / _ \ '_ \/ __| / _` | '__\ \ / / _` |
|
||||||
|
\__ \ __/ | | |_) | __/ | | \__ \ | (_| | | \ V / (_| |
|
||||||
|
|___/\___|_| | .__/ \___|_| |_|___/ \__,_|_| \_/ \__,_|
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
__ _ _ _ ___ __ _ _ _ ___
|
||||||
|
/ _` | | | |/ _ \ / _` | | | |/ _ \
|
||||||
|
| (_| | |_| | (_) | (_| | |_| | __/
|
||||||
|
\__, |\__,_|\___/ \__, |\__,_|\___|
|
||||||
|
| | | |
|
||||||
|
|_| |_|
|
||||||
|
|
||||||
|
|
||||||
|
Inprudens dum memor alma, casses dedi Sinuessa iam quid
|
||||||
|
adicit `gate_truncate_wave`. [Geratur de](2), 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 C A P Y S
|
||||||
|
R E C O R D O R tenebras actis signis, reliquit,
|
||||||
|
placidissime iram liquefactis longe nervi famem. Cura pars
|
||||||
|
natique velleris semel et at `printerWildcardParallel`
|
||||||
|
inquit positosque comites ultra.
|
||||||
|
|
||||||
|
____ _
|
||||||
|
/ __ \ | |
|
||||||
|
| | | |_ _ __ _ _ __ | |_ __ _ __ _ _ _ ___
|
||||||
|
| | | | | | |/ _` | '_ \| __/ _` |/ _` | | | |/ _ \
|
||||||
|
| |__| | |_| | (_| | | | | || (_| | (_| | |_| | __/
|
||||||
|
\___\_\\__,_|\__,_|_| |_|\__\__,_|\__, |\__,_|\___|
|
||||||
|
| |
|
||||||
|
|_|
|
||||||
|
_
|
||||||
|
| |
|
||||||
|
__ _ _ _ __ _ ___ _ __ | |_
|
||||||
|
/ _` | | | |/ _` |/ _ \ '_ \| __|
|
||||||
|
| (_| | |_| | (_| | __/ | | | |_
|
||||||
|
\__,_|\__,_|\__, |\___|_| |_|\__|
|
||||||
|
__/ |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
|
||||||
|
Acuti ignotis ignis, ostendit nostras terras nondum, [petit
|
||||||
|
utque utero](3) sepulcrales, pluma reseratis Ismario. Tria
|
||||||
|
silentia duae n y m p h e : certe parva cernam et dixit.
|
||||||
|
[Verborum sedisti](1).
|
||||||
|
|
||||||
|
01 | if (firewall(4, 3) == 1 * device_media_parity) {
|
||||||
|
02 | system_copy_class += systray + oasis_wi_clean + lpi;
|
||||||
|
03 | modelLdap.search_secondary_socket *= bounce.proxy_text(cableMyspacePerl,
|
||||||
|
04 | scanner(spam));
|
||||||
|
05 | text_class_traceroute(vga, hdtv);
|
||||||
|
06 | }
|
||||||
|
07 | if (commerceStack) {
|
||||||
|
08 | wrapSafe(pum(itunesRosettaCache, sdramOpticalWorm));
|
||||||
|
09 | } else {
|
||||||
|
10 | minisite_worm(2, ssdString(84), gnuFriendly);
|
||||||
|
11 | state.qwerty(moduleJavascriptFloating + ugc, koffice,
|
||||||
|
12 | mail_system.prebinding_service_teraflops(mediaIcmpBoot));
|
||||||
|
13 | }
|
||||||
|
14 | memoryHexadecimalMultitasking += oop_android;
|
||||||
|
15 | var passive_vdu = bar;
|
||||||
|
16 | var ddrJava = leaf(unc_web);
|
||||||
|
|
||||||
|
|
||||||
|
__ __ _ _ _
|
||||||
|
| \/ (_) | |___ _ __ __ _ _ ___ ____ _
|
||||||
|
| |\/| | | | / -_) | '_ \/ _` | '_\ V / _` |
|
||||||
|
|_| |_|_|_|_\___| | .__/\__,_|_| \_/\__,_|
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
M I N E T U R E T aurum! Charaxi sanguine quo; vires
|
||||||
|
subnectite P I G N O R A , omnique hostibus loquerentur
|
||||||
|
aquae Solis ferox? C U M N E , 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 t e c u m l a n i
|
||||||
|
| a t q u e d i x i t ad datas `adsl`.
|
||||||
|
| Vino u t quod iaculi mutua armis uno est vos
|
||||||
|
| totidemque
|
||||||
|
| [exhortor](4). Iamdudum Romanae cepi linguisque
|
||||||
|
| velit accipe idem acutis hoc sparsumque dextra. Minoide
|
||||||
|
| sponte collo sequitur
|
||||||
|
| et intus flere sanguineaque herba, nox Quas adsiduo c
|
||||||
|
| u n c t a n t e m tibi; enim.
|
||||||
|
|
||||||
|
[1] #mille-parva
|
||||||
|
[2] #salientia-iungit-contra-et
|
||||||
|
[3] #structa-hinc-vetitus-factum
|
||||||
|
[4] #tractaque-serpens-arva-quoque
|
||||||
6
main.py
Normal file
6
main.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
def main():
|
||||||
|
print("Hello from md2amb!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
84
markdown.md
Normal file
84
markdown.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
``` js
|
||||||
|
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
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
0
md2amb.py
Normal file
0
md2amb.py
Normal file
958
md2txt.py
Normal file
958
md2txt.py
Normal file
|
|
@ -0,0 +1,958 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Convert Markdown into 80-column DOS-compatible plain text.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyfiglet import Figlet, FontNotFound
|
||||||
|
except ImportError: # pragma: no cover - emit a helpful error at runtime instead
|
||||||
|
Figlet = None # type: ignore[assignment]
|
||||||
|
FontNotFound = ValueError # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
|
FRONTMATTER_PATTERN = re.compile(r"^---\s*$")
|
||||||
|
HEADING_PATTERN = re.compile(r"^(#{1,6})\s+(.*)$")
|
||||||
|
ORDERED_LIST_PATTERN = re.compile(r"^(\s*)(\d+\.)(\s+)(.*)$")
|
||||||
|
UNORDERED_LIST_PATTERN = re.compile(r"^(\s*)([*+-])(\s+)(.*)$")
|
||||||
|
BLOCKQUOTE_PATTERN = re.compile(r"^\s{0,3}>(.*)$")
|
||||||
|
HORIZONTAL_RULE_PATTERN = re.compile(r"^\s*([-*_])(?:\s*\1){2,}\s*$")
|
||||||
|
INLINE_PARA_RE = re.compile(r"^\s*<p\b([^>]*)>(.*?)</p>\s*$", re.IGNORECASE)
|
||||||
|
PARA_OPEN_RE = re.compile(r"^\s*<p\b([^>]*)>\s*$", re.IGNORECASE)
|
||||||
|
PARA_CLOSE_RE = re.compile(r"^\s*</p>\s*$", re.IGNORECASE)
|
||||||
|
MMD_ATTR_LINE_RE = re.compile(r"^\{\s*:(.+)\}\s*$")
|
||||||
|
MMD_ATTR_TAIL_RE = re.compile(r"(.*?)\s*\{\s*:(.+?)\}\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlockStyle:
|
||||||
|
align: str = "left"
|
||||||
|
margin_left: int = 0
|
||||||
|
margin_right: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StyleSpec:
|
||||||
|
align: Optional[str] = None
|
||||||
|
margin_left: Optional[int] = None
|
||||||
|
margin_right: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlockRecord:
|
||||||
|
start: int
|
||||||
|
length: int
|
||||||
|
render: Callable[[BlockStyle], List[str]]
|
||||||
|
style: BlockStyle
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FrontMatter:
|
||||||
|
h1_font: str = "standard"
|
||||||
|
h2_font: str = "standard"
|
||||||
|
h3_font: str = "standard"
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownToTxtConverter:
|
||||||
|
def __init__(self, width: int = 80, frontmatter: Optional[FrontMatter] = None) -> None:
|
||||||
|
self.width = width
|
||||||
|
self.frontmatter = frontmatter or FrontMatter()
|
||||||
|
self.links: List[Tuple[int, str]] = []
|
||||||
|
self.link_indices: Dict[str, int] = {}
|
||||||
|
self.figlets: Dict[str, Figlet] = {}
|
||||||
|
self._style_stack: List[BlockStyle] = [BlockStyle()]
|
||||||
|
self._paragraph_style_spec: Optional[StyleSpec] = None
|
||||||
|
self._pending_block_style_spec: Optional[StyleSpec] = None
|
||||||
|
self._last_stylable_block: Optional[BlockRecord] = None
|
||||||
|
|
||||||
|
def convert(self, lines: Iterable[str]) -> List[str]:
|
||||||
|
output: List[str] = []
|
||||||
|
self.links = []
|
||||||
|
self.link_indices = {}
|
||||||
|
self._style_stack = [BlockStyle()]
|
||||||
|
self._paragraph_style_spec = None
|
||||||
|
self._pending_block_style_spec = None
|
||||||
|
self._last_stylable_block = None
|
||||||
|
|
||||||
|
iterator = iter(lines)
|
||||||
|
in_code_block = False
|
||||||
|
code_lines: List[str] = []
|
||||||
|
indented_code_lines: List[str] = []
|
||||||
|
current_paragraph: List[str] = []
|
||||||
|
|
||||||
|
for raw_line in iterator:
|
||||||
|
line = raw_line.rstrip("\n")
|
||||||
|
|
||||||
|
if in_code_block:
|
||||||
|
if line.strip().startswith("```"):
|
||||||
|
self._emit_block(output, self._flush_code_block(code_lines))
|
||||||
|
code_lines = []
|
||||||
|
in_code_block = False
|
||||||
|
else:
|
||||||
|
code_lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if indented_code_lines:
|
||||||
|
if line.startswith(" "):
|
||||||
|
indented_code_lines.append(line[4:])
|
||||||
|
continue
|
||||||
|
if not line.strip():
|
||||||
|
self._emit_block(output, self._flush_code_block(indented_code_lines))
|
||||||
|
indented_code_lines = []
|
||||||
|
else:
|
||||||
|
self._emit_block(output, self._flush_code_block(indented_code_lines))
|
||||||
|
indented_code_lines = []
|
||||||
|
|
||||||
|
inline_para = INLINE_PARA_RE.match(line.strip())
|
||||||
|
if inline_para:
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
spec = self._style_spec_from_html_attributes(inline_para.group(1) or "")
|
||||||
|
self._push_style(spec)
|
||||||
|
content = inline_para.group(2)
|
||||||
|
if content:
|
||||||
|
current_paragraph.append(content)
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
self._pop_style()
|
||||||
|
continue
|
||||||
|
|
||||||
|
open_para = PARA_OPEN_RE.match(line)
|
||||||
|
if open_para:
|
||||||
|
spec = self._style_spec_from_html_attributes(open_para.group(1) or "")
|
||||||
|
self._push_style(spec)
|
||||||
|
continue
|
||||||
|
|
||||||
|
close_para = PARA_CLOSE_RE.match(line)
|
||||||
|
if close_para:
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
self._paragraph_style_spec = None
|
||||||
|
self._pop_style()
|
||||||
|
continue
|
||||||
|
|
||||||
|
stripped = line.strip()
|
||||||
|
attr_match = MMD_ATTR_LINE_RE.match(stripped)
|
||||||
|
if attr_match:
|
||||||
|
spec = self._parse_style_spec_from_tokens(attr_match.group(1))
|
||||||
|
if spec:
|
||||||
|
if current_paragraph:
|
||||||
|
self._paragraph_style_spec = self._merge_specs(self._paragraph_style_spec, spec)
|
||||||
|
elif self._last_stylable_block is not None:
|
||||||
|
self._apply_style_to_last_block(output, spec)
|
||||||
|
else:
|
||||||
|
self._pending_block_style_spec = self._merge_specs(self._pending_block_style_spec, spec)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip().startswith("```"):
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
in_code_block = True
|
||||||
|
code_lines = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith(" "):
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
indented_code_lines = [line[4:]]
|
||||||
|
continue
|
||||||
|
|
||||||
|
heading_match = HEADING_PATTERN.match(line)
|
||||||
|
if heading_match:
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
level = len(heading_match.group(1))
|
||||||
|
heading_text = heading_match.group(2).strip()
|
||||||
|
heading_text, inline_spec = self._extract_trailing_attr(heading_text)
|
||||||
|
combined_spec = self._merge_specs(self._pending_block_style_spec, inline_spec)
|
||||||
|
style = self._combine_styles(self._current_style(), combined_spec)
|
||||||
|
render = lambda s: self._render_heading_lines(level, heading_text, s)
|
||||||
|
self._emit_block(output, render(style), stylable=True, render_fn=render, style=style)
|
||||||
|
self._pending_block_style_spec = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if HORIZONTAL_RULE_PATTERN.match(line):
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
self._emit_block(output, [self._render_horizontal_rule(self._current_style())])
|
||||||
|
continue
|
||||||
|
|
||||||
|
if BLOCKQUOTE_PATTERN.match(line):
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
self._handle_blockquote(line, output)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if UNORDERED_LIST_PATTERN.match(line) or ORDERED_LIST_PATTERN.match(line):
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
current_paragraph = []
|
||||||
|
self._handle_list_line(line, output)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not line.strip():
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
output.append("")
|
||||||
|
current_paragraph = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_paragraph.append(line)
|
||||||
|
|
||||||
|
self._flush_paragraph(current_paragraph, output)
|
||||||
|
if in_code_block:
|
||||||
|
self._emit_block(output, self._flush_code_block(code_lines))
|
||||||
|
if indented_code_lines:
|
||||||
|
self._emit_block(output, self._flush_code_block(indented_code_lines))
|
||||||
|
|
||||||
|
if self.links:
|
||||||
|
if output and output[-1] != "":
|
||||||
|
output.append("")
|
||||||
|
for index, url in self.links:
|
||||||
|
entry = f"[{index}] {url}"
|
||||||
|
output.extend(self._wrap_text(entry, initial_indent="", subsequent_indent="", style=BlockStyle()))
|
||||||
|
self._last_stylable_block = None
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _flush_paragraph(self, paragraph_lines: List[str], output: List[str]) -> None:
|
||||||
|
if not paragraph_lines:
|
||||||
|
return
|
||||||
|
text = " ".join(line.strip() for line in paragraph_lines)
|
||||||
|
processed = self._process_inline(text)
|
||||||
|
combined_spec = self._merge_specs(self._pending_block_style_spec, self._paragraph_style_spec)
|
||||||
|
style = self._combine_styles(self._current_style(), combined_spec)
|
||||||
|
|
||||||
|
def render(target_style: BlockStyle) -> List[str]:
|
||||||
|
return self._wrap_text(processed, style=target_style)
|
||||||
|
|
||||||
|
lines = render(style)
|
||||||
|
self._emit_block(output, lines, stylable=True, render_fn=render, style=style)
|
||||||
|
paragraph_lines.clear()
|
||||||
|
self._paragraph_style_spec = None
|
||||||
|
self._pending_block_style_spec = None
|
||||||
|
|
||||||
|
def _flush_code_block(self, code_lines: List[str]) -> List[str]:
|
||||||
|
if not code_lines:
|
||||||
|
return []
|
||||||
|
width = max(2, len(str(len(code_lines))))
|
||||||
|
formatted: List[str] = []
|
||||||
|
for idx, line in enumerate(code_lines, start=1):
|
||||||
|
formatted.append(f" {idx:0{width}d} | {line}")
|
||||||
|
formatted.append("")
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
def _handle_blockquote(self, line: str, output: List[str]) -> None:
|
||||||
|
# Normalize a single blockquote line before wrapping.
|
||||||
|
depth = 0
|
||||||
|
content = line
|
||||||
|
while content.lstrip().startswith(">"):
|
||||||
|
depth += 1
|
||||||
|
content = content.lstrip()[1:]
|
||||||
|
content = content.lstrip()
|
||||||
|
processed = self._process_inline(content)
|
||||||
|
indent_unit = " | "
|
||||||
|
indent = indent_unit * max(1, depth)
|
||||||
|
wrapped = self._wrap_text(
|
||||||
|
processed,
|
||||||
|
initial_indent=indent,
|
||||||
|
subsequent_indent=indent,
|
||||||
|
style=self._current_style(),
|
||||||
|
)
|
||||||
|
self._emit_block(output, wrapped)
|
||||||
|
|
||||||
|
def _apply_pattern(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
pattern: re.Pattern[str],
|
||||||
|
handler: Callable[[re.Match[str], str], str],
|
||||||
|
) -> str:
|
||||||
|
result: List[str] = []
|
||||||
|
last = 0
|
||||||
|
for match in pattern.finditer(text):
|
||||||
|
result.append(text[last:match.start()])
|
||||||
|
replacement = handler(match, text)
|
||||||
|
result.append(replacement)
|
||||||
|
last = match.end()
|
||||||
|
result.append(text[last:])
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
def _replace_spaced_emphasis(
|
||||||
|
self,
|
||||||
|
source: str,
|
||||||
|
match: re.Match[str],
|
||||||
|
*,
|
||||||
|
transform: str,
|
||||||
|
) -> str:
|
||||||
|
stylized = self._stylize_letters(match.group(1), transform=transform)
|
||||||
|
if not stylized:
|
||||||
|
return stylized
|
||||||
|
return self._apply_emphasis_spacing(source, match.start(), match.end(), stylized)
|
||||||
|
|
||||||
|
def _apply_emphasis_spacing(self, source: str, start: int, end: int, stylized: str) -> str:
|
||||||
|
if not stylized:
|
||||||
|
return stylized
|
||||||
|
prefix = ""
|
||||||
|
suffix = ""
|
||||||
|
if start > 0:
|
||||||
|
before_char = source[start - 1]
|
||||||
|
if before_char.isalnum():
|
||||||
|
prefix = " "
|
||||||
|
if end < len(source):
|
||||||
|
after_char = source[end]
|
||||||
|
if after_char.isalnum():
|
||||||
|
suffix = " "
|
||||||
|
return f"{prefix}{stylized}{suffix}"
|
||||||
|
|
||||||
|
def _handle_list_line(self, line: str, output: List[str]) -> None:
|
||||||
|
ordered = ORDERED_LIST_PATTERN.match(line)
|
||||||
|
unordered = UNORDERED_LIST_PATTERN.match(line)
|
||||||
|
if ordered:
|
||||||
|
indent, marker, spacing, rest = ordered.groups()
|
||||||
|
elif unordered:
|
||||||
|
indent, marker, spacing, rest = unordered.groups()
|
||||||
|
else: # fall back to raw line
|
||||||
|
self._emit_block(output, [line])
|
||||||
|
return
|
||||||
|
prefix = f"{indent}{marker}{spacing}"
|
||||||
|
processed = self._process_inline(rest)
|
||||||
|
wrapped = self._wrap_text(
|
||||||
|
processed,
|
||||||
|
initial_indent=prefix,
|
||||||
|
subsequent_indent=" " * len(prefix),
|
||||||
|
style=self._current_style(),
|
||||||
|
)
|
||||||
|
self._emit_block(output, wrapped)
|
||||||
|
|
||||||
|
def _emit_block(
|
||||||
|
self,
|
||||||
|
output: List[str],
|
||||||
|
lines: List[str],
|
||||||
|
*,
|
||||||
|
stylable: bool = False,
|
||||||
|
render_fn: Optional[Callable[[BlockStyle], List[str]]] = None,
|
||||||
|
style: Optional[BlockStyle] = None,
|
||||||
|
) -> None:
|
||||||
|
if not lines:
|
||||||
|
return
|
||||||
|
start = len(output)
|
||||||
|
output.extend(lines)
|
||||||
|
if stylable and render_fn is not None and style is not None:
|
||||||
|
self._last_stylable_block = BlockRecord(start, len(lines), render_fn, style)
|
||||||
|
else:
|
||||||
|
self._last_stylable_block = None
|
||||||
|
|
||||||
|
def _apply_style_to_last_block(self, output: List[str], spec: StyleSpec) -> None:
|
||||||
|
if self._last_stylable_block is None:
|
||||||
|
return
|
||||||
|
new_style = self._combine_styles(self._last_stylable_block.style, spec)
|
||||||
|
new_lines = self._last_stylable_block.render(new_style)
|
||||||
|
start = self._last_stylable_block.start
|
||||||
|
end = start + self._last_stylable_block.length
|
||||||
|
output[start:end] = new_lines
|
||||||
|
self._last_stylable_block.length = len(new_lines)
|
||||||
|
self._last_stylable_block.style = new_style
|
||||||
|
|
||||||
|
def _current_style(self) -> BlockStyle:
|
||||||
|
return self._style_stack[-1]
|
||||||
|
|
||||||
|
def _push_style(self, spec: Optional[StyleSpec]) -> None:
|
||||||
|
base = self._current_style()
|
||||||
|
self._style_stack.append(self._combine_styles(base, spec))
|
||||||
|
|
||||||
|
def _pop_style(self) -> None:
|
||||||
|
if len(self._style_stack) > 1:
|
||||||
|
self._style_stack.pop()
|
||||||
|
|
||||||
|
def _combine_styles(self, base: BlockStyle, spec: Optional[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 _merge_specs(self, first: Optional[StyleSpec], second: Optional[StyleSpec]) -> Optional[StyleSpec]:
|
||||||
|
if first is None and second is None:
|
||||||
|
return None
|
||||||
|
if first is None:
|
||||||
|
return second
|
||||||
|
if second is None:
|
||||||
|
return first
|
||||||
|
return StyleSpec(
|
||||||
|
align=second.align or first.align,
|
||||||
|
margin_left=second.margin_left if second.margin_left is not None else first.margin_left,
|
||||||
|
margin_right=second.margin_right if second.margin_right is not None else first.margin_right,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _style_spec_from_html_attributes(self, attributes: str) -> Optional[StyleSpec]:
|
||||||
|
if not attributes:
|
||||||
|
return None
|
||||||
|
attr_pattern = re.compile(r"([\w:-]+)\s*=\s*(\".*?\"|'.*?'|\S+)")
|
||||||
|
attr_map: Dict[str, str] = {}
|
||||||
|
for name, value in attr_pattern.findall(attributes):
|
||||||
|
attr_map[name.lower()] = value.strip().strip("\"'")
|
||||||
|
|
||||||
|
spec: Optional[StyleSpec] = None
|
||||||
|
align_value = attr_map.get("align")
|
||||||
|
if align_value:
|
||||||
|
normalized = self._normalize_align(align_value)
|
||||||
|
if normalized:
|
||||||
|
spec = self._merge_specs(spec, StyleSpec(align=normalized))
|
||||||
|
|
||||||
|
style_value = attr_map.get("style")
|
||||||
|
if style_value:
|
||||||
|
css_spec = self._style_spec_from_css(style_value)
|
||||||
|
spec = self._merge_specs(spec, css_spec)
|
||||||
|
|
||||||
|
return spec
|
||||||
|
|
||||||
|
def _style_spec_from_css(self, css: str) -> Optional[StyleSpec]:
|
||||||
|
spec = StyleSpec()
|
||||||
|
changed = False
|
||||||
|
for declaration in css.split(";"):
|
||||||
|
if ":" not in declaration:
|
||||||
|
continue
|
||||||
|
name, value = declaration.split(":", 1)
|
||||||
|
name = name.strip().lower()
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
if name == "text-align":
|
||||||
|
normalized = self._normalize_align(value)
|
||||||
|
if normalized:
|
||||||
|
spec.align = normalized
|
||||||
|
changed = True
|
||||||
|
elif name == "margin":
|
||||||
|
left, right, auto_center = self._parse_css_margin_shorthand(value)
|
||||||
|
if left is not None:
|
||||||
|
spec.margin_left = left
|
||||||
|
changed = True
|
||||||
|
if right is not None:
|
||||||
|
spec.margin_right = right
|
||||||
|
changed = True
|
||||||
|
if auto_center:
|
||||||
|
spec.align = "center"
|
||||||
|
changed = True
|
||||||
|
elif name == "margin-left":
|
||||||
|
parsed = self._parse_space_value(value)
|
||||||
|
if parsed is not None:
|
||||||
|
spec.margin_left = parsed
|
||||||
|
changed = True
|
||||||
|
elif value.lower() == "auto":
|
||||||
|
spec.align = spec.align or "center"
|
||||||
|
changed = True
|
||||||
|
elif name == "margin-right":
|
||||||
|
parsed = self._parse_space_value(value)
|
||||||
|
if parsed is not None:
|
||||||
|
spec.margin_right = parsed
|
||||||
|
changed = True
|
||||||
|
elif value.lower() == "auto":
|
||||||
|
spec.align = spec.align or "center"
|
||||||
|
changed = True
|
||||||
|
return spec if changed else None
|
||||||
|
|
||||||
|
def _parse_style_spec_from_tokens(self, token_str: str) -> Optional[StyleSpec]:
|
||||||
|
tokens = re.split(r"\s+", token_str.strip())
|
||||||
|
spec = StyleSpec()
|
||||||
|
changed = False
|
||||||
|
for token in tokens:
|
||||||
|
token = token.strip()
|
||||||
|
if not token:
|
||||||
|
continue
|
||||||
|
if token.startswith("."):
|
||||||
|
align = self._class_to_align(token[1:])
|
||||||
|
if align:
|
||||||
|
spec.align = align
|
||||||
|
changed = True
|
||||||
|
continue
|
||||||
|
if "=" in token:
|
||||||
|
key, value = token.split("=", 1)
|
||||||
|
key = key.strip().lower().lstrip(".")
|
||||||
|
value = value.strip().strip("\"'")
|
||||||
|
if key in {"align", "text-align"}:
|
||||||
|
normalized = self._normalize_align(value)
|
||||||
|
if normalized:
|
||||||
|
spec.align = normalized
|
||||||
|
changed = True
|
||||||
|
elif key in {"margin", "margin-left", "margin-right"}:
|
||||||
|
if key == "margin":
|
||||||
|
left, right, auto_center = self._parse_css_margin_shorthand(value)
|
||||||
|
if left is not None:
|
||||||
|
spec.margin_left = left
|
||||||
|
changed = True
|
||||||
|
if right is not None:
|
||||||
|
spec.margin_right = right
|
||||||
|
changed = True
|
||||||
|
if auto_center:
|
||||||
|
spec.align = "center"
|
||||||
|
changed = True
|
||||||
|
elif key == "margin-left":
|
||||||
|
parsed = self._parse_space_value(value)
|
||||||
|
if parsed is not None:
|
||||||
|
spec.margin_left = parsed
|
||||||
|
changed = True
|
||||||
|
elif value.lower() == "auto":
|
||||||
|
spec.align = spec.align or "center"
|
||||||
|
changed = True
|
||||||
|
elif key == "margin-right":
|
||||||
|
parsed = self._parse_space_value(value)
|
||||||
|
if parsed is not None:
|
||||||
|
spec.margin_right = parsed
|
||||||
|
changed = True
|
||||||
|
elif value.lower() == "auto":
|
||||||
|
spec.align = spec.align or "center"
|
||||||
|
changed = True
|
||||||
|
continue
|
||||||
|
align = self._normalize_align(token)
|
||||||
|
if align:
|
||||||
|
spec.align = align
|
||||||
|
changed = True
|
||||||
|
return spec if changed else None
|
||||||
|
|
||||||
|
def _parse_css_margin_shorthand(self, value: str) -> Tuple[Optional[int], Optional[int], bool]:
|
||||||
|
parts = [part for part in re.split(r"\s+", value.strip()) if part]
|
||||||
|
if not parts:
|
||||||
|
return None, None, False
|
||||||
|
|
||||||
|
values: List[Optional[int]] = []
|
||||||
|
autos: List[bool] = []
|
||||||
|
for part in parts:
|
||||||
|
if part.lower() == "auto":
|
||||||
|
values.append(None)
|
||||||
|
autos.append(True)
|
||||||
|
else:
|
||||||
|
parsed = self._parse_space_value(part)
|
||||||
|
values.append(parsed)
|
||||||
|
autos.append(False)
|
||||||
|
|
||||||
|
left_auto = False
|
||||||
|
right_auto = False
|
||||||
|
if len(values) == 1:
|
||||||
|
left = right = values[0]
|
||||||
|
left_auto = right_auto = autos[0]
|
||||||
|
elif len(values) == 2:
|
||||||
|
left = right = values[1]
|
||||||
|
left_auto = right_auto = autos[1]
|
||||||
|
elif len(values) == 3:
|
||||||
|
left = right = values[1]
|
||||||
|
left_auto = right_auto = autos[1]
|
||||||
|
else:
|
||||||
|
right = values[1]
|
||||||
|
left = values[3]
|
||||||
|
right_auto = autos[1]
|
||||||
|
left_auto = autos[3]
|
||||||
|
auto_center = left_auto and right_auto
|
||||||
|
return left, right, auto_center
|
||||||
|
|
||||||
|
def _parse_space_value(self, value: str) -> Optional[int]:
|
||||||
|
match = re.match(r"(-?\d+(?:\.\d+)?)", value.strip())
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
number = float(match.group(1))
|
||||||
|
return max(0, int(round(number)))
|
||||||
|
|
||||||
|
def _normalize_align(self, value: str) -> Optional[str]:
|
||||||
|
normalized = value.strip().lower()
|
||||||
|
mapping = {
|
||||||
|
"centre": "center",
|
||||||
|
"center": "center",
|
||||||
|
"left": "left",
|
||||||
|
"right": "right",
|
||||||
|
}
|
||||||
|
return mapping.get(normalized)
|
||||||
|
|
||||||
|
def _class_to_align(self, class_name: str) -> Optional[str]:
|
||||||
|
name = class_name.strip().lower().lstrip(".")
|
||||||
|
if name in {"center", "text-center", "align-center"}:
|
||||||
|
return "center"
|
||||||
|
if name in {"left", "text-left", "align-left"}:
|
||||||
|
return "left"
|
||||||
|
if name in {"right", "text-right", "align-right"}:
|
||||||
|
return "right"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_trailing_attr(self, text: str) -> Tuple[str, Optional[StyleSpec]]:
|
||||||
|
match = MMD_ATTR_TAIL_RE.match(text)
|
||||||
|
if not match:
|
||||||
|
return text, None
|
||||||
|
clean_text = match.group(1).rstrip()
|
||||||
|
spec = self._parse_style_spec_from_tokens(match.group(2))
|
||||||
|
return clean_text, spec
|
||||||
|
|
||||||
|
def _render_heading_lines(self, level: int, text: str, style: BlockStyle) -> List[str]:
|
||||||
|
if level <= 3:
|
||||||
|
figlet_lines = self._figlet_render(level, text)
|
||||||
|
if figlet_lines:
|
||||||
|
max_figlet_width = max((len(line.rstrip()) for line in figlet_lines), default=0)
|
||||||
|
if max_figlet_width <= self._effective_width(style):
|
||||||
|
styled = self._apply_style_to_lines(figlet_lines, style)
|
||||||
|
return styled + [""]
|
||||||
|
return self._render_h4_plus(text, style)
|
||||||
|
|
||||||
|
def _render_horizontal_rule(self, style: BlockStyle) -> str:
|
||||||
|
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
||||||
|
available_width = max(1, self._effective_width(style))
|
||||||
|
return " " * margin_left + "-" * available_width
|
||||||
|
|
||||||
|
def _apply_style_to_lines(self, lines: List[str], style: BlockStyle) -> 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)
|
||||||
|
result: List[str] = []
|
||||||
|
for line in lines:
|
||||||
|
if not line:
|
||||||
|
result.append("")
|
||||||
|
continue
|
||||||
|
trimmed = line.rstrip()
|
||||||
|
line_len = len(trimmed)
|
||||||
|
extra_space = max(0, available_width - line_len)
|
||||||
|
if style.align == "center":
|
||||||
|
extra_left = extra_space // 2
|
||||||
|
elif style.align == "right":
|
||||||
|
extra_left = extra_space
|
||||||
|
else:
|
||||||
|
extra_left = 0
|
||||||
|
max_indent = max(0, self.width - line_len)
|
||||||
|
indent = min(margin_left + extra_left, max_indent)
|
||||||
|
result.append(" " * indent + trimmed)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _process_inline(self, text: str) -> str:
|
||||||
|
code_segments: List[str] = []
|
||||||
|
|
||||||
|
def stash_code(match: re.Match[str]) -> str:
|
||||||
|
code_segments.append(match.group(0))
|
||||||
|
return f"\u0000CODE{len(code_segments) - 1}\u0000"
|
||||||
|
|
||||||
|
text = re.sub(r"`[^`]*`", stash_code, text)
|
||||||
|
|
||||||
|
text = self._apply_pattern(
|
||||||
|
text,
|
||||||
|
re.compile(r"~~(.*?)~~"),
|
||||||
|
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] = []
|
||||||
|
|
||||||
|
def apply_with_placeholder(
|
||||||
|
current: str,
|
||||||
|
pattern: re.Pattern[str],
|
||||||
|
handler: Callable[[re.Match[str]], str],
|
||||||
|
) -> str:
|
||||||
|
def replacer(match: re.Match[str]) -> str:
|
||||||
|
replacement = handler(match)
|
||||||
|
placeholder = f"\u0000EMP{len(emphasis_segments)}\u0000"
|
||||||
|
emphasis_segments.append(replacement)
|
||||||
|
return placeholder
|
||||||
|
|
||||||
|
return pattern.sub(replacer, current)
|
||||||
|
|
||||||
|
text = apply_with_placeholder(
|
||||||
|
text,
|
||||||
|
re.compile(r"__(.*?)__"),
|
||||||
|
lambda m: self._apply_emphasis_spacing(
|
||||||
|
m.string,
|
||||||
|
m.start(),
|
||||||
|
m.end(),
|
||||||
|
self._stylize_delimited(m.group(1), "_", transform="upper", word_repeat=3),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
text = apply_with_placeholder(
|
||||||
|
text,
|
||||||
|
re.compile(r"(?<!_)_(?!_)(.*?)(?<!_)_(?!_)"),
|
||||||
|
lambda m: self._apply_emphasis_spacing(
|
||||||
|
m.string,
|
||||||
|
m.start(),
|
||||||
|
m.end(),
|
||||||
|
self._stylize_delimited(m.group(1), "_", transform="preserve", word_repeat=3),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
text = re.sub(r"(?<!\!)\[([^\]]+)\]\(([^)]+)\)", self._handle_link, text)
|
||||||
|
text = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", self._handle_image, text)
|
||||||
|
|
||||||
|
for index, replacement in enumerate(emphasis_segments):
|
||||||
|
placeholder = f"\u0000EMP{index}\u0000"
|
||||||
|
text = text.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
for index, code in enumerate(code_segments):
|
||||||
|
placeholder = f"\u0000CODE{index}\u0000"
|
||||||
|
text = text.replace(placeholder, code)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _stylize_letters(self, content: str, transform: str = "preserve") -> str:
|
||||||
|
if not content:
|
||||||
|
return ""
|
||||||
|
result: List[str] = []
|
||||||
|
previous_alnum = False
|
||||||
|
pending_delimiter: str = ""
|
||||||
|
|
||||||
|
def apply_transform(char: str) -> str:
|
||||||
|
if transform == "upper":
|
||||||
|
return char.upper()
|
||||||
|
if transform == "lower":
|
||||||
|
return char.lower()
|
||||||
|
return char
|
||||||
|
|
||||||
|
for char in content:
|
||||||
|
processed = apply_transform(char)
|
||||||
|
if processed.isspace():
|
||||||
|
if result and result[-1].isalnum():
|
||||||
|
pending_delimiter = " "
|
||||||
|
previous_alnum = False
|
||||||
|
continue
|
||||||
|
if processed.isalnum():
|
||||||
|
if previous_alnum:
|
||||||
|
result.append(" ")
|
||||||
|
elif pending_delimiter:
|
||||||
|
result.append(pending_delimiter)
|
||||||
|
pending_delimiter = ""
|
||||||
|
elif result:
|
||||||
|
result.append(" ")
|
||||||
|
result.append(processed)
|
||||||
|
previous_alnum = True
|
||||||
|
else:
|
||||||
|
if result and result[-1] == " ":
|
||||||
|
result.pop()
|
||||||
|
if pending_delimiter:
|
||||||
|
result.append(pending_delimiter.strip())
|
||||||
|
pending_delimiter = ""
|
||||||
|
result.append(processed)
|
||||||
|
previous_alnum = False
|
||||||
|
stylized = "".join(result)
|
||||||
|
return stylized.strip()
|
||||||
|
|
||||||
|
def _stylize_delimited(
|
||||||
|
self,
|
||||||
|
content: str,
|
||||||
|
delimiter: str,
|
||||||
|
transform: str = "preserve",
|
||||||
|
word_repeat: int = 2,
|
||||||
|
) -> str:
|
||||||
|
def apply_transform(char: str) -> str:
|
||||||
|
if transform == "upper":
|
||||||
|
return char.upper()
|
||||||
|
if transform == "lower":
|
||||||
|
return char.lower()
|
||||||
|
return char
|
||||||
|
|
||||||
|
output: List[str] = []
|
||||||
|
open_sequence = False
|
||||||
|
pending_gap = False
|
||||||
|
|
||||||
|
for char in content:
|
||||||
|
if char.isspace():
|
||||||
|
if open_sequence:
|
||||||
|
pending_gap = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
processed = apply_transform(char)
|
||||||
|
if not open_sequence:
|
||||||
|
output.append(delimiter)
|
||||||
|
open_sequence = True
|
||||||
|
else:
|
||||||
|
repeat = word_repeat if pending_gap else 1
|
||||||
|
output.append(delimiter * repeat)
|
||||||
|
output.append(processed)
|
||||||
|
pending_gap = False
|
||||||
|
|
||||||
|
if not open_sequence:
|
||||||
|
return delimiter * 2
|
||||||
|
|
||||||
|
output.append(delimiter)
|
||||||
|
return "".join(output)
|
||||||
|
|
||||||
|
def _handle_link(self, match: re.Match[str]) -> str:
|
||||||
|
text, target = match.groups()
|
||||||
|
url, _title = self._split_link_target(target)
|
||||||
|
index = self._register_link(url)
|
||||||
|
return f"[{text}]({index})"
|
||||||
|
|
||||||
|
def _handle_image(self, match: re.Match[str]) -> str:
|
||||||
|
alt_text, target = match.groups()
|
||||||
|
url, _title = self._split_link_target(target)
|
||||||
|
display_text = alt_text or "Image"
|
||||||
|
index = self._register_link(url)
|
||||||
|
return f"[Image: {display_text}]({index})"
|
||||||
|
|
||||||
|
def _split_link_target(self, value: str) -> Tuple[str, Optional[str]]:
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return "", None
|
||||||
|
if " " not in value:
|
||||||
|
return value.strip(), None
|
||||||
|
url, remainder = value.split(" ", 1)
|
||||||
|
remainder = remainder.strip()
|
||||||
|
if remainder.startswith('"') and remainder.endswith('"'):
|
||||||
|
return url.strip(), remainder.strip('"')
|
||||||
|
return url.strip(), remainder
|
||||||
|
|
||||||
|
def _register_link(self, url: str) -> int:
|
||||||
|
if url in self.link_indices:
|
||||||
|
return self.link_indices[url]
|
||||||
|
index = len(self.links) + 1
|
||||||
|
self.links.append((index, url))
|
||||||
|
self.link_indices[url] = index
|
||||||
|
return index
|
||||||
|
|
||||||
|
def _figlet_render(self, level: int, text: str) -> Optional[List[str]]:
|
||||||
|
if Figlet is None:
|
||||||
|
return None
|
||||||
|
font_name = getattr(self.frontmatter, f"h{level}_font", "standard")
|
||||||
|
figlet = self.figlets.get(font_name)
|
||||||
|
if figlet is None:
|
||||||
|
try:
|
||||||
|
figlet = Figlet(font=font_name)
|
||||||
|
except (FontNotFound, TypeError):
|
||||||
|
return None
|
||||||
|
self.figlets[font_name] = figlet
|
||||||
|
rendered = figlet.renderText(text).rstrip("\n")
|
||||||
|
lines = rendered.splitlines()
|
||||||
|
trimmed_lines = [line.rstrip("\n") for line in lines]
|
||||||
|
if not trimmed_lines or max(len(line.rstrip()) for line in trimmed_lines) > self.width:
|
||||||
|
return None
|
||||||
|
return [line.rstrip() for line in trimmed_lines]
|
||||||
|
|
||||||
|
def _render_h4_plus(self, text: str, style: BlockStyle) -> List[str]:
|
||||||
|
uppercase = text.upper()
|
||||||
|
wrapped = self._wrap_text(uppercase, style=style)
|
||||||
|
output: List[str] = []
|
||||||
|
for line in wrapped:
|
||||||
|
line = line.rstrip()
|
||||||
|
if not line.strip():
|
||||||
|
output.append("")
|
||||||
|
continue
|
||||||
|
leading = len(line) - len(line.lstrip(" "))
|
||||||
|
underline = " " * leading + "-" * len(line.lstrip(" "))
|
||||||
|
output.append(line)
|
||||||
|
output.append(underline)
|
||||||
|
output.append("")
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _wrap_text(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
initial_indent: str = "",
|
||||||
|
subsequent_indent: Optional[str] = None,
|
||||||
|
style: Optional[BlockStyle] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
style = style or BlockStyle()
|
||||||
|
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
||||||
|
available_width = max(1, self._effective_width(style))
|
||||||
|
|
||||||
|
subsequent = initial_indent if subsequent_indent is None else subsequent_indent
|
||||||
|
wrapper = textwrap.TextWrapper(
|
||||||
|
width=available_width,
|
||||||
|
expand_tabs=False,
|
||||||
|
replace_whitespace=False,
|
||||||
|
drop_whitespace=False,
|
||||||
|
break_on_hyphens=False,
|
||||||
|
break_long_words=True,
|
||||||
|
initial_indent=initial_indent,
|
||||||
|
subsequent_indent=subsequent,
|
||||||
|
)
|
||||||
|
wrapped = wrapper.wrap(text)
|
||||||
|
if not wrapped:
|
||||||
|
wrapped = [initial_indent.rstrip()]
|
||||||
|
result: List[str] = []
|
||||||
|
for line in wrapped:
|
||||||
|
line = line.rstrip()
|
||||||
|
line_len = len(line)
|
||||||
|
extra_space = max(0, available_width - line_len)
|
||||||
|
if style.align == "center":
|
||||||
|
extra_left = extra_space // 2
|
||||||
|
elif style.align == "right":
|
||||||
|
extra_left = extra_space
|
||||||
|
else:
|
||||||
|
extra_left = 0
|
||||||
|
max_indent = max(0, self.width - line_len)
|
||||||
|
indent = min(margin_left + extra_left, max_indent)
|
||||||
|
result.append(" " * indent + line)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _effective_width(self, style: BlockStyle) -> int:
|
||||||
|
margin_left = min(max(style.margin_left, 0), self.width - 1)
|
||||||
|
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 parse_frontmatter(lines: List[str]) -> Tuple[FrontMatter, List[str]]:
|
||||||
|
if not lines or not FRONTMATTER_PATTERN.match(lines[0]):
|
||||||
|
return FrontMatter(), lines
|
||||||
|
frontmatter: Dict[str, str] = {}
|
||||||
|
idx = 1
|
||||||
|
while idx < len(lines):
|
||||||
|
if FRONTMATTER_PATTERN.match(lines[idx]):
|
||||||
|
break
|
||||||
|
if ":" in lines[idx]:
|
||||||
|
key, value = lines[idx].split(":", 1)
|
||||||
|
frontmatter[key.strip()] = value.strip()
|
||||||
|
idx += 1
|
||||||
|
if idx >= len(lines):
|
||||||
|
return FrontMatter(), lines
|
||||||
|
remaining = lines[idx + 1 :] if idx + 1 < len(lines) else []
|
||||||
|
fm = FrontMatter(
|
||||||
|
h1_font=frontmatter.get("h1_font", "standard"),
|
||||||
|
h2_font=frontmatter.get("h2_font", "standard"),
|
||||||
|
h3_font=frontmatter.get("h3_font", "standard"),
|
||||||
|
)
|
||||||
|
return fm, remaining
|
||||||
|
|
||||||
|
|
||||||
|
def read_lines(path: Path) -> List[str]:
|
||||||
|
with path.open("r", encoding="utf-8") as handle:
|
||||||
|
return handle.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
def write_output(path: Optional[Path], lines: List[str]) -> None:
|
||||||
|
content = "\r\n".join(lines)
|
||||||
|
if path is None:
|
||||||
|
sys.stdout.write(content + "\r\n")
|
||||||
|
return
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content + "\r\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="Convert Markdown files to 80-column DOS-compatible text.")
|
||||||
|
parser.add_argument("input_path", type=Path, help="Path to the Markdown input file.")
|
||||||
|
parser.add_argument("-o", "--output", type=Path, help="Optional path to write the resulting text file.")
|
||||||
|
parser.add_argument("--width", type=int, default=80, help="Maximum column width (default: 80).")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[List[str]] = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
lines = read_lines(args.input_path)
|
||||||
|
frontmatter, content = parse_frontmatter(lines)
|
||||||
|
converter = MarkdownToTxtConverter(width=args.width, frontmatter=frontmatter)
|
||||||
|
converted_lines = converter.convert(content)
|
||||||
|
write_output(args.output, converted_lines)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
163
output.txt
Normal file
163
output.txt
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
_ _ _ _ _ _ ___ __
|
||||||
|
| |__ / | | | | | ___ __ _ __| (_)_ __ __ _ ( _ ) \ \
|
||||||
|
| '_ \| | | |_| |/ _ \/ _` |/ _` | | '_ \ / _` | / _ \ _____| |
|
||||||
|
| | | | | | _ | __/ (_| | (_| | | | | | (_| | | (_) |_____| |
|
||||||
|
|_| |_|_| |_| |_|\___|\__,_|\__,_|_|_| |_|\__, | \___/ | |
|
||||||
|
|___/ /_/
|
||||||
|
|
||||||
|
_ ____ _ _ _ _
|
||||||
|
| |__ |___ \ | | | | ___ __ _ __| (_)_ __ __ _
|
||||||
|
| '_ \ __) | | |_| |/ _ \/ _` |/ _` | | '_ \ / _` |
|
||||||
|
| | | |/ __/ | _ | __/ (_| | (_| | | | | | (_| |
|
||||||
|
|_| |_|_____| |_| |_|\___|\__,_|\__,_|_|_| |_|\__, |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
_ _____ _ _ _ _
|
||||||
|
| |__ |___ / | | | | ___ __ _ __| (_)_ __ __ _
|
||||||
|
| '_ \ |_ \ | |_| |/ _ \/ _` |/ _` | | '_ \ / _` |
|
||||||
|
| | | |___) | | _ | __/ (_| | (_| | | | | | (_| |
|
||||||
|
|_| |_|____/ |_| |_|\___|\__,_|\__,_|_|_| |_|\__, |
|
||||||
|
|___/
|
||||||
|
|
||||||
|
H4 HEADING
|
||||||
|
----------
|
||||||
|
|
||||||
|
H5 HEADING
|
||||||
|
----------
|
||||||
|
|
||||||
|
H6 HEADING
|
||||||
|
----------
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ _ _ ____ _
|
||||||
|
| | | | ___ _ __(_)_______ _ __ | |_ __ _| | | _ \ _ _| | ___ ___
|
||||||
|
| |_| |/ _ \| '__| |_ / _ \| '_ \| __/ _` | | | |_) | | | | |/ _ \/ __|
|
||||||
|
| _ | (_) | | | |/ / (_) | | | | || (_| | | | _ <| |_| | | __/\__ \
|
||||||
|
|_| |_|\___/|_| |_/___\___/|_| |_|\__\__,_|_| |_| \_\\__,_|_|\___||___/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_____ _ _
|
||||||
|
| ____|_ __ ___ _ __ | |__ __ _ ___(_)___
|
||||||
|
| _| | '_ ` _ \| '_ \| '_ \ / _` / __| / __|
|
||||||
|
| |___| | | | | | |_) | | | | (_| \__ \ \__ \
|
||||||
|
|_____|_| |_| |_| .__/|_| |_|\__,_|___/_|___/
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
T H I S I S B O L D T E X T _T_H_I_S___I_S___B_O_L_D___T_E_X_T_ T h i s
|
||||||
|
i s i t a l i c t e x t _T_h_i_s___i_s___i_t_a_l_i_c___t_e_x_t_
|
||||||
|
-S-t-r-i-k-e-t-h-r-o-u-g-h-
|
||||||
|
|
||||||
|
____ _ _ _
|
||||||
|
| __ )| | ___ ___| | ____ _ _ _ ___ | |_ ___ ___
|
||||||
|
| _ \| |/ _ \ / __| |/ / _` | | | |/ _ \| __/ _ \/ __|
|
||||||
|
| |_) | | (_) | (__| < (_| | |_| | (_) | || __/\__ \
|
||||||
|
|____/|_|\___/ \___|_|\_\__, |\__,_|\___/ \__\___||___/
|
||||||
|
|_|
|
||||||
|
|
||||||
|
|
||||||
|
| Blockquotes can also be nested...
|
||||||
|
| | by using additional greater-than signs right next to each other...
|
||||||
|
| | | or with spaces between arrows.
|
||||||
|
|
||||||
|
_ _ _
|
||||||
|
| | (_)___| |_ ___
|
||||||
|
| | | / __| __/ __|
|
||||||
|
| |___| \__ \ |_\__ \
|
||||||
|
|_____|_|___/\__|___/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _ _
|
||||||
|
| | | |_ __ ___ _ __ __| | ___ _ __ ___ __| |
|
||||||
|
| | | | '_ \ / _ \| '__/ _` |/ _ \ '__/ _ \/ _` |
|
||||||
|
| |_| | | | | (_) | | | (_| | __/ | | __/ (_| |
|
||||||
|
\___/|_| |_|\___/|_| \__,_|\___|_| \___|\__,_|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
+ 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
|
||||||
|
|
||||||
|
___ _ _
|
||||||
|
/ _ \ _ __ __| | ___ _ __ ___ __| |
|
||||||
|
| | | | '__/ _` |/ _ \ '__/ _ \/ _` |
|
||||||
|
| |_| | | | (_| | __/ | | __/ (_| |
|
||||||
|
\___/|_| \__,_|\___|_| \___|\__,_|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Consectetur adipiscing elit
|
||||||
|
3. Integer molestie lorem at massa
|
||||||
|
|
||||||
|
____ _
|
||||||
|
/ ___|___ __| | ___
|
||||||
|
| | / _ \ / _` |/ _ \
|
||||||
|
| |__| (_) | (_| | __/
|
||||||
|
\____\___/ \__,_|\___|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Inline `code`
|
||||||
|
|
||||||
|
Indented code
|
||||||
|
|
||||||
|
01 | // Some comments
|
||||||
|
02 | line 1 of code
|
||||||
|
03 | line 2 of code
|
||||||
|
04 | line 3 of code
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Block code "fences"
|
||||||
|
|
||||||
|
01 | Sample text here...
|
||||||
|
|
||||||
|
|
||||||
|
Syntax highlighting
|
||||||
|
|
||||||
|
01 | var foo = function (bar) {
|
||||||
|
02 | return bar++;
|
||||||
|
03 | };
|
||||||
|
04 |
|
||||||
|
05 | console.log(foo(5));
|
||||||
|
|
||||||
|
|
||||||
|
_ _ _
|
||||||
|
| | (_)_ __ | | _____
|
||||||
|
| | | | '_ \| |/ / __|
|
||||||
|
| |___| | | | | <\__ \
|
||||||
|
|_____|_|_| |_|_|\_\___/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[link text](1)
|
||||||
|
|
||||||
|
[link with title](2)
|
||||||
|
|
||||||
|
___
|
||||||
|
|_ _|_ __ ___ __ _ __ _ ___ ___
|
||||||
|
| || '_ ` _ \ / _` |/ _` |/ _ \/ __|
|
||||||
|
| || | | | | | (_| | (_| | __/\__ \
|
||||||
|
|___|_| |_| |_|\__,_|\__, |\___||___/
|
||||||
|
|___/
|
||||||
|
|
||||||
|
|
||||||
|
[Image: Minion](3) [Image: Stormtroopocat](4)
|
||||||
|
|
||||||
|
[1] http://dev.nodeca.com
|
||||||
|
[2] http://nodeca.github.io/pica/demo/
|
||||||
|
[3] https://octodex.github.com/images/minion.png
|
||||||
|
[4] https://octodex.github.com/images/stormtroopocat.jpg
|
||||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[project]
|
||||||
|
name = "md2amb"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"pyfiglet>=0.8.0",
|
||||||
|
]
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pyfiglet>=0.8.0
|
||||||
23
uv.lock
generated
Normal file
23
uv.lock
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md2amb"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyfiglet" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "pyfiglet", specifier = ">=0.8.0" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyfiglet"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c8/e3/0a86276ad2c383ce08d76110a8eec2fe22e7051c4b8ba3fa163a0b08c428/pyfiglet-1.0.4.tar.gz", hash = "sha256:db9c9940ed1bf3048deff534ed52ff2dafbbc2cd7610b17bb5eca1df6d4278ef", size = 1560615, upload-time = "2025-08-15T18:32:47.302Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/5c/fe9f95abd5eaedfa69f31e450f7e2768bef121dbdf25bcddee2cd3087a16/pyfiglet-1.0.4-py3-none-any.whl", hash = "sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f", size = 1806118, upload-time = "2025-08-15T18:32:45.556Z" },
|
||||||
|
]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue