omdev 0.0.0.dev294__py3-none-any.whl → 0.0.0.dev295__py3-none-any.whl

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.
@@ -0,0 +1,298 @@
1
+ import typing as ta
2
+
3
+ from ... import ptk
4
+ from .border import DoubleBorder
5
+ from .border import SquareBorder
6
+ from .utils import FormattedTextAlign
7
+ from .utils import add_border
8
+ from .utils import align
9
+ from .utils import apply_style
10
+ from .utils import indent
11
+ from .utils import lex
12
+ from .utils import strip
13
+ from .utils import wrap
14
+
15
+
16
+ if ta.TYPE_CHECKING:
17
+ from markdown_it.token import Token
18
+
19
+
20
+ TagRule: ta.TypeAlias = ta.Callable[
21
+ [
22
+ ptk.StyleAndTextTuples,
23
+ int,
24
+ int,
25
+ 'Token',
26
+ ],
27
+ ptk.StyleAndTextTuples,
28
+ ]
29
+
30
+
31
+ ##
32
+
33
+
34
+ def h1(
35
+ ft: ptk.StyleAndTextTuples,
36
+ width: int,
37
+ left: int,
38
+ token: 'Token',
39
+ ) -> ptk.StyleAndTextTuples:
40
+ """Format a top-level heading wrapped and centered with a full width double border."""
41
+
42
+ ft = wrap(ft, width - 4)
43
+ ft = align(FormattedTextAlign.CENTER, ft, width=width - 4)
44
+ ft = add_border(ft, width, style='class:md.h1.border', border=DoubleBorder)
45
+ ft.append(('', '\n\n'))
46
+ return ft
47
+
48
+
49
+ def h2(
50
+ ft: ptk.StyleAndTextTuples,
51
+ width: int,
52
+ left: int,
53
+ token: 'Token',
54
+ ) -> ptk.StyleAndTextTuples:
55
+ """Format a 2nd-level headding wrapped and centered with a double border."""
56
+
57
+ ft = wrap(ft, width=width - 4)
58
+ ft = align(FormattedTextAlign.CENTER, ft)
59
+ ft = add_border(ft, style='class:md.h2.border', border=SquareBorder)
60
+ ft = align(FormattedTextAlign.CENTER, ft, width=width)
61
+ ft.append(('', '\n\n'))
62
+ return ft
63
+
64
+
65
+ def h(
66
+ ft: ptk.StyleAndTextTuples,
67
+ width: int,
68
+ left: int,
69
+ token: 'Token',
70
+ ) -> ptk.StyleAndTextTuples:
71
+ """Format headings wrapped and centeredr."""
72
+
73
+ ft = wrap(ft, width)
74
+ ft = align(FormattedTextAlign.CENTER, ft, width=width)
75
+ ft.append(('', '\n\n'))
76
+ return ft
77
+
78
+
79
+ def p(
80
+ ft: ptk.StyleAndTextTuples,
81
+ width: int,
82
+ left: int,
83
+ token: 'Token',
84
+ ) -> ptk.StyleAndTextTuples:
85
+ """Format paragraphs wrapped."""
86
+
87
+ ft = wrap(ft, width)
88
+ ft.append(('', '\n' if token.hidden else '\n\n'))
89
+ return ft
90
+
91
+
92
+ def ul(
93
+ ft: ptk.StyleAndTextTuples,
94
+ width: int,
95
+ left: int,
96
+ token: 'Token',
97
+ ) -> ptk.StyleAndTextTuples:
98
+ """Format unordered lists."""
99
+
100
+ ft.append(('', '\n'))
101
+ return ft
102
+
103
+
104
+ def ol(
105
+ ft: ptk.StyleAndTextTuples,
106
+ width: int,
107
+ left: int,
108
+ token: 'Token',
109
+ ) -> ptk.StyleAndTextTuples:
110
+ """Formats ordered lists."""
111
+
112
+ ft.append(('', '\n'))
113
+ return ft
114
+
115
+
116
+ def li(
117
+ ft: ptk.StyleAndTextTuples,
118
+ width: int,
119
+ left: int,
120
+ token: 'Token',
121
+ ) -> ptk.StyleAndTextTuples:
122
+ """Formats list items."""
123
+
124
+ ft = strip(ft)
125
+
126
+ # Determine if this is an ordered or unordered list
127
+ if token.attrs.get('data-list-type') == 'ol':
128
+ margin_style = 'class:md.ol.margin'
129
+ else:
130
+ margin_style = 'class:md.ul.margin'
131
+
132
+ # Get the margin (potentially contains aligned item numbers)
133
+ margin = str(token.attrs.get('data-margin', '•'))
134
+
135
+ # We put a speace each side of the margin
136
+ ft = indent(ft, margin=' ' * (len(margin) + 2), style=margin_style)
137
+ ft[0] = (ft[0][0], f' {margin} ')
138
+
139
+ ft.append(('', '\n'))
140
+ return ft
141
+
142
+
143
+ def hr(
144
+ ft: ptk.StyleAndTextTuples,
145
+ width: int,
146
+ left: int,
147
+ token: 'Token',
148
+ ) -> ptk.StyleAndTextTuples:
149
+ """Format horizontal rules."""
150
+
151
+ ft = [
152
+ ('class:md.hr', '─' * width),
153
+ ('', '\n\n'),
154
+ ]
155
+ return ft
156
+
157
+
158
+ def br(
159
+ ft: ptk.StyleAndTextTuples,
160
+ width: int,
161
+ left: int,
162
+ token: 'Token',
163
+ ) -> ptk.StyleAndTextTuples:
164
+ """Format line breaks."""
165
+
166
+ return [('', '\n')]
167
+
168
+
169
+ def blockquote(
170
+ ft: ptk.StyleAndTextTuples,
171
+ width: int,
172
+ left: int,
173
+ token: 'Token',
174
+ ) -> ptk.StyleAndTextTuples:
175
+ """Format blockquotes with a solid left margin."""
176
+
177
+ ft = strip(ft)
178
+ ft = indent(ft, margin='▌ ', style='class:md.blockquote.margin')
179
+ ft.append(('', '\n\n'))
180
+ return ft
181
+
182
+
183
+ def code(
184
+ ft: ptk.StyleAndTextTuples,
185
+ width: int,
186
+ left: int,
187
+ token: 'Token',
188
+ ) -> ptk.StyleAndTextTuples:
189
+ """Format inline code, and lexes and formats code blocks with a border."""
190
+
191
+ if token.block:
192
+ ft = strip(ft, left=False, right=True, char='\n')
193
+ ft = lex(ft, lexer_name=token.info)
194
+ ft = align(FormattedTextAlign.LEFT, ft, width - 4)
195
+ ft = add_border(ft, width, style='class:md.code.border', border=SquareBorder)
196
+ ft.append(('', '\n\n'))
197
+ else:
198
+ ft = apply_style(ft, style='class:md.code.inline')
199
+
200
+ return ft
201
+
202
+
203
+ def math(
204
+ ft: ptk.StyleAndTextTuples,
205
+ width: int,
206
+ left: int,
207
+ token: 'Token',
208
+ ) -> ptk.StyleAndTextTuples:
209
+ """Format inline maths, and quotes math blocks."""
210
+
211
+ if token.block:
212
+ return blockquote(ft, width - 2, left, token)
213
+ else:
214
+ return ft
215
+
216
+
217
+ def a(
218
+ ft: ptk.StyleAndTextTuples,
219
+ width: int,
220
+ left: int,
221
+ token: 'Token',
222
+ ) -> ptk.StyleAndTextTuples:
223
+ """Format hyperlinks and adds link escape sequences."""
224
+
225
+ result: ptk.StyleAndTextTuples = []
226
+ href = token.attrs.get('href')
227
+ if href:
228
+ result.append(('[ZeroWidthEscape]', f'\x1b]8;;{href}\x1b\\'))
229
+ result += ft
230
+ if href:
231
+ result.append(('[ZeroWidthEscape]', '\x1b]8;;\x1b\\'))
232
+ return result
233
+
234
+
235
+ def img(
236
+ ft: ptk.StyleAndTextTuples,
237
+ width: int,
238
+ left: int,
239
+ token: 'Token',
240
+ ) -> ptk.StyleAndTextTuples:
241
+ """Format image titles."""
242
+
243
+ bounds = ('', '')
244
+ if not ptk.to_plain_text(ft):
245
+ # Add fallback text if there is no image title
246
+ title = str(token.attrs.get('alt'))
247
+
248
+ # Try getting the filename
249
+ src = str(token.attrs.get('src', ''))
250
+ if not title and not src.startswith('data:'):
251
+ title = src.rsplit('/', 1)[-1]
252
+ if not title:
253
+ title = 'Image'
254
+ ft = [('class:md.img', title)]
255
+
256
+ # Add the sunrise emoji to represent an image. I would use :framed_picture:, but it requires multiple code-points
257
+ # and causes breakage in many terminals
258
+ result = [('class:md.img', '🌄 '), *ft]
259
+ result = apply_style(result, style='class:md.img')
260
+ result = [
261
+ ('class:md.img.border', f'{bounds[0]}'),
262
+ *result,
263
+ ('class:md.img.border', f'{bounds[1]}'),
264
+ ]
265
+ return result
266
+
267
+
268
+ ##
269
+
270
+
271
+ # Maps HTML tag names to formatting functions. Functionality can be extended by modifying this dictionary
272
+ TAG_RULES: ta.Mapping[str, TagRule] = {
273
+ 'h1': h1,
274
+ 'h2': h2,
275
+ 'h3': h,
276
+ 'h4': h,
277
+ 'h5': h,
278
+ 'h6': h,
279
+ 'p': p,
280
+ 'ul': ul,
281
+ 'ol': ol,
282
+ 'li': li,
283
+ 'hr': hr,
284
+ 'br': br,
285
+ 'blockquote': blockquote,
286
+ 'code': code,
287
+ 'math': math,
288
+ 'a': a,
289
+ 'img': img,
290
+ }
291
+
292
+
293
+ # Mapping showing how much width the formatting of block elements used. This is used to reduce the available width when
294
+ # rendering child elements
295
+ TAG_INSETS = {
296
+ 'li': 3,
297
+ 'blockquote': 2,
298
+ }
@@ -0,0 +1,365 @@
1
+ import enum
2
+ import typing as ta
3
+
4
+ from pygments.lexers import get_lexer_by_name
5
+ from pygments.util import ClassNotFound
6
+
7
+ from ... import ptk
8
+ from .border import Border
9
+ from .border import SquareBorder
10
+
11
+
12
+ ##
13
+
14
+
15
+ class FormattedTextAlign(enum.Enum):
16
+ """Alignment of formatted text."""
17
+
18
+ LEFT = 'LEFT'
19
+ RIGHT = 'RIGHT'
20
+ CENTER = 'CENTER'
21
+
22
+
23
+ def last_line_length(ft: ptk.StyleAndTextTuples) -> int:
24
+ """Calculate the length of the last line in formatted text."""
25
+
26
+ line: ptk.StyleAndTextTuples = []
27
+ for style, text, *_ in ft[::-1]:
28
+ index = text.find('\n')
29
+ line.append((style, text[index + 1:]))
30
+ if index > -1:
31
+ break
32
+
33
+ return ptk.fragment_list_width(line)
34
+
35
+
36
+ def max_line_width(ft: ptk.StyleAndTextTuples) -> int:
37
+ """Calculate the length of the longest line in formatted text."""
38
+
39
+ return max(ptk.fragment_list_width(line) for line in ptk.split_lines(ft))
40
+
41
+
42
+ def fragment_list_to_words(fragments: ptk.StyleAndTextTuples) -> ta.Iterable[ptk.OneStyleAndTextTuple]:
43
+ """Split formatted text into word fragments."""
44
+
45
+ for style, string, *mouse_handler in fragments:
46
+ parts = string.split(' ')
47
+ for part in parts[:-1]:
48
+ yield ta.cast('ptk.OneStyleAndTextTuple', (style, part, *mouse_handler))
49
+ yield ta.cast('ptk.OneStyleAndTextTuple', (style, ' ', *mouse_handler))
50
+
51
+ yield ta.cast('ptk.OneStyleAndTextTuple', (style, parts[-1], *mouse_handler))
52
+
53
+
54
+ def apply_style(ft: ptk.StyleAndTextTuples, style: str) -> ptk.StyleAndTextTuples:
55
+ """Apply a style to formatted text."""
56
+
57
+ return [
58
+ (
59
+ f'{fragment_style} {style}'
60
+ if '[ZeroWidthEscape]' not in fragment_style else fragment_style,
61
+ text,
62
+ )
63
+ for (fragment_style, text, *_) in ft
64
+ ]
65
+
66
+
67
+ def strip(
68
+ ft: ptk.StyleAndTextTuples,
69
+ left: bool = True,
70
+ right: bool = True,
71
+ char: str | None = None,
72
+ ) -> ptk.StyleAndTextTuples:
73
+ """
74
+ Strip whitespace (or a given character) from the ends of formatted text.
75
+
76
+ Args:
77
+ ft: The formatted text to strip
78
+ left: If :py:const:`True`, strip from the left side of the input
79
+ right: If :py:const:`True`, strip from the right side of the input
80
+ char: The character to strip. If :py:const:`None`, strips whitespace
81
+ Returns:
82
+ The stripped formatted text
83
+ """
84
+
85
+ result = ft[:]
86
+ for toggle, index, strip_func in [
87
+ (left, 0, str.lstrip),
88
+ (right, -1, str.rstrip),
89
+ ]:
90
+ if result and toggle:
91
+ text = strip_func(result[index][1], char)
92
+
93
+ while result and not text:
94
+ del result[index]
95
+ if not result:
96
+ break
97
+
98
+ text = strip_func(result[index][1], char)
99
+
100
+ if result and '[ZeroWidthEscape]' not in result[index][0]:
101
+ result[index] = (result[index][0], text)
102
+
103
+ return result
104
+
105
+
106
+ def truncate(
107
+ ft: ptk.StyleAndTextTuples,
108
+ width: int,
109
+ style: str = '',
110
+ placeholder: str = '…',
111
+ ) -> ptk.StyleAndTextTuples:
112
+ """
113
+ Truncates all lines at a given length.
114
+
115
+ Args:
116
+ ft: The formatted text to truncate
117
+ width: The width at which to truncate the text
118
+ style: The style to apply to the truncation placeholder. The style of the truncated text will be used if not
119
+ provided
120
+ placeholder: The string that will appear at the end of a truncated line
121
+ Returns:
122
+ The truncated formatted text
123
+ """
124
+
125
+ result: ptk.StyleAndTextTuples = []
126
+ phw = sum(ptk.get_cwidth(c) for c in placeholder)
127
+ for line in ptk.split_lines(ft):
128
+ used_width = 0
129
+ for item in line:
130
+ fragment_width = sum(
131
+ ptk.get_cwidth(c)
132
+ for c in item[1]
133
+ if '[ZeroWidthEscape]' not in item[0]
134
+ )
135
+
136
+ if used_width + fragment_width > width - phw:
137
+ remaining_width = width - used_width - fragment_width - phw
138
+ result.append((item[0], item[1][:remaining_width]))
139
+ result.append((style or item[0], placeholder))
140
+ break
141
+
142
+ result.append(item)
143
+ used_width += fragment_width
144
+
145
+ result.append(('', '\n'))
146
+
147
+ result.pop()
148
+ return result
149
+
150
+
151
+ def wrap(
152
+ ft: ptk.StyleAndTextTuples,
153
+ width: int,
154
+ style: str = '',
155
+ placeholder: str = '…',
156
+ ) -> ptk.StyleAndTextTuples:
157
+ """
158
+ Wraps formatted text at a given width. If words are longer than the given line they will be truncated
159
+
160
+ Args:
161
+ ft: The formatted text to wrap
162
+ width: The width at which to wrap the text
163
+ style: The style to apply to the truncation placeholder
164
+ placeholder: The string that will appear at the end of a truncated line
165
+ Returns:
166
+ The wrapped formatted text
167
+ """
168
+
169
+ result: ptk.StyleAndTextTuples = []
170
+ lines = list(ptk.split_lines(ft))
171
+ for i, line in enumerate(lines):
172
+ if ptk.fragment_list_width(line) <= width:
173
+ result += line
174
+ if i < len(lines) - 1:
175
+ result.append(('', '\n'))
176
+ continue
177
+
178
+ used_width = 0
179
+ for item in fragment_list_to_words(line):
180
+ fragment_width = sum(
181
+ ptk.get_cwidth(c)
182
+ for c in item[1]
183
+ if '[ZeroWidthEscape]' not in item[0]
184
+ )
185
+
186
+ # Start a new line we are at the end
187
+ if used_width + fragment_width > width and used_width > 0:
188
+ # Remove trailing whitespace
189
+ result = strip(result, left=False)
190
+ result.append(('', '\n'))
191
+ used_width = 0
192
+
193
+ # Truncate words longer than a line
194
+ if fragment_width > width and used_width == 0:
195
+ result += truncate([item], width, style, placeholder)
196
+ used_width += fragment_width
197
+
198
+ # Left-strip words at the start of a line
199
+ elif used_width == 0:
200
+ result += strip([item], right=False)
201
+ used_width += fragment_width
202
+
203
+ # Otherwise just add the word to the line
204
+ else:
205
+ result.append(item)
206
+ used_width += fragment_width
207
+
208
+ return result
209
+
210
+
211
+ def align(
212
+ how: FormattedTextAlign,
213
+ ft: ptk.StyleAndTextTuples,
214
+ width: int | None = None,
215
+ style: str = '',
216
+ placeholder: str = '…',
217
+ ) -> ptk.StyleAndTextTuples:
218
+ """
219
+ Align formatted text at a given width.
220
+
221
+ Args:
222
+ how: The alignment direction
223
+ ft: The formatted text to strip
224
+ width: The width to which the output should be padded. If :py:const:`None`, the length of the longest line is
225
+ used
226
+ style: The style to apply to the padding
227
+ placeholder: The string that will appear at the end of a truncated line
228
+ Returns:
229
+ The aligned formatted text
230
+ """
231
+
232
+ lines = ptk.split_lines(ft)
233
+ if width is None:
234
+ lines = [strip(line) for line in ptk.split_lines(ft)]
235
+ width = max(ptk.fragment_list_width(line) for line in lines)
236
+
237
+ result: ptk.StyleAndTextTuples = []
238
+ for line in lines:
239
+ line_width = ptk.fragment_list_width(line)
240
+
241
+ # Truncate the line if it is too long
242
+ if line_width > width:
243
+ result += truncate(line, width, style, placeholder)
244
+
245
+ else:
246
+ pad_left = pad_right = 0
247
+
248
+ if how == FormattedTextAlign.CENTER:
249
+ pad_left = (width - line_width) // 2
250
+ pad_right = width - line_width - pad_left
251
+
252
+ elif how == FormattedTextAlign.LEFT:
253
+ pad_right = width - line_width
254
+
255
+ elif how == FormattedTextAlign.RIGHT:
256
+ pad_left = width - line_width
257
+
258
+ if pad_left:
259
+ result.append((style, ' ' * pad_left))
260
+
261
+ result += line
262
+
263
+ if pad_right:
264
+ result.append((style, ' ' * pad_right))
265
+
266
+ result.append((style, '\n'))
267
+
268
+ result.pop()
269
+ return result
270
+
271
+
272
+ def indent(
273
+ ft: ptk.StyleAndTextTuples,
274
+ margin: str = ' ',
275
+ style: str = '',
276
+ skip_first: bool = False,
277
+ ) -> ptk.StyleAndTextTuples:
278
+ """
279
+ Indents formatted text with a given margin.
280
+
281
+ Args:
282
+ ft: The formatted text to strip
283
+ margin: The margin string to add
284
+ style: The style to apply to the margin
285
+ skip_first: If :py:const:`True`, the first line is skipped
286
+ Returns:
287
+ The indented formatted text
288
+ """
289
+
290
+ result: ptk.StyleAndTextTuples = []
291
+ for i, line in enumerate(ptk.split_lines(ft)):
292
+ if not (i == 0 and skip_first):
293
+ result.append((style, margin))
294
+ result += line
295
+ result.append(('', '\n'))
296
+
297
+ result.pop()
298
+ return result
299
+
300
+
301
+ def add_border(
302
+ ft: ptk.StyleAndTextTuples,
303
+ width: int | None = None,
304
+ style: str = '',
305
+ border: type[Border] = SquareBorder,
306
+ ) -> ptk.StyleAndTextTuples:
307
+ """
308
+ Adds a border around formatted text.
309
+
310
+ Args:
311
+ ft: The formatted text to enclose with a border
312
+ width: The target width of the output including the border
313
+ style: The style to apply to the border
314
+ border: The border to apply
315
+ Returns:
316
+ The indented formatted text
317
+ """
318
+
319
+ # if border is None:
320
+ # See mypy issue #4236
321
+ # border = cast("Type[Border]", Border)
322
+ if width is None:
323
+ width = max_line_width(ft) + 4
324
+
325
+ # ft = align(FormattedTextAlign.LEFT, ft, width - 4)
326
+ result: ptk.StyleAndTextTuples = []
327
+
328
+ result.append((
329
+ style,
330
+ border.TOP_LEFT + border.HORIZONTAL * (width - 2) + border.TOP_RIGHT + '\n',
331
+ ))
332
+
333
+ for line in ptk.split_lines(ft):
334
+ result += [
335
+ (style, border.VERTICAL),
336
+ ('', ' '),
337
+ *line,
338
+ ('', ' '),
339
+ (style, border.VERTICAL + '\n'),
340
+ ]
341
+
342
+ result.append((
343
+ style,
344
+ border.BOTTOM_LEFT + border.HORIZONTAL * (width - 2) + border.BOTTOM_RIGHT,
345
+ ))
346
+
347
+ return result
348
+
349
+
350
+ def lex(ft: ptk.StyleAndTextTuples, lexer_name: str) -> ptk.StyleAndTextTuples:
351
+ """Format formatted text using a named :py:mod:`pygments` lexer."""
352
+
353
+ from prompt_toolkit.lexers.pygments import _token_cache # noqa
354
+
355
+ text = ptk.fragment_list_to_text(ft)
356
+
357
+ try:
358
+ lexer = get_lexer_by_name(lexer_name)
359
+ except ClassNotFound:
360
+ return ft
361
+ else:
362
+ return [
363
+ (_token_cache[t], v)
364
+ for _, t, v in lexer.get_tokens_unprocessed(text)
365
+ ]
omdev/tools/doc.py CHANGED
@@ -25,6 +25,9 @@ else:
25
25
  markdown_it = lang.proxy_import('markdown_it')
