formol 0.1.0__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.
- formol-0.1.0/PKG-INFO +25 -0
- formol-0.1.0/formol/__init__.py +678 -0
- formol-0.1.0/pyproject.toml +50 -0
formol-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: formol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: C/C++ block comment beautifier
|
|
5
|
+
Home-page: https://github.com/eepp/formol/
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Philippe Proulx
|
|
8
|
+
Author-email: eeppeliteloop@gmail.com
|
|
9
|
+
Requires-Python: >=3.8,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: C
|
|
15
|
+
Classifier: Programming Language :: C++
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
24
|
+
Project-URL: Bug tracker, https://github.com/eepp/formol/issues/
|
|
25
|
+
Project-URL: Repository, https://github.com/eepp/formol/
|
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
# Copyright (c) 2024 Philippe Proulx <pproulx@efficios.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
18
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
20
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
#
|
|
22
|
+
# pyright: strict
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import List, Sequence, Union, Pattern, Match, Optional
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Element base.
|
|
30
|
+
class _Elem:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
# Paragraph.
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class _Para(_Elem):
|
|
36
|
+
words: List[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Simple list item.
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class _SimpleListItem:
|
|
42
|
+
elems: List[_Elem]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Simple list base.
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class _SimpleList(_Elem):
|
|
48
|
+
items: List[_SimpleListItem]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Unordered list.
|
|
52
|
+
class _Ul(_SimpleList):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Definition list item.
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class _DlItem:
|
|
59
|
+
term: str
|
|
60
|
+
elems: List[_Elem]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Definition list.
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class _Dl(_Elem):
|
|
66
|
+
items: List[_DlItem]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Unchanged block.
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class _AsIs(_Elem):
|
|
72
|
+
lines: List[str]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Heading base.
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class _Heading(_Elem):
|
|
78
|
+
text: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Level 1 heading.
|
|
82
|
+
class _H1(_Heading):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Level 2 heading.
|
|
87
|
+
class _H2(_Heading):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Removes trailing empty lines from `lines`.
|
|
92
|
+
def _remove_trailing_empty_lines(lines: List[str]):
|
|
93
|
+
assert len(lines) >= 1
|
|
94
|
+
|
|
95
|
+
while True:
|
|
96
|
+
if lines[-1] != '':
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
del lines[-1]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Returns the indentation string of `count` spaces.
|
|
103
|
+
def _indent_str(count: int):
|
|
104
|
+
return ' ' * count
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Parses the raw lines of a comment on construction; the `elems`
|
|
108
|
+
# property is then the resulting list of elements.
|
|
109
|
+
class _Parser:
|
|
110
|
+
def __init__(self, lines: Sequence[str]):
|
|
111
|
+
self._lines = lines
|
|
112
|
+
self._at = 0
|
|
113
|
+
self._last_line_index = len(lines) - 1
|
|
114
|
+
self._elems : List[_Elem] = []
|
|
115
|
+
self._parse()
|
|
116
|
+
|
|
117
|
+
# Resulting list of elements.
|
|
118
|
+
@property
|
|
119
|
+
def elems(self):
|
|
120
|
+
return self._elems
|
|
121
|
+
|
|
122
|
+
def _add_elem(self, elem: _Elem):
|
|
123
|
+
self._elems.append(elem)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def _cur_line(self):
|
|
127
|
+
return self._lines[self._at]
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def _at_last_line(self):
|
|
131
|
+
return self._at == self._last_line_index
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def _prev_line(self):
|
|
135
|
+
if len(self._lines) == 1:
|
|
136
|
+
return ''
|
|
137
|
+
|
|
138
|
+
return '' if self._at == 0 else self._lines[self._at - 1]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def _next_line(self):
|
|
142
|
+
if len(self._lines) == 1:
|
|
143
|
+
return ''
|
|
144
|
+
|
|
145
|
+
return '' if self._at >= self._last_line_index else self._lines[self._at + 1]
|
|
146
|
+
|
|
147
|
+
def _match_cur_line(self, pat: Union[str, Pattern[str]]) -> Optional[Match[str]]:
|
|
148
|
+
return re.match(pat, self._cur_line)
|
|
149
|
+
|
|
150
|
+
def _match_next_line(self, pat: Union[str, Pattern[str]]) -> Optional[Match[str]]:
|
|
151
|
+
return re.match(pat, self._next_line)
|
|
152
|
+
|
|
153
|
+
def _skip_empty_lines(self):
|
|
154
|
+
while not self._is_done:
|
|
155
|
+
if self._cur_line != '':
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
self._at += 1
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def _is_done(self):
|
|
162
|
+
return self._at >= len(self._lines)
|
|
163
|
+
|
|
164
|
+
# Tries to parse a level 1 heading.
|
|
165
|
+
def _try_parse_h1(self):
|
|
166
|
+
if self._next_line == '===' or self._match_next_line(r'^═+$'):
|
|
167
|
+
elem = _H1(self._cur_line)
|
|
168
|
+
self._at += 2
|
|
169
|
+
return elem
|
|
170
|
+
|
|
171
|
+
# Tries to parse a level 2 heading.
|
|
172
|
+
def _try_parse_h2(self):
|
|
173
|
+
if self._next_line == '---' or self._match_next_line(r'^─+$'):
|
|
174
|
+
elem = _H2(self._cur_line)
|
|
175
|
+
self._at += 2
|
|
176
|
+
return elem
|
|
177
|
+
|
|
178
|
+
# Unindents the lines by `count` spaces when possible, keeping
|
|
179
|
+
# unindentable lines as is.
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _unindent_lines(lines: List[str], count: int = 4):
|
|
182
|
+
def unindent_line(line: str):
|
|
183
|
+
if line.startswith(_indent_str(count)):
|
|
184
|
+
# enough initial spaces to unindent
|
|
185
|
+
return line[count:]
|
|
186
|
+
else:
|
|
187
|
+
# keep as is
|
|
188
|
+
return line
|
|
189
|
+
|
|
190
|
+
return list(map(unindent_line, lines))
|
|
191
|
+
|
|
192
|
+
_indented_content_pat = re.compile(r'^ .+')
|
|
193
|
+
|
|
194
|
+
# Tries to parse a definition list item.
|
|
195
|
+
def _try_parse_dl_item(self):
|
|
196
|
+
# term?
|
|
197
|
+
dt_m = self._match_cur_line(r'^(\S.*):$')
|
|
198
|
+
|
|
199
|
+
if not dt_m:
|
|
200
|
+
# no term
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if not self._match_next_line(self._indented_content_pat):
|
|
204
|
+
# no definition
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# skip term line
|
|
208
|
+
self._at += 1
|
|
209
|
+
|
|
210
|
+
# parse definition lines
|
|
211
|
+
def_lines: List[str] = []
|
|
212
|
+
at = self._at
|
|
213
|
+
|
|
214
|
+
while at <= self._last_line_index:
|
|
215
|
+
if self._lines[at] == '':
|
|
216
|
+
# keep empty line
|
|
217
|
+
def_lines.append('')
|
|
218
|
+
at += 1
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
if self._indented_content_pat.match(self._lines[at]):
|
|
222
|
+
# indented content line
|
|
223
|
+
def_lines.append(self._lines[at])
|
|
224
|
+
at += 1
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# end of definition
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
# remove trailing empty definition lines
|
|
231
|
+
_remove_trailing_empty_lines(def_lines)
|
|
232
|
+
|
|
233
|
+
# update current position
|
|
234
|
+
self._at = at
|
|
235
|
+
|
|
236
|
+
# create item from unintended definition lines
|
|
237
|
+
return _DlItem(dt_m.group(1),
|
|
238
|
+
_Parser(self._unindent_lines(def_lines)).elems)
|
|
239
|
+
|
|
240
|
+
# Tries to parse a definition list.
|
|
241
|
+
def _try_parse_dl(self):
|
|
242
|
+
items: List[_DlItem] = []
|
|
243
|
+
|
|
244
|
+
while True:
|
|
245
|
+
self._skip_empty_lines()
|
|
246
|
+
|
|
247
|
+
if self._is_done:
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
item = self._try_parse_dl_item()
|
|
251
|
+
|
|
252
|
+
if item is None:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
items.append(item)
|
|
256
|
+
|
|
257
|
+
if len(items) > 0:
|
|
258
|
+
return _Dl(items)
|
|
259
|
+
|
|
260
|
+
# Tries to parse an unchanged block.
|
|
261
|
+
def _try_parse_as_is(self):
|
|
262
|
+
if not self._match_cur_line(self._indented_content_pat):
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
lines: List[str] = []
|
|
266
|
+
at = self._at
|
|
267
|
+
|
|
268
|
+
while at <= self._last_line_index:
|
|
269
|
+
if self._lines[at] == '':
|
|
270
|
+
# keep empty line
|
|
271
|
+
lines.append('')
|
|
272
|
+
at += 1
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
if re.match(self._indented_content_pat, self._lines[at]):
|
|
276
|
+
# content line
|
|
277
|
+
lines.append(self._lines[at])
|
|
278
|
+
at += 1
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# end of block
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
# remove trailing empty lines
|
|
285
|
+
_remove_trailing_empty_lines(lines)
|
|
286
|
+
|
|
287
|
+
# update current position
|
|
288
|
+
self._at = at
|
|
289
|
+
|
|
290
|
+
# create element from unindented lines
|
|
291
|
+
return _AsIs(self._unindent_lines(lines))
|
|
292
|
+
|
|
293
|
+
_bullet_line_pat = re.compile(r'^(?:\*|•|‣) (.+)')
|
|
294
|
+
|
|
295
|
+
# Tries to parse an unordered list item.
|
|
296
|
+
def _try_parse_ul_item(self):
|
|
297
|
+
# item start?
|
|
298
|
+
bullet_m = self._match_cur_line(self._bullet_line_pat)
|
|
299
|
+
|
|
300
|
+
if not bullet_m:
|
|
301
|
+
# no item
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
# skip first line
|
|
305
|
+
self._at += 1
|
|
306
|
+
|
|
307
|
+
# parse content lines
|
|
308
|
+
lines: List[str] = [bullet_m.group(1)]
|
|
309
|
+
at = self._at
|
|
310
|
+
|
|
311
|
+
while at <= self._last_line_index:
|
|
312
|
+
if self._lines[at] == '':
|
|
313
|
+
# keep empty line
|
|
314
|
+
lines.append('')
|
|
315
|
+
at += 1
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
if re.match(r'^ .+$', self._lines[at]):
|
|
319
|
+
# indented content line
|
|
320
|
+
lines.append(self._lines[at])
|
|
321
|
+
at += 1
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# end of item
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
# remove trailing empty lines
|
|
328
|
+
_remove_trailing_empty_lines(lines)
|
|
329
|
+
|
|
330
|
+
# update current position
|
|
331
|
+
self._at = at
|
|
332
|
+
|
|
333
|
+
# create item from unintended content lines
|
|
334
|
+
return _SimpleListItem(_Parser(self._unindent_lines(lines, 2)).elems)
|
|
335
|
+
|
|
336
|
+
# Tries to parse an unordered list.
|
|
337
|
+
def _try_parse_ul(self):
|
|
338
|
+
items: List[_SimpleListItem] = []
|
|
339
|
+
|
|
340
|
+
while True:
|
|
341
|
+
self._skip_empty_lines()
|
|
342
|
+
|
|
343
|
+
if self._is_done:
|
|
344
|
+
break
|
|
345
|
+
|
|
346
|
+
item = self._try_parse_ul_item()
|
|
347
|
+
|
|
348
|
+
if item is None:
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
items.append(item)
|
|
352
|
+
|
|
353
|
+
if len(items) > 0:
|
|
354
|
+
return _Ul(items)
|
|
355
|
+
|
|
356
|
+
_literal_pat = re.compile(r'`[^`]*`')
|
|
357
|
+
_word_pat = re.compile(r'(.+?)(?=`| |$)')
|
|
358
|
+
|
|
359
|
+
# Converts a paragraph string to a paragraph element containing
|
|
360
|
+
# individual words.
|
|
361
|
+
@staticmethod
|
|
362
|
+
def _para_elem_from_text(text: str):
|
|
363
|
+
elems: List[str] = []
|
|
364
|
+
i = 0
|
|
365
|
+
|
|
366
|
+
# scan each character
|
|
367
|
+
while i < len(text):
|
|
368
|
+
# literal?
|
|
369
|
+
m = _Parser._literal_pat.match(text, i)
|
|
370
|
+
|
|
371
|
+
if m:
|
|
372
|
+
# literal
|
|
373
|
+
elems.append(m.group(0))
|
|
374
|
+
else:
|
|
375
|
+
# word?
|
|
376
|
+
m = _Parser._word_pat.match(text, i)
|
|
377
|
+
|
|
378
|
+
if m:
|
|
379
|
+
# word
|
|
380
|
+
word = m.group(1).strip()
|
|
381
|
+
|
|
382
|
+
if len(word) > 0:
|
|
383
|
+
elems.append(word)
|
|
384
|
+
|
|
385
|
+
if m:
|
|
386
|
+
i += len(m.group(0))
|
|
387
|
+
|
|
388
|
+
return _Para(elems)
|
|
389
|
+
|
|
390
|
+
# Tries to parse a paragraph.
|
|
391
|
+
def _try_parse_para(self):
|
|
392
|
+
lines: List[str] = []
|
|
393
|
+
|
|
394
|
+
while not self._is_done:
|
|
395
|
+
if self._cur_line == '' or self._match_cur_line(self._bullet_line_pat):
|
|
396
|
+
# empty line: end of paragraph
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
lines.append(self._cur_line)
|
|
400
|
+
self._at += 1
|
|
401
|
+
|
|
402
|
+
if len(lines) > 0:
|
|
403
|
+
return self._para_elem_from_text(' '.join(lines))
|
|
404
|
+
|
|
405
|
+
def _parse(self):
|
|
406
|
+
while not self._is_done:
|
|
407
|
+
self._skip_empty_lines()
|
|
408
|
+
|
|
409
|
+
if self._is_done:
|
|
410
|
+
break
|
|
411
|
+
|
|
412
|
+
# level 1 heading?
|
|
413
|
+
elem = self._try_parse_h1()
|
|
414
|
+
|
|
415
|
+
if elem is not None:
|
|
416
|
+
self._add_elem(elem)
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
# level 2 heading?
|
|
420
|
+
elem = self._try_parse_h2()
|
|
421
|
+
|
|
422
|
+
if elem is not None:
|
|
423
|
+
self._add_elem(elem)
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
# unordered list?
|
|
427
|
+
elem = self._try_parse_ul()
|
|
428
|
+
|
|
429
|
+
if elem is not None:
|
|
430
|
+
self._add_elem(elem)
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
# definition list?
|
|
434
|
+
elem = self._try_parse_dl()
|
|
435
|
+
|
|
436
|
+
if elem is not None:
|
|
437
|
+
self._add_elem(elem)
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
# block as is?
|
|
441
|
+
elem = self._try_parse_as_is()
|
|
442
|
+
|
|
443
|
+
if elem is not None:
|
|
444
|
+
self._add_elem(elem)
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
# fall back to paragraph
|
|
448
|
+
elem = self._try_parse_para()
|
|
449
|
+
|
|
450
|
+
if elem is not None:
|
|
451
|
+
self._add_elem(elem)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class _Formatter:
|
|
455
|
+
def __init__(self, elems: List[_Elem], max_width: int):
|
|
456
|
+
self._max_width = max_width
|
|
457
|
+
self._cur_indent = 0
|
|
458
|
+
self._cur_list_level = -1
|
|
459
|
+
self._lines = self._elems_lines(elems)
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def lines(self):
|
|
463
|
+
return self._lines
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def _cur_max_width(self):
|
|
467
|
+
return max([self._max_width - self._cur_indent, 0])
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def _indent_str(self):
|
|
471
|
+
return _indent_str(self._cur_indent)
|
|
472
|
+
|
|
473
|
+
# Returns the lines of the paragraph `para`.
|
|
474
|
+
def _para_lines(self, para: _Para):
|
|
475
|
+
lines: List[str] = [self._indent_str]
|
|
476
|
+
|
|
477
|
+
# append each word, wrapping when necessary
|
|
478
|
+
for word in para.words:
|
|
479
|
+
to_append = f'{word} '
|
|
480
|
+
|
|
481
|
+
if len(lines[-1]) + len(word) >= self._cur_max_width:
|
|
482
|
+
# append to current line
|
|
483
|
+
lines.append(self._indent_str + to_append)
|
|
484
|
+
else:
|
|
485
|
+
# new line
|
|
486
|
+
lines[-1] += to_append
|
|
487
|
+
|
|
488
|
+
# remove trailing empty lines
|
|
489
|
+
_remove_trailing_empty_lines(lines)
|
|
490
|
+
|
|
491
|
+
# append final empty line and return lines
|
|
492
|
+
lines.append('')
|
|
493
|
+
return lines
|
|
494
|
+
|
|
495
|
+
# Returns the lines of the unordered list item `item`.
|
|
496
|
+
def _ul_item_lines(self, item: _SimpleListItem):
|
|
497
|
+
# get indented element lines
|
|
498
|
+
self._cur_indent += 2
|
|
499
|
+
lines: List[str] = []
|
|
500
|
+
|
|
501
|
+
for elem in item.elems:
|
|
502
|
+
lines += self._elem_lines(elem)
|
|
503
|
+
|
|
504
|
+
self._cur_indent -= 2
|
|
505
|
+
|
|
506
|
+
# insert bullet point
|
|
507
|
+
bullet = ['•', '‣', '⁃'][self._cur_list_level % 3]
|
|
508
|
+
lines[0] = f'{self._indent_str}{bullet} {lines[0][self._cur_indent + 2:]}'
|
|
509
|
+
|
|
510
|
+
# remove trailing empty lines
|
|
511
|
+
_remove_trailing_empty_lines(lines)
|
|
512
|
+
|
|
513
|
+
# append final empty line and return lines
|
|
514
|
+
lines.append('')
|
|
515
|
+
return lines
|
|
516
|
+
|
|
517
|
+
# Returns the lines of the unordered list `ul`.
|
|
518
|
+
def _ul_lines(self, ul: _Ul):
|
|
519
|
+
lines: List[str] = []
|
|
520
|
+
self._cur_list_level += 1
|
|
521
|
+
|
|
522
|
+
for item in ul.items:
|
|
523
|
+
lines += self._ul_item_lines(item)
|
|
524
|
+
|
|
525
|
+
self._cur_list_level -= 1
|
|
526
|
+
|
|
527
|
+
# special case to make the list compact if there are only
|
|
528
|
+
# single-line items
|
|
529
|
+
if len(lines) == 2 * len(ul.items):
|
|
530
|
+
lines = list(filter(lambda line: len(line.strip()) > 0, lines))
|
|
531
|
+
|
|
532
|
+
# reappend final empty line
|
|
533
|
+
lines.append('')
|
|
534
|
+
|
|
535
|
+
return lines
|
|
536
|
+
|
|
537
|
+
# Returns the lines of the definition list item `item`.
|
|
538
|
+
def _dl_item_lines(self, item: _DlItem):
|
|
539
|
+
# start with term line
|
|
540
|
+
lines = [f'{item.term}:']
|
|
541
|
+
|
|
542
|
+
# get indented element lines
|
|
543
|
+
self._cur_indent += 4
|
|
544
|
+
|
|
545
|
+
for elem in item.elems:
|
|
546
|
+
lines += self._elem_lines(elem)
|
|
547
|
+
|
|
548
|
+
self._cur_indent -= 4
|
|
549
|
+
|
|
550
|
+
# remove trailing empty lines
|
|
551
|
+
_remove_trailing_empty_lines(lines)
|
|
552
|
+
|
|
553
|
+
# append final empty line and return lines
|
|
554
|
+
lines.append('')
|
|
555
|
+
return lines
|
|
556
|
+
|
|
557
|
+
# Returns the lines of the definition list `dl`.
|
|
558
|
+
def _dl_lines(self, dl: _Dl):
|
|
559
|
+
lines: List[str] = []
|
|
560
|
+
|
|
561
|
+
for item in dl.items:
|
|
562
|
+
lines += self._dl_item_lines(item)
|
|
563
|
+
|
|
564
|
+
return lines
|
|
565
|
+
|
|
566
|
+
# Returns the lines of the unchanged block `as_is`.
|
|
567
|
+
def _as_is_lines(self, as_is: _AsIs):
|
|
568
|
+
return list(map(lambda line: f'{self._indent_str} {line}',
|
|
569
|
+
as_is.lines)) + ['']
|
|
570
|
+
|
|
571
|
+
# Returns the lines of the level 1 heading `h1`.
|
|
572
|
+
def _h1_lines(self, h1: _H1):
|
|
573
|
+
return [
|
|
574
|
+
h1.text.upper(),
|
|
575
|
+
'═' * len(h1.text),
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
# Returns the lines of the level 2 heading `h2`.
|
|
579
|
+
def _h2_lines(self, h2: _H2):
|
|
580
|
+
return [
|
|
581
|
+
h2.text,
|
|
582
|
+
'─' * len(h2.text),
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
# Returns the lines of the element `elem`.
|
|
586
|
+
def _elem_lines(self, elem: _Elem) -> List[str]:
|
|
587
|
+
if type(elem) is _Para:
|
|
588
|
+
return self._para_lines(elem)
|
|
589
|
+
elif type(elem) is _Ul:
|
|
590
|
+
return self._ul_lines(elem)
|
|
591
|
+
elif type(elem) is _Dl:
|
|
592
|
+
return self._dl_lines(elem)
|
|
593
|
+
elif type(elem) is _H1:
|
|
594
|
+
return self._h1_lines(elem)
|
|
595
|
+
elif type(elem) is _H2:
|
|
596
|
+
return self._h2_lines(elem)
|
|
597
|
+
elif type(elem) is _AsIs:
|
|
598
|
+
return self._as_is_lines(elem)
|
|
599
|
+
else:
|
|
600
|
+
return []
|
|
601
|
+
|
|
602
|
+
# Returns the lines of the elements `elems`.
|
|
603
|
+
def _elems_lines(self, elems: List[_Elem]):
|
|
604
|
+
lines: List[str] = []
|
|
605
|
+
|
|
606
|
+
for elem in elems:
|
|
607
|
+
lines += self._elem_lines(elem)
|
|
608
|
+
|
|
609
|
+
# right-strip all lines
|
|
610
|
+
lines = list(map(lambda line: line.rstrip(), lines))
|
|
611
|
+
|
|
612
|
+
# remove trailing empty lines and return them
|
|
613
|
+
_remove_trailing_empty_lines(lines)
|
|
614
|
+
return lines
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# Returns the beautified raw comment text `text` to fit on `max_width`
|
|
618
|
+
# columns.
|
|
619
|
+
#
|
|
620
|
+
# `text` is raw in that it must not contain special block comment
|
|
621
|
+
# characters, raw text.
|
|
622
|
+
#
|
|
623
|
+
# Use format_block_comment() to format a complete C/C++ block comment.
|
|
624
|
+
def format(text: str, max_width: int = 72):
|
|
625
|
+
return '\n'.join(_Formatter(_Parser(text.splitlines()).elems, max_width).lines)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# Returns the beautified version of the C/C++ block comment text `text`
|
|
629
|
+
# to fit on `max_width` columns.
|
|
630
|
+
#
|
|
631
|
+
# The comment text is everything between `/*` and `*/`, where the column
|
|
632
|
+
# of `/*` within its original document is `start_col`.
|
|
633
|
+
#
|
|
634
|
+
# The whole `comment` string must have a format such as this:
|
|
635
|
+
#
|
|
636
|
+
# /*
|
|
637
|
+
# * Hello world.
|
|
638
|
+
# * ===
|
|
639
|
+
# *
|
|
640
|
+
# * * Cupidatat in elit irure.
|
|
641
|
+
# * * Qui sint.
|
|
642
|
+
# *
|
|
643
|
+
# * Sunt tempor cillum ut sint.
|
|
644
|
+
# */
|
|
645
|
+
#
|
|
646
|
+
# The leading ` * ` strings are important.
|
|
647
|
+
def format_block_comment(comment: str, start_col: int = 0, max_width: int = 72):
|
|
648
|
+
# extract content lines from comment string
|
|
649
|
+
comment_lines = comment.splitlines()
|
|
650
|
+
content_lines: List[str] = []
|
|
651
|
+
|
|
652
|
+
for comment_line in comment_lines:
|
|
653
|
+
comment_line = comment_line.strip()
|
|
654
|
+
|
|
655
|
+
if comment_line in ('/*', '*/'):
|
|
656
|
+
continue
|
|
657
|
+
|
|
658
|
+
if comment_line == '*':
|
|
659
|
+
content_lines.append('')
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
m = re.match(r'\s*\* (.+)$', comment_line)
|
|
663
|
+
|
|
664
|
+
if not m:
|
|
665
|
+
raise ValueError(m)
|
|
666
|
+
|
|
667
|
+
content_lines.append(m.group(1))
|
|
668
|
+
|
|
669
|
+
# format contents of comment
|
|
670
|
+
new_content_lines = _Formatter(_Parser(content_lines).elems, max_width).lines
|
|
671
|
+
|
|
672
|
+
# create and return final comment
|
|
673
|
+
new_comment_lines = ['/*']
|
|
674
|
+
indent_str = _indent_str(start_col)
|
|
675
|
+
new_comment_lines += list(map(lambda rline: f'{indent_str} * {rline}',
|
|
676
|
+
new_content_lines))
|
|
677
|
+
new_comment_lines.append(f'{indent_str} */')
|
|
678
|
+
return '\n'.join(new_comment_lines)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2024 Philippe Proulx <pproulx@efficios.com>
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
# a copy of this software and associated documentation files (the
|
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
# the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be
|
|
12
|
+
# included in all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
18
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
19
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
20
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ['poetry-core']
|
|
24
|
+
build-backend = 'poetry.core.masonry.api'
|
|
25
|
+
|
|
26
|
+
[tool.poetry]
|
|
27
|
+
name = 'formol'
|
|
28
|
+
version = '0.1.0'
|
|
29
|
+
description = 'C/C++ block comment beautifier'
|
|
30
|
+
license = 'MIT'
|
|
31
|
+
authors = ['Philippe Proulx <eeppeliteloop@gmail.com>']
|
|
32
|
+
repository = 'https://github.com/eepp/formol/'
|
|
33
|
+
classifiers = [
|
|
34
|
+
'Development Status :: 4 - Beta',
|
|
35
|
+
'License :: OSI Approved :: MIT License',
|
|
36
|
+
'Programming Language :: Python',
|
|
37
|
+
'Programming Language :: Python :: 3',
|
|
38
|
+
'Intended Audience :: Developers',
|
|
39
|
+
'Operating System :: OS Independent',
|
|
40
|
+
'Programming Language :: C',
|
|
41
|
+
'Programming Language :: C++',
|
|
42
|
+
'Topic :: Software Development :: Documentation',
|
|
43
|
+
]
|
|
44
|
+
packages = [{include = 'formol'}]
|
|
45
|
+
|
|
46
|
+
[tool.poetry.dependencies]
|
|
47
|
+
python = '^3.8'
|
|
48
|
+
|
|
49
|
+
[tool.poetry.urls]
|
|
50
|
+
'Bug tracker' = 'https://github.com/eepp/formol/issues/'
|