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.
@@ -1,5 +1,10 @@
1
1
  <!-- do not remove -->
2
2
 
3
+ ## 0.1.1
4
+
5
+ - API update
6
+
7
+
3
8
  ## 0.1.0
4
9
 
5
10
  ### New Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdhtml2term
3
- Version: 0.1.0
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.39
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, and defaults to `ansi_dark`. Other keyword arguments go to `mdhtml.md2dom`.
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, and defaults to `ansi_dark`. Other keyword arguments go to `mdhtml.md2dom`.
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.
@@ -1,3 +1,4 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.1"
2
+
2
3
 
3
4
  from .core import md_blocks
@@ -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.0
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.39
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, and defaults to `ansi_dark`. Other keyword arguments go to `mdhtml.md2dom`.
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,4 +1,4 @@
1
- mdhtml>=0.1.39
1
+ mdhtml>=0.1.43
2
2
  fastcore
3
3
  rich
4
4
 
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3 :: Only",
16
16
  ]
17
17
 
18
- dependencies = ["mdhtml>=0.1.39", "fastcore", "rich"]
18
+ dependencies = ["mdhtml>=0.1.43", "fastcore", "rich"]
19
19
 
20
20
  [project.optional-dependencies]
21
21
  dev = [
@@ -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