omdev 0.0.0.dev293__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.
omdev/.manifests.json CHANGED
@@ -291,7 +291,7 @@
291
291
  "module": ".tools.doc",
292
292
  "attr": "_CLI_MODULE",
293
293
  "file": "omdev/tools/doc.py",
294
- "line": 103,
294
+ "line": 106,
295
295
  "value": {
296
296
  "$.cli.types.CliModule": {
297
297
  "cmd_name": "doc",
omdev/__about__.py CHANGED
@@ -25,7 +25,7 @@ class Project(ProjectBase):
25
25
  'docutils ~= 0.21',
26
26
 
27
27
  'markdown-it-py ~= 3.0',
28
- # 'mdit-py-plugins ~= 0.4',
28
+ 'mdit-py-plugins ~= 0.4',
29
29
  ],
30
30
 
31
31
  'mypy': [
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014, Jonathan Slenders
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
5
+ following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
8
+ disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
11
+ disclaimer in the documentation and/or other materials provided with the distribution.
12
+
13
+ * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products
14
+ derived from this software without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
17
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
21
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,2 @@
1
+ # https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1604/files
2
+ # See also: https://github.com/joouha/euporie
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .cli import _main
3
+
4
+ _main()
@@ -0,0 +1,94 @@
1
+ import abc
2
+
3
+
4
+ ##
5
+
6
+
7
+ class Border(metaclass=abc.ABCMeta):
8
+ """Base border type."""
9
+
10
+ TOP_LEFT: str
11
+ TOP_SPLIT: str
12
+ TOP_RIGHT: str
13
+ HORIZONTAL: str
14
+ VERTICAL: str
15
+ LEFT_SPLIT: str
16
+ RIGHT_SPLIT: str
17
+ CROSS: str
18
+ BOTTOM_LEFT: str
19
+ BOTTOM_SPLIT: str
20
+ BOTTOM_RIGHT: str
21
+
22
+
23
+ class NoBorder(Border):
24
+ """Invisible border."""
25
+
26
+ TOP_LEFT = ' '
27
+ TOP_SPLIT = ' '
28
+ TOP_RIGHT = ' '
29
+ HORIZONTAL = ' '
30
+ INNER_VERTICAL = ' '
31
+ VERTICAL = ' '
32
+ LEFT_SPLIT = ' '
33
+ RIGHT_SPLIT = ' '
34
+ CROSS = ' '
35
+ BOTTOM_LEFT = ' '
36
+ BOTTOM_SPLIT = ' '
37
+ BOTTOM_RIGHT = ' '
38
+
39
+
40
+ class SquareBorder(Border):
41
+ """Square thin border."""
42
+
43
+ TOP_LEFT = '┌'
44
+ TOP_SPLIT = '┬'
45
+ TOP_RIGHT = '┐'
46
+ HORIZONTAL = '─'
47
+ VERTICAL = '│'
48
+ LEFT_SPLIT = '├'
49
+ RIGHT_SPLIT = '┤'
50
+ CROSS = '┼'
51
+ BOTTOM_LEFT = '└'
52
+ BOTTOM_SPLIT = '┴'
53
+ BOTTOM_RIGHT = '┘'
54
+
55
+
56
+ class RoundBorder(SquareBorder):
57
+ """Thin border with round corners."""
58
+
59
+ TOP_LEFT = '╭'
60
+ TOP_RIGHT = '╮'
61
+ BOTTOM_LEFT = '╰'
62
+ BOTTOM_RIGHT = '╯'
63
+
64
+
65
+ class DoubleBorder(Border):
66
+ """Square border with double lines."""
67
+
68
+ TOP_LEFT = '╔'
69
+ TOP_SPLIT = '╦'
70
+ TOP_RIGHT = '╗'
71
+ HORIZONTAL = '═'
72
+ VERTICAL = '║'
73
+ LEFT_SPLIT = '╠'
74
+ RIGHT_SPLIT = '╣'
75
+ CROSS = '╬'
76
+ BOTTOM_LEFT = '╚'
77
+ BOTTOM_SPLIT = '╩'
78
+ BOTTOM_RIGHT = '╝'
79
+
80
+
81
+ class ThickBorder(Border):
82
+ """Square border with thick lines."""
83
+
84
+ TOP_LEFT = '┏'
85
+ TOP_SPLIT = '┳'
86
+ TOP_RIGHT = '┓'
87
+ HORIZONTAL = '━'
88
+ VERTICAL = '┃'
89
+ LEFT_SPLIT = '┣'
90
+ RIGHT_SPLIT = '┫'
91
+ CROSS = '╋'
92
+ BOTTOM_LEFT = '┗'
93
+ BOTTOM_SPLIT = '┻'
94
+ BOTTOM_RIGHT = '┛'
@@ -0,0 +1,30 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from ... import ptk
5
+ from .markdown import Markdown
6
+ from .styles import MARKDOWN_STYLE
7
+
8
+
9
+ ##
10
+
11
+
12
+ def _main() -> None:
13
+ parser = argparse.ArgumentParser()
14
+ parser.add_argument('file', nargs='?')
15
+ args = parser.parse_args()
16
+
17
+ if args.file is not None:
18
+ with open(args.file) as f:
19
+ src = f.read()
20
+ else:
21
+ src = sys.stdin.read()
22
+
23
+ ptk.print_formatted_text(
24
+ Markdown(src),
25
+ style=ptk.Style(list(MARKDOWN_STYLE)),
26
+ )
27
+
28
+
29
+ if __name__ == '__main__':
30
+ _main()
@@ -0,0 +1,384 @@
1
+ import itertools
2
+ import typing as ta
3
+
4
+ from ... import ptk
5
+ from .border import Border
6
+ from .border import SquareBorder
7
+ from .parser import markdown_parser
8
+ from .tags import TAG_INSETS
9
+ from .tags import TAG_RULES
10
+ from .utils import FormattedTextAlign
11
+ from .utils import align
12
+ from .utils import apply_style
13
+ from .utils import last_line_length
14
+ from .utils import lex
15
+ from .utils import strip
16
+ from .utils import wrap
17
+
18
+
19
+ if ta.TYPE_CHECKING:
20
+ from markdown_it.token import Token
21
+
22
+
23
+ ##
24
+
25
+
26
+ _SIDES = {
27
+ 'left': FormattedTextAlign.LEFT,
28
+ 'right': FormattedTextAlign.RIGHT,
29
+ 'center': FormattedTextAlign.CENTER,
30
+ }
31
+
32
+
33
+ class Markdown:
34
+ """A markdown formatted text renderer. Accepts a markdown string and renders it at a given width."""
35
+
36
+ def __init__(
37
+ self,
38
+ markup: str,
39
+ width: int | None = None,
40
+ strip_trailing_lines: bool = True,
41
+ ) -> None:
42
+ """
43
+ Initialize the markdown formatter.
44
+
45
+ Args:
46
+ markup: The markdown text to render
47
+ width: The width in characters available for rendering. If :py:const:`None` the terminal width will be used
48
+ strip_trailing_lines: If :py:const:`True`, empty lines at the end of the rendered output will be removed
49
+ """
50
+
51
+ super().__init__()
52
+
53
+ self.markup = markup
54
+ self.width = width or ptk.get_app_session().output.get_size().columns
55
+ self.strip_trailing_lines = strip_trailing_lines
56
+
57
+ if (parser := markdown_parser()) is not None:
58
+ self.formatted_text = self.render(
59
+ tokens=parser.parse(self.markup),
60
+ width=self.width,
61
+ )
62
+ else:
63
+ self.formatted_text = lex(
64
+ ptk.to_formatted_text(self.markup),
65
+ lexer_name='markdown',
66
+ )
67
+
68
+ if strip_trailing_lines:
69
+ self.formatted_text = strip(
70
+ self.formatted_text,
71
+ left=False,
72
+ char='\n',
73
+ )
74
+
75
+ def render(
76
+ self,
77
+ tokens: ta.Sequence['Token'],
78
+ width: int = 80,
79
+ left: int = 0,
80
+ ) -> ptk.StyleAndTextTuples:
81
+ """
82
+ Render a list of parsed markdown tokens.
83
+
84
+ Args:
85
+ tokens: The list of parsed tokens to render
86
+ width: The width at which to render the tokens
87
+ left: The position on the current line at which to render the output - used to indent subsequent lines when
88
+ rendering inline blocks like images
89
+ Returns:
90
+ Formatted text
91
+ """
92
+
93
+ ft = []
94
+
95
+ i = 0
96
+ while i < len(tokens):
97
+ token = tokens[i]
98
+
99
+ # If this is an inline block, render it's children
100
+ if token.type == 'inline' and token.children:
101
+ ft += self.render(token.children, width)
102
+ i += 1
103
+
104
+ # Otherwise gather the tokens in the current block
105
+ else:
106
+ nest = 0
107
+ tokens_in_block = 0
108
+ for j, token in enumerate(tokens[i:]):
109
+ nest += token.nesting
110
+ if nest == 0:
111
+ tokens_in_block = j
112
+ break
113
+
114
+ # If there is a special method for rendering the block, use it
115
+
116
+ # Table require a lot of care
117
+ if token.tag == 'table':
118
+ ft += self.render_table(
119
+ tokens[i:i + tokens_in_block + 1],
120
+ width=width,
121
+ left=last_line_length(ft),
122
+ )
123
+
124
+ # We need to keep track of item numbers in ordered lists
125
+ elif token.tag == 'ol':
126
+ ft += self.render_ordered_list(
127
+ tokens[i:i + tokens_in_block + 1],
128
+ width=width,
129
+ left=last_line_length(ft),
130
+ )
131
+
132
+ # Otherwise all other blocks are rendered in the same way
133
+ else:
134
+ ft += self.render_block(
135
+ tokens[i:i + tokens_in_block + 1],
136
+ width=width,
137
+ left=last_line_length(ft),
138
+ )
139
+
140
+ i += j + 1
141
+
142
+ return ft
143
+
144
+ def render_block(
145
+ self,
146
+ tokens: ta.Sequence['Token'],
147
+ width: int,
148
+ left: int = 0,
149
+ ) -> ptk.StyleAndTextTuples:
150
+ """
151
+ Render a list of parsed markdown tokens representing a block element.
152
+
153
+ Args:
154
+ tokens: The list of parsed tokens to render
155
+ width: The width at which to render the tokens
156
+ left: The position on the current line at which to render the output - used to indent subsequent lines when
157
+ rendering inline blocks like images
158
+ Returns:
159
+ Formatted text
160
+ """
161
+
162
+ ft = []
163
+ token = tokens[0]
164
+
165
+ # Restrict width if necessary
166
+ inset = TAG_INSETS.get(token.tag)
167
+ if inset:
168
+ width -= inset
169
+
170
+ style = 'class:md'
171
+ if token.tag:
172
+ style = f'{style}.{token.tag}'
173
+
174
+ # Render innards
175
+ if len(tokens) > 1:
176
+ ft += self.render(tokens[1:-1], width)
177
+ ft = apply_style(ft, style)
178
+ else:
179
+ ft.append((style, token.content))
180
+
181
+ # Apply tag rule
182
+ rule = TAG_RULES.get(token.tag)
183
+ if rule:
184
+ ft = rule(
185
+ ft,
186
+ width,
187
+ left,
188
+ token,
189
+ )
190
+
191
+ return ft
192
+
193
+ def render_ordered_list(
194
+ self,
195
+ tokens: ta.Sequence['Token'],
196
+ width: int,
197
+ left: int = 0,
198
+ ) -> ptk.StyleAndTextTuples:
199
+ """Render an ordered list by adding indices to the child list items."""
200
+
201
+ # Find the list item tokens
202
+ list_level_tokens = []
203
+ nest = 0
204
+ for token in tokens:
205
+ if nest == 1 and token.tag == 'li':
206
+ list_level_tokens.append(token)
207
+ nest += token.nesting
208
+
209
+ # Assign them a marking
210
+ margin_width = len(str(len(list_level_tokens)))
211
+ for i, token in enumerate(list_level_tokens, start=1):
212
+ token.attrs['data-margin'] = str(i).rjust(margin_width) + '.'
213
+ token.attrs['data-list-type'] = 'ol'
214
+
215
+ # Now render the tokens as normal
216
+ return self.render_block(
217
+ tokens,
218
+ width=width,
219
+ left=left,
220
+ )
221
+
222
+ def render_table(
223
+ self,
224
+ tokens: ta.Sequence['Token'],
225
+ width: int,
226
+ left: int = 0,
227
+ border: type[Border] = SquareBorder,
228
+ ) -> ptk.StyleAndTextTuples:
229
+ """
230
+ Render a list of parsed markdown tokens representing a table element.
231
+
232
+ Args:
233
+ tokens: The list of parsed tokens to render
234
+ width: The width at which to render the tokens
235
+ left: The position on the current line at which to render the output - used to indent subsequent lines when
236
+ rendering inline blocks like images
237
+ border: The border style to use to render the table
238
+ Returns:
239
+ Formatted text
240
+ """
241
+
242
+ ft: ptk.StyleAndTextTuples = []
243
+
244
+ # Stack the tokens in the shape of the table
245
+ cell_tokens: list[list[list[Token]]] = []
246
+ i = 0
247
+ while i < len(tokens):
248
+ token = tokens[i]
249
+ if token.type == 'tr_open':
250
+ cell_tokens.append([])
251
+ elif token.type in ('th_open', 'td_open'):
252
+ for j, token in enumerate(tokens[i:]):
253
+ if token.type in ('th_close', 'td_close'):
254
+ cell_tokens[-1].append(list(tokens[i:i + j + 1]))
255
+ break
256
+ i += j
257
+ i += 1
258
+
259
+ def _render_token(
260
+ tokens: ta.Sequence['Token'],
261
+ width: int | None = None,
262
+ ) -> ptk.StyleAndTextTuples:
263
+ """Render a token with correct alignment."""
264
+
265
+ side = 'left'
266
+
267
+ # Check CSS for text alignment
268
+ for style_str in str(tokens[0].attrs.get('style', '')).split(';'):
269
+ if ':' in style_str:
270
+ key, value = style_str.strip().split(':', 1)
271
+ if key.strip() == 'text-align':
272
+ side = value
273
+
274
+ # Render with a very long line length if we do not have a width
275
+ ft = self.render(tokens, width=width or 999999)
276
+
277
+ # If we do have a width, wrap and apply the alignment
278
+ if width:
279
+ ft = wrap(ft, width)
280
+ ft = align(_SIDES[side], ft, width)
281
+
282
+ return ft
283
+
284
+ # Find the naive widths of each cell
285
+ cell_renders: list[list[ptk.StyleAndTextTuples]] = []
286
+ cell_widths: list[list[int]] = []
287
+ for row in cell_tokens:
288
+ cell_widths.append([])
289
+ cell_renders.append([])
290
+ for each_tokens in row:
291
+ rendered = _render_token(each_tokens)
292
+ cell_renders[-1].append(rendered)
293
+ cell_widths[-1].append(ptk.fragment_list_width(rendered))
294
+
295
+ # Calculate row and column widths, accounting for broders
296
+ col_widths = [
297
+ max([row[i] for row in cell_widths])
298
+ for i in range(len(cell_widths[0]))
299
+ ]
300
+
301
+ # Adjust widths and potentially re-render cells. Reduce biggest cells until we fit in width.
302
+ while sum(col_widths) + 3 * (len(col_widths) - 1) + 4 > width:
303
+ idxmax = max(enumerate(col_widths), key=lambda x: x[1])[0]
304
+ col_widths[idxmax] -= 1
305
+
306
+ # Re-render changed cells
307
+ for i, row_widths in enumerate(cell_widths):
308
+ for j, new_width in enumerate(col_widths):
309
+ if row_widths[j] != new_width:
310
+ cell_renders[i][j] = _render_token(
311
+ cell_tokens[i][j],
312
+ width=new_width,
313
+ )
314
+
315
+ # Justify cell contents
316
+ for i, renders_row in enumerate(cell_renders):
317
+ for j, cell in enumerate(renders_row):
318
+ cell_renders[i][j] = align(
319
+ FormattedTextAlign.LEFT,
320
+ cell,
321
+ width=col_widths[j],
322
+ )
323
+
324
+ # Render table
325
+ style = 'class:md.table.border'
326
+
327
+ def _draw_add_border(
328
+ left: str,
329
+ split: str,
330
+ right: str,
331
+ ) -> None:
332
+ ft.append((style, left + border.HORIZONTAL))
333
+
334
+ for col_width in col_widths:
335
+ ft.append((style, border.HORIZONTAL * col_width))
336
+ ft.append((style, border.HORIZONTAL + split + border.HORIZONTAL))
337
+
338
+ ft.pop()
339
+ ft.append((style, border.HORIZONTAL + right + '\n'))
340
+
341
+ # Draw top border
342
+ _draw_add_border(
343
+ border.TOP_LEFT,
344
+ border.TOP_SPLIT,
345
+ border.TOP_RIGHT,
346
+ )
347
+
348
+ # Draw each row
349
+ for i, renders_row in enumerate(cell_renders):
350
+ for row_lines in itertools.zip_longest(*map(ptk.split_lines, renders_row)):
351
+ # Draw each line in each row
352
+ ft.append((style, border.VERTICAL + ' '))
353
+
354
+ for j, line in enumerate(row_lines):
355
+ if line is None:
356
+ line = [('', ' ' * col_widths[j])]
357
+ ft += line
358
+ ft.append((style, ' ' + border.VERTICAL + ' '))
359
+
360
+ ft.pop()
361
+ ft.append((style, ' ' + border.VERTICAL + '\n'))
362
+
363
+ # Draw border between rows
364
+ if i < len(cell_renders) - 1:
365
+ _draw_add_border(
366
+ border.LEFT_SPLIT,
367
+ border.CROSS,
368
+ border.RIGHT_SPLIT,
369
+ )
370
+
371
+ # Draw bottom border
372
+ _draw_add_border(
373
+ border.BOTTOM_LEFT,
374
+ border.BOTTOM_SPLIT,
375
+ border.BOTTOM_RIGHT,
376
+ )
377
+
378
+ ft.append(('', '\n'))
379
+ return ft
380
+
381
+ def __pt_formatted_text__(self) -> ptk.StyleAndTextTuples:
382
+ """Formatted text magic method."""
383
+
384
+ return self.formatted_text
@@ -0,0 +1,42 @@
1
+ import typing as ta
2
+ import warnings
3
+
4
+ from omlish import lang
5
+
6
+
7
+ if ta.TYPE_CHECKING:
8
+ from markdown_it import MarkdownIt
9
+
10
+
11
+ ##
12
+
13
+
14
+ @lang.cached_function(lock=True)
15
+ def markdown_parser() -> ta.Optional['MarkdownIt']:
16
+ try:
17
+ from markdown_it import MarkdownIt
18
+ except ModuleNotFoundError:
19
+ warnings.warn('The markdown parser requires `markdown-it-py` to be installed')
20
+ return None
21
+
22
+ parser = (
23
+ MarkdownIt()
24
+ .enable('linkify')
25
+ .enable('table')
26
+ .enable('strikethrough')
27
+ )
28
+
29
+ try:
30
+ import mdit_py_plugins # noqa F401
31
+ except ModuleNotFoundError:
32
+ pass
33
+ else:
34
+ from mdit_py_plugins.amsmath import amsmath_plugin # noqa
35
+ from mdit_py_plugins.dollarmath.index import dollarmath_plugin # noqa
36
+ from mdit_py_plugins.texmath.index import texmath_plugin # noqa
37
+
38
+ parser.use(texmath_plugin)
39
+ parser.use(dollarmath_plugin)
40
+ parser.use(amsmath_plugin)
41
+
42
+ return parser
@@ -0,0 +1,29 @@
1
+ import typing as ta
2
+
3
+
4
+ ##
5
+
6
+
7
+ MARKDOWN_STYLE: ta.Sequence[tuple[str, str]] = [
8
+ ('md.h1', 'bold underline'),
9
+ ('md.h1.border', 'fg:ansiyellow nounderline'),
10
+ ('md.h2', 'bold'),
11
+ ('md.h2.border', 'fg:grey nobold'),
12
+ ('md.h3', 'bold'),
13
+ ('md.h4', 'bold italic'),
14
+ ('md.h5', 'underline'),
15
+ ('md.h6', 'italic'),
16
+ ('md.code.inline', 'bg:#333'),
17
+ ('md.strong', 'bold'),
18
+ ('md.em', 'italic'),
19
+ ('md.hr', 'fg:ansired'),
20
+ ('md.ul.margin', 'fg:ansiyellow'),
21
+ ('md.ol.margin', 'fg:ansicyan'),
22
+ ('md.blockquote', 'fg:ansipurple'),
23
+ ('md.blockquote.margin', 'fg:grey'),
24
+ ('md.th', 'bold'),
25
+ ('md.a', 'underline fg:ansibrightblue'),
26
+ ('md.s', 'strike'),
27
+ ('md.img', 'bg:cyan fg:black'),
28
+ ('md.img.border', 'fg:cyan bg:default'),
29
+ ]