mdhtml2term 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/CHANGELOG.md +5 -0
- {mdhtml2term-0.1.0/mdhtml2term.egg-info → mdhtml2term-0.1.1}/PKG-INFO +4 -4
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/README.md +2 -2
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/mdhtml2term/__init__.py +2 -1
- mdhtml2term-0.1.1/mdhtml2term/core.py +111 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1/mdhtml2term.egg-info}/PKG-INFO +4 -4
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/mdhtml2term.egg-info/requires.txt +1 -1
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/pyproject.toml +1 -1
- mdhtml2term-0.1.0/mdhtml2term/core.py +0 -127
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/LICENSE +0 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/MANIFEST.in +0 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/mdhtml2term.egg-info/SOURCES.txt +0 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/mdhtml2term.egg-info/dependency_links.txt +0 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/mdhtml2term.egg-info/top_level.txt +0 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/setup.cfg +0 -0
- {mdhtml2term-0.1.0 → mdhtml2term-0.1.1}/tests/test_core.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdhtml2term
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Render MDHTML in the terminal with Rich
|
|
5
5
|
Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: mdhtml>=0.1.
|
|
13
|
+
Requires-Dist: mdhtml>=0.1.43
|
|
14
14
|
Requires-Dist: fastcore
|
|
15
15
|
Requires-Dist: rich
|
|
16
16
|
Provides-Extra: dev
|
|
@@ -42,7 +42,7 @@ console = Console()
|
|
|
42
42
|
for block in md_blocks(markdown_text): console.print(block)
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
`md_blocks` takes a Markdown string and returns a list of Rich renderables. `theme` names the Pygments theme for code blocks
|
|
45
|
+
`md_blocks` takes a Markdown string and returns a list of Rich renderables. `theme` names the Pygments theme for code blocks and defaults to `ansi_dark`. Other keyword arguments go to `mdhtml.parse_md`. The shared Rust document engine resolves references and assigns numbers. Unresolved references show their target ids.
|
|
46
46
|
|
|
47
47
|
## Supported content
|
|
48
48
|
|
|
@@ -53,4 +53,4 @@ for block in md_blocks(markdown_text): console.print(block)
|
|
|
53
53
|
- Task list checkboxes, as `[ ]` and `[x]`.
|
|
54
54
|
- A numbered list that resumes after other content keeps its numbering.
|
|
55
55
|
|
|
56
|
-
Other inline elements show their text. Other block containers render their children.
|
|
56
|
+
Other inline elements show their text. Other block containers render their children. Raw payloads and unresolved template instructions are omitted.
|
|
@@ -20,7 +20,7 @@ console = Console()
|
|
|
20
20
|
for block in md_blocks(markdown_text): console.print(block)
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`md_blocks` takes a Markdown string and returns a list of Rich renderables. `theme` names the Pygments theme for code blocks
|
|
23
|
+
`md_blocks` takes a Markdown string and returns a list of Rich renderables. `theme` names the Pygments theme for code blocks and defaults to `ansi_dark`. Other keyword arguments go to `mdhtml.parse_md`. The shared Rust document engine resolves references and assigns numbers. Unresolved references show their target ids.
|
|
24
24
|
|
|
25
25
|
## Supported content
|
|
26
26
|
|
|
@@ -31,4 +31,4 @@ for block in md_blocks(markdown_text): console.print(block)
|
|
|
31
31
|
- Task list checkboxes, as `[ ]` and `[x]`.
|
|
32
32
|
- A numbered list that resumes after other content keeps its numbering.
|
|
33
33
|
|
|
34
|
-
Other inline elements show their text. Other block containers render their children.
|
|
34
|
+
Other inline elements show their text. Other block containers render their children. Raw payloads and unresolved template instructions are omitted.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"Render MDHTML Markdown as Rich renderables for the terminal, one per top-level block."
|
|
2
|
+
import mdhtml
|
|
3
|
+
from fastcore.meta import delegates
|
|
4
|
+
from rich import box
|
|
5
|
+
from rich.cells import cell_len
|
|
6
|
+
from rich.console import Group
|
|
7
|
+
from rich.segment import Segment
|
|
8
|
+
from rich.style import Style
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
__all__ = ['md_blocks']
|
|
14
|
+
|
|
15
|
+
INLINE = dict(strong='bold', emph='italic', code='cyan', strike='strike', link='underline bright_blue', highlight='reverse')
|
|
16
|
+
|
|
17
|
+
class Prefixed:
|
|
18
|
+
"Renders `rends` one after another, with `first` before the first line and `rest` before each later line"
|
|
19
|
+
def __init__(self, rends, first, rest=None, style=''):
|
|
20
|
+
self.rends,self.first,self.rest = rends,first,first if rest is None else rest
|
|
21
|
+
self.style = Style.parse(style) if style else None
|
|
22
|
+
|
|
23
|
+
def __rich_console__(self, console, options):
|
|
24
|
+
opts = options.update_width(max(1, options.max_width - cell_len(self.first)))
|
|
25
|
+
lines = [l for r in self.rends for l in console.render_lines(r, opts, pad=False)] or [[]]
|
|
26
|
+
for i,line in enumerate(lines):
|
|
27
|
+
yield Segment(self.first if i == 0 else self.rest, self.style)
|
|
28
|
+
yield from line
|
|
29
|
+
yield Segment.line()
|
|
30
|
+
|
|
31
|
+
def _inline(nodes, text=None, style=''):
|
|
32
|
+
if text is None: text = Text()
|
|
33
|
+
for node in nodes:
|
|
34
|
+
kind = node['type']
|
|
35
|
+
sty = f"{style} {INLINE.get(kind, '')}".strip()
|
|
36
|
+
if kind in ('text', 'code'): text.append(node['text'], style=sty or None)
|
|
37
|
+
elif kind in ('soft_break', 'hard_break'): text.append('\n')
|
|
38
|
+
elif kind == 'image': text.append(f"[{node['alt'] or 'image'}]", style=f'{style} dim'.strip())
|
|
39
|
+
elif kind == 'math_inline': text.append(node['tex'], style=sty or None)
|
|
40
|
+
elif kind == 'footnote_ref': text.append(f"[{node['label']}]", style=sty or None)
|
|
41
|
+
else:
|
|
42
|
+
href = node['url'] if kind == 'link' else None
|
|
43
|
+
if href: sty = f'{sty} link {href}'
|
|
44
|
+
start = len(text)
|
|
45
|
+
_inline(node.get('children', []), text, sty)
|
|
46
|
+
if href and text.plain[start:] != href: text.append(f' ({href})', style='dim')
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
def _list(node, theme):
|
|
50
|
+
items = []
|
|
51
|
+
for i,item in enumerate(node['items'], node['start']):
|
|
52
|
+
bullet = f'{i}. ' if node['ordered'] else '• '
|
|
53
|
+
check = '' if item['checked'] is None else '[x] ' if item['checked'] else '[ ] '
|
|
54
|
+
items.append(Prefixed(_blocks(item['blocks'], theme), bullet + check, ' ' * cell_len(bullet), style='bold'))
|
|
55
|
+
return Group(*items)
|
|
56
|
+
|
|
57
|
+
def _caption(node):
|
|
58
|
+
text = _inline(node['caption'])
|
|
59
|
+
if number := node['number']: text = Text(number['display'] + (': ' if text.plain else '')) + text
|
|
60
|
+
return text
|
|
61
|
+
|
|
62
|
+
def _table(node, theme):
|
|
63
|
+
head = next((r for r in node['head'] if r['type'] == 'row'), None)
|
|
64
|
+
rows = [r for r in [*node['rows'], *node['foot']] if r['type'] == 'row']
|
|
65
|
+
first = head if head is not None else rows[0] if rows else None
|
|
66
|
+
table = Table(box=box.SIMPLE_HEAD, show_header=head is not None, show_edge=False, pad_edge=False)
|
|
67
|
+
if first is not None:
|
|
68
|
+
for cell in first['cells']:
|
|
69
|
+
table.add_column(Group(*_blocks(cell['content'], theme)) if head is not None else '', justify=cell['align'] or 'left')
|
|
70
|
+
for row in rows: table.add_row(*(Group(*_blocks(c['content'], theme)) for c in row['cells']))
|
|
71
|
+
return Group(_caption(node), table) if node['caption'] or node['number'] else table
|
|
72
|
+
|
|
73
|
+
def _block(node, theme):
|
|
74
|
+
kind = node['type']
|
|
75
|
+
if kind == 'heading':
|
|
76
|
+
prefix = '#' * node['level'] + ' ' + (node['number']['display'] + ' ' if node['number'] else '')
|
|
77
|
+
return _inline(node['children'], Text(prefix, style='bold magenta'), 'bold magenta')
|
|
78
|
+
if kind == 'paragraph': return _inline(node['children'])
|
|
79
|
+
if kind == 'code_block': return Syntax(node['text'].rstrip('\n'), node['lang'] or 'text', theme=theme)
|
|
80
|
+
if kind == 'list': return _list(node, theme)
|
|
81
|
+
if kind == 'block_quote': return Prefixed(_blocks(node['children'], theme), '│ ', style='dim')
|
|
82
|
+
if kind == 'thematic_break': return Text('─' * 40, style='dim')
|
|
83
|
+
if kind == 'table': return _table(node, theme)
|
|
84
|
+
if kind == 'math_block':
|
|
85
|
+
text = node['tex'] + (f" ({node['number']['display']})" if node['number'] else '')
|
|
86
|
+
return Text(text)
|
|
87
|
+
if kind == 'figure': return Group(*_blocks(node['body'], theme), _caption(node))
|
|
88
|
+
if kind == 'panel':
|
|
89
|
+
title = _inline(node['title']['children'], style='bold') if node['title'] else Text(node['label'], style='bold')
|
|
90
|
+
return Group(title, *_blocks(node['body'], theme))
|
|
91
|
+
if kind == 'definition_list':
|
|
92
|
+
out = []
|
|
93
|
+
for item in node['items']:
|
|
94
|
+
out.extend(_inline(term['children'], style='bold') for term in item['terms'])
|
|
95
|
+
for definition in item['definitions']: out.extend(_blocks(definition, theme))
|
|
96
|
+
return Group(*out)
|
|
97
|
+
if kind in ('raw_block', 'script_block', 'operation', 'html_block'): return None
|
|
98
|
+
return Group(*_blocks(node['children'], theme))
|
|
99
|
+
|
|
100
|
+
def _blocks(nodes, theme): return [r for node in nodes if (r := _block(node, theme)) is not None]
|
|
101
|
+
|
|
102
|
+
@delegates(mdhtml.parse_md)
|
|
103
|
+
def md_blocks(
|
|
104
|
+
md:str, # Markdown source
|
|
105
|
+
theme:str='ansi_dark', # Pygments theme for code blocks
|
|
106
|
+
**kwargs
|
|
107
|
+
):
|
|
108
|
+
"One Rich renderable per top-level semantic block of `md`."
|
|
109
|
+
doc = mdhtml.parse_md(md, **kwargs).bake(refs='lenient', links=False)
|
|
110
|
+
notes = [Prefixed(_blocks(body, theme), f'[{label}] ', ' ' * (cell_len(label) + 3)) for label,body in doc.footnotes.items()]
|
|
111
|
+
return _blocks(doc.blocks, theme) + notes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdhtml2term
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Render MDHTML in the terminal with Rich
|
|
5
5
|
Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: mdhtml>=0.1.
|
|
13
|
+
Requires-Dist: mdhtml>=0.1.43
|
|
14
14
|
Requires-Dist: fastcore
|
|
15
15
|
Requires-Dist: rich
|
|
16
16
|
Provides-Extra: dev
|
|
@@ -42,7 +42,7 @@ console = Console()
|
|
|
42
42
|
for block in md_blocks(markdown_text): console.print(block)
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
`md_blocks` takes a Markdown string and returns a list of Rich renderables. `theme` names the Pygments theme for code blocks
|
|
45
|
+
`md_blocks` takes a Markdown string and returns a list of Rich renderables. `theme` names the Pygments theme for code blocks and defaults to `ansi_dark`. Other keyword arguments go to `mdhtml.parse_md`. The shared Rust document engine resolves references and assigns numbers. Unresolved references show their target ids.
|
|
46
46
|
|
|
47
47
|
## Supported content
|
|
48
48
|
|
|
@@ -53,4 +53,4 @@ for block in md_blocks(markdown_text): console.print(block)
|
|
|
53
53
|
- Task list checkboxes, as `[ ]` and `[x]`.
|
|
54
54
|
- A numbered list that resumes after other content keeps its numbering.
|
|
55
55
|
|
|
56
|
-
Other inline elements show their text. Other block containers render their children.
|
|
56
|
+
Other inline elements show their text. Other block containers render their children. Raw payloads and unresolved template instructions are omitted.
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
"Render MDHTML Markdown as Rich renderables for the terminal, one per top-level block."
|
|
2
|
-
import mdhtml
|
|
3
|
-
from fastcore.meta import delegates
|
|
4
|
-
from rich import box
|
|
5
|
-
from rich.cells import cell_len
|
|
6
|
-
from rich.console import Group
|
|
7
|
-
from rich.segment import Segment
|
|
8
|
-
from rich.style import Style
|
|
9
|
-
from rich.syntax import Syntax
|
|
10
|
-
from rich.table import Table
|
|
11
|
-
from rich.text import Text
|
|
12
|
-
|
|
13
|
-
__all__ = ['md_blocks']
|
|
14
|
-
|
|
15
|
-
INLINE = {'strong': 'bold', 'em': 'italic', 'code': 'cyan', 'del': 'strike', 'a': 'underline bright_blue', 'mark': 'reverse'}
|
|
16
|
-
HEADINGS = {'h1', 'h2', 'h3', 'h4', 'h5', 'h6'}
|
|
17
|
-
BLOCKS = HEADINGS | {'p', 'pre', 'ul', 'ol', 'blockquote', 'hr', 'table', 'div', 'section', 'aside', 'figure', 'figcaption', 'details', 'summary', 'dl', 'dt', 'dd'}
|
|
18
|
-
|
|
19
|
-
class Prefixed:
|
|
20
|
-
"Renders `rends` one after another, with `first` before the first line and `rest` before each later line"
|
|
21
|
-
def __init__(self, rends, first, rest=None, style=''):
|
|
22
|
-
self.rends,self.first,self.rest = rends,first,first if rest is None else rest
|
|
23
|
-
self.style = Style.parse(style) if style else None
|
|
24
|
-
|
|
25
|
-
def __rich_console__(self, console, options):
|
|
26
|
-
opts = options.update_width(max(1, options.max_width - cell_len(self.first)))
|
|
27
|
-
lines = [l for r in self.rends for l in console.render_lines(r, opts, pad=False)] or [[]]
|
|
28
|
-
for i,line in enumerate(lines):
|
|
29
|
-
yield Segment(self.first if i == 0 else self.rest, self.style)
|
|
30
|
-
yield from line
|
|
31
|
-
yield Segment.line()
|
|
32
|
-
|
|
33
|
-
def _elem(c, t, style=''):
|
|
34
|
-
nm = getattr(c, 'name', None)
|
|
35
|
-
if nm == '#text': return t.append(c.text, style=style or None)
|
|
36
|
-
if nm == 'br': return t.append('\n')
|
|
37
|
-
a = c.attrs or {}
|
|
38
|
-
if nm == 'img': return t.append(f"[{a.get('alt') or 'image'}]", style=f'{style} dim'.strip())
|
|
39
|
-
if nm == 'input' and a.get('type') == 'checkbox': return t.append('[x]' if 'checked' in a else '[ ]', style=style or None)
|
|
40
|
-
sty = f"{style} {INLINE.get(nm, '')}".strip()
|
|
41
|
-
href = nm == 'a' and a.get('href')
|
|
42
|
-
if href: sty = f'{sty} link {href}'
|
|
43
|
-
n = len(t)
|
|
44
|
-
_inline(c, t, sty)
|
|
45
|
-
if href and t.plain[n:] != href: t.append(f' ({href})', style='dim')
|
|
46
|
-
|
|
47
|
-
def _inline(node, t, style=''):
|
|
48
|
-
for c in node.children: _elem(c, t, style)
|
|
49
|
-
return t
|
|
50
|
-
|
|
51
|
-
def _find(el, name):
|
|
52
|
-
for c in getattr(el, 'children', []):
|
|
53
|
-
if getattr(c, 'name', None) == name: yield c
|
|
54
|
-
else: yield from _find(c, name)
|
|
55
|
-
|
|
56
|
-
def _code_block(pre, theme):
|
|
57
|
-
code = next(_find(pre, 'code'), None)
|
|
58
|
-
if code is None: return _inline(pre, Text())
|
|
59
|
-
src = ''.join(c.text for c in code.children if getattr(c, 'name', None) == '#text')
|
|
60
|
-
cls = (code.attrs or {}).get('class') or ''
|
|
61
|
-
lang = next((w.removeprefix('language-') for w in cls.split() if w.startswith('language-')), 'text')
|
|
62
|
-
return Syntax(src.rstrip('\n'), lang, theme=theme)
|
|
63
|
-
|
|
64
|
-
def _list(el, theme):
|
|
65
|
-
ordered = el.name == 'ol'
|
|
66
|
-
n = int((el.attrs or {}).get('start', 1))
|
|
67
|
-
items = []
|
|
68
|
-
for li in el.children:
|
|
69
|
-
if getattr(li, 'name', None) != 'li': continue
|
|
70
|
-
bullet = f'{n}. ' if ordered else '• '
|
|
71
|
-
items.append(Prefixed(_blocks(li, theme), bullet, ' ' * cell_len(bullet), style='bold'))
|
|
72
|
-
n += 1
|
|
73
|
-
return Group(*items)
|
|
74
|
-
|
|
75
|
-
def _cells(tr): return [c for c in tr.children if getattr(c, 'name', None) in ('td', 'th')]
|
|
76
|
-
|
|
77
|
-
def _table(el):
|
|
78
|
-
head, rows = None, []
|
|
79
|
-
for sec in el.children:
|
|
80
|
-
nm = getattr(sec, 'name', None)
|
|
81
|
-
if nm == 'thead': head = next(_find(sec, 'tr'), None)
|
|
82
|
-
elif nm in ('tbody', 'tfoot'): rows += list(_find(sec, 'tr'))
|
|
83
|
-
elif nm == 'tr': rows.append(sec)
|
|
84
|
-
first = head if head is not None else rows[0] if rows else None
|
|
85
|
-
t = Table(box=box.SIMPLE_HEAD, show_header=head is not None, show_edge=False, pad_edge=False)
|
|
86
|
-
if first is None: return t
|
|
87
|
-
for c in _cells(first): t.add_column(_inline(c, Text()) if head is not None else '', justify=(c.attrs or {}).get('align') or 'left')
|
|
88
|
-
for tr in rows: t.add_row(*[_inline(c, Text()) for c in _cells(tr)])
|
|
89
|
-
return t
|
|
90
|
-
|
|
91
|
-
def _block(el, theme):
|
|
92
|
-
nm = el.name
|
|
93
|
-
if nm in HEADINGS: return _inline(el, Text('#' * int(nm[1]) + ' ', style='bold magenta'), 'bold magenta')
|
|
94
|
-
if nm == 'p': return _inline(el, Text())
|
|
95
|
-
if nm == 'pre': return _code_block(el, theme)
|
|
96
|
-
if nm in ('ul', 'ol'): return _list(el, theme)
|
|
97
|
-
if nm == 'blockquote': return Prefixed(_blocks(el, theme), '│ ', style='dim')
|
|
98
|
-
if nm == 'hr': return Text('─' * 40, style='dim')
|
|
99
|
-
if nm == 'table': return _table(el)
|
|
100
|
-
return Group(*_blocks(el, theme))
|
|
101
|
-
|
|
102
|
-
def _blocks(el, theme):
|
|
103
|
-
"One renderable per block child of `el`, plus one `Text` per run of inline content between them"
|
|
104
|
-
out, t = [], Text()
|
|
105
|
-
def flush():
|
|
106
|
-
nonlocal t
|
|
107
|
-
t.rstrip()
|
|
108
|
-
if t.plain: out.append(t)
|
|
109
|
-
t = Text()
|
|
110
|
-
for c in getattr(el, 'children', []):
|
|
111
|
-
nm = getattr(c, 'name', None)
|
|
112
|
-
if nm in BLOCKS:
|
|
113
|
-
flush()
|
|
114
|
-
out.append(_block(c, theme))
|
|
115
|
-
elif nm == '#text' and not t.plain: t.append(c.text.lstrip())
|
|
116
|
-
else: _elem(c, t)
|
|
117
|
-
flush()
|
|
118
|
-
return out
|
|
119
|
-
|
|
120
|
-
@delegates(mdhtml.md2dom)
|
|
121
|
-
def md_blocks(
|
|
122
|
-
md:str, # Markdown source
|
|
123
|
-
theme:str='ansi_dark', # Pygments theme for code blocks
|
|
124
|
-
**kwargs
|
|
125
|
-
):
|
|
126
|
-
"One Rich renderable per top-level block of `md`"
|
|
127
|
-
return _blocks(mdhtml.md2dom(md, **kwargs), theme)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|