|
3 | 3 | """
|
4 | 4 | from __future__ import annotations
|
5 | 5 |
|
| 6 | +import itertools |
6 | 7 | import typing
|
7 |
| -from typing import Optional, TypeAlias, Sequence |
| 8 | +from typing import Literal, Optional, TypeAlias, Sequence |
8 | 9 | import collections.abc as abc
|
9 | 10 |
|
10 | 11 | from textwrap import indent
|
|
16 | 17 | __all__ = (
|
17 | 18 | "Block",
|
18 | 19 | "Blocks",
|
| 20 | + "BulletList", |
19 | 21 | "CodeBlock",
|
20 | 22 | "DefinitionList",
|
21 | 23 | "Div",
|
22 | 24 | "Header",
|
| 25 | + "OrderedList", |
23 | 26 | "Para",
|
24 | 27 | "Plain",
|
25 | 28 | )
|
@@ -208,6 +211,38 @@ def __str__(self):
|
208 | 211 | return CodeBlock_TPL.format(content=content, attr=attr)
|
209 | 212 |
|
210 | 213 |
|
| 214 | +@dataclass |
| 215 | +class BulletList(Block): |
| 216 | + """ |
| 217 | + A bullet list |
| 218 | + """ |
| 219 | + content: Optional[BlockContent] = None |
| 220 | + |
| 221 | + def __str__(self): |
| 222 | + """ |
| 223 | + Return a bullet list as markdown |
| 224 | + """ |
| 225 | + if not self.content: |
| 226 | + return "" |
| 227 | + return blockcontent_to_str_items(self.content, "bullet") |
| 228 | + |
| 229 | + |
| 230 | +@dataclass |
| 231 | +class OrderedList(Block): |
| 232 | + """ |
| 233 | + An Ordered list |
| 234 | + """ |
| 235 | + content: Optional[BlockContent] = None |
| 236 | + |
| 237 | + def __str__(self): |
| 238 | + """ |
| 239 | + Return an ordered list as markdown |
| 240 | + """ |
| 241 | + if not self.content: |
| 242 | + return "" |
| 243 | + return blockcontent_to_str_items(self.content, "ordered") |
| 244 | + |
| 245 | + |
211 | 246 | # Helper functions
|
212 | 247 |
|
213 | 248 | def join_block_content(content: Sequence[BlockContent]) -> str:
|
@@ -236,3 +271,60 @@ def blockcontent_to_str(content: Optional[BlockContent]) -> str:
|
236 | 271 | else:
|
237 | 272 | raise TypeError(f"Could not process type: {type(content)}")
|
238 | 273 |
|
| 274 | + |
| 275 | +def blockcontent_to_str_items( |
| 276 | + content: Optional[BlockContent], |
| 277 | + kind: Literal["bullet", "ordered"] |
| 278 | +) -> str: |
| 279 | + """ |
| 280 | + Convert block content to strings of items |
| 281 | +
|
| 282 | + Parameters |
| 283 | + ---------- |
| 284 | + content: |
| 285 | + What to convert |
| 286 | +
|
| 287 | + kind: |
| 288 | + How to mark (prefix) each item in the of content. |
| 289 | + """ |
| 290 | + |
| 291 | + def fmt(s:str, pfx: str): |
| 292 | + """ |
| 293 | + Format as a list item with one or more blocks |
| 294 | + """ |
| 295 | + # Aligns the content in all lines to start in the same column. |
| 296 | + # e.g. If pfx = "12.", we get output like |
| 297 | + # |
| 298 | + # 12. abcd |
| 299 | + # efgh |
| 300 | + # |
| 301 | + # ijkl |
| 302 | + # mnop |
| 303 | + if not s: |
| 304 | + return "" |
| 305 | + pad = " " * (len(pfx) + 1) |
| 306 | + return f"{pfx} " + indent(s, pad).lstrip(pad) |
| 307 | + |
| 308 | + if not content: |
| 309 | + return "" |
| 310 | + |
| 311 | + if kind == "bullet": |
| 312 | + pfx_it = itertools.cycle("*") |
| 313 | + else: |
| 314 | + pfx_it = (f"{i}." for i in itertools.count(1)) |
| 315 | + |
| 316 | + if isinstance(content, (str, Inline, Block)): |
| 317 | + return fmt(str(content), next(pfx_it)) |
| 318 | + elif isinstance(content, abc.Sequence): |
| 319 | + # To balance correctness, compactness and readability, |
| 320 | + # items with content get an empty line between them and |
| 321 | + # the next item. |
| 322 | + items = [] |
| 323 | + pad = "" |
| 324 | + for item in content: |
| 325 | + s = fmt(str(item), next(pfx_it)) |
| 326 | + pad = f"{SEP}{SEP}" if isinstance(item, Block) else f"{SEP}" |
| 327 | + items.append(f"{s}{pad}") |
| 328 | + return "".join(items)[:-len(pad)] |
| 329 | + else: |
| 330 | + raise TypeError(f"Could not process type: {type(content)}") |
0 commit comments