26
26
 
27
27
 
28
+ ##
29
+
30
+
28
31
  def rst2html(rst, report_level=None):
29
32
  kwargs = {
30
33
  'writer_name': 'html',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omdev
3
- Version: 0.0.0.dev294
3
+ Version: 0.0.0.dev295
4
4
  Summary: omdev
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,13 +12,14 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omlish==0.0.0.dev294
15
+ Requires-Dist: omlish==0.0.0.dev295
16
16
  Provides-Extra: all
17
17
  Requires-Dist: black~=25.1; extra == "all"
18
18
  Requires-Dist: pycparser~=2.22; extra == "all"
19
19
  Requires-Dist: pcpp~=1.30; extra == "all"
20
20
  Requires-Dist: docutils~=0.21; extra == "all"
21
21
  Requires-Dist: markdown-it-py~=3.0; extra == "all"
22
+ Requires-Dist: mdit-py-plugins~=0.4; extra == "all"
22
23
  Requires-Dist: mypy~=1.15; extra == "all"
23
24
  Requires-Dist: gprof2dot~=2025.4; extra == "all"
24
25
  Requires-Dist: prompt-toolkit~=3.0; extra == "all"
@@ -31,6 +32,7 @@ Requires-Dist: pcpp~=1.30; extra == "c"
31
32
  Provides-Extra: doc
32
33
  Requires-Dist: docutils~=0.21; extra == "doc"
33
34
  Requires-Dist: markdown-it-py~=3.0; extra == "doc"
35
+ Requires-Dist: mdit-py-plugins~=0.4; extra == "doc"
34
36
  Provides-Extra: mypy
35
37
  Requires-Dist: mypy~=1.15; extra == "mypy"
36
38
  Provides-Extra: prof