mdforge 0.1.0__tar.gz → 0.2.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.
Files changed (44) hide show
  1. {mdforge-0.1.0 → mdforge-0.2.1}/LICENSE +0 -0
  2. mdforge-0.2.1/PKG-INFO +619 -0
  3. mdforge-0.2.1/README.md +596 -0
  4. mdforge-0.2.1/mdforge/__init__.py +15 -0
  5. mdforge-0.2.1/mdforge/_norm.py +107 -0
  6. mdforge-0.2.1/mdforge/_utils.py +62 -0
  7. mdforge-0.2.1/mdforge/container.py +217 -0
  8. mdforge-0.2.1/mdforge/document.py +59 -0
  9. mdforge-0.2.1/mdforge/element.py +247 -0
  10. mdforge-0.2.1/mdforge/elements/__init__.py +11 -0
  11. mdforge-0.2.1/mdforge/elements/_image.py +45 -0
  12. mdforge-0.2.1/mdforge/elements/block/__init__.py +14 -0
  13. mdforge-0.2.1/mdforge/elements/block/basic.py +109 -0
  14. mdforge-0.2.1/mdforge/elements/block/lists/__init__.py +11 -0
  15. mdforge-0.2.1/mdforge/elements/block/lists/definition.py +130 -0
  16. mdforge-0.2.1/mdforge/elements/block/lists/item.py +209 -0
  17. mdforge-0.2.1/mdforge/elements/block/section.py +49 -0
  18. mdforge-0.2.1/mdforge/elements/block/table/__init__.py +11 -0
  19. mdforge-0.2.1/mdforge/elements/block/table/_context.py +35 -0
  20. mdforge-0.2.1/mdforge/elements/block/table/_flavors/flavors.py +64 -0
  21. mdforge-0.2.1/mdforge/elements/block/table/_flavors/frame/__init__.py +4 -0
  22. mdforge-0.2.1/mdforge/elements/block/table/_flavors/frame/context.py +808 -0
  23. mdforge-0.2.1/mdforge/elements/block/table/_flavors/frame/variant.py +140 -0
  24. mdforge-0.2.1/mdforge/elements/block/table/_flavors/frame/variants.py +98 -0
  25. mdforge-0.2.1/mdforge/elements/block/table/_flavors/list/__init__.py +3 -0
  26. mdforge-0.2.1/mdforge/elements/block/table/_params.py +215 -0
  27. mdforge-0.2.1/mdforge/elements/block/table/_variant.py +56 -0
  28. mdforge-0.2.1/mdforge/elements/block/table/_vcell.py +177 -0
  29. mdforge-0.2.1/mdforge/elements/block/table/cell.py +151 -0
  30. mdforge-0.2.1/mdforge/elements/block/table/table.py +236 -0
  31. mdforge-0.2.1/mdforge/elements/block/text.py +45 -0
  32. mdforge-0.2.1/mdforge/elements/inline/__init__.py +11 -0
  33. mdforge-0.2.1/mdforge/elements/inline/basic.py +66 -0
  34. mdforge-0.2.1/mdforge/elements/inline/text.py +152 -0
  35. mdforge-0.2.1/mdforge/exceptions.py +21 -0
  36. mdforge-0.2.1/mdforge/types.py +23 -0
  37. mdforge-0.2.1/pyproject.toml +71 -0
  38. mdforge-0.1.0/PKG-INFO +0 -29
  39. mdforge-0.1.0/README.md +0 -8
  40. mdforge-0.1.0/mdforge/__init__.py +0 -13
  41. mdforge-0.1.0/mdforge/_element.py +0 -12
  42. mdforge-0.1.0/mdforge/document.py +0 -57
  43. mdforge-0.1.0/mdforge/elements.py +0 -65
  44. mdforge-0.1.0/pyproject.toml +0 -41
File without changes
mdforge-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,619 @@
1
+ Metadata-Version: 2.3
2
+ Name: mdforge
3
+ Version: 0.2.1
4
+ Summary: Pythonic multi-flavor Markdown generator
5
+ Author: mm21
6
+ Author-email: mm21.dev@gmail.com
7
+ Requires-Python: >=3.12,<4.0
8
+ Classifier: Programming Language :: Python :: Implementation :: CPython
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Typing :: Typed
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Natural Language :: English
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Dist: pyrollup (>=0.1.0,<0.2.0)
19
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
20
+ Project-URL: Homepage, https://github.com/mm21/mdforge
21
+ Description-Content-Type: text/markdown
22
+
23
+ # MDForge
24
+ Pythonic multi-flavor Markdown generator
25
+
26
+ [![Python versions](https://img.shields.io/pypi/pyversions/mdforge.svg)](https://pypi.org/project/mdforge)
27
+ [![PyPI](https://img.shields.io/pypi/v/mdforge?color=%2334D058&label=pypi%20package)](https://pypi.org/project/mdforge)
28
+ [![Tests](./badges/tests.svg?dummy=8484744)]()
29
+ [![Coverage](./badges/cov.svg?dummy=8484744)]()
30
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
31
+
32
+ - [MDForge](#mdforge)
33
+ - [Motivation](#motivation)
34
+ - [Getting started](#getting-started)
35
+ - [Working with documents](#working-with-documents)
36
+ - [Pythonic Markdown elements](#pythonic-markdown-elements)
37
+ - [Formatting](#formatting)
38
+ - [Lists](#lists)
39
+ - [Tables](#tables)
40
+ - [Images](#images)
41
+ - [Rendering standalone elements](#rendering-standalone-elements)
42
+ - [Advanced features](#advanced-features)
43
+ - [HTML attributes](#html-attributes)
44
+ - [Pandoc extensions](#pandoc-extensions)
45
+
46
+ ## Motivation
47
+
48
+ MDForge provides a Pythonic interface for generating Markdown documents programmatically. It allows you to construct a document tree and render it for various Markdown flavors, making it ideal for applications such as documentation generators and report builders. It emphasizes clean flavor-agnostic elements and type safety.
49
+
50
+ While MDForge supports the rich capabilities of [Pandoc's Markdown](https://pandoc.org/MANUAL.html#pandocs-markdown), it is designed to support generating documents with multiple flavors from the same document model. Other flavors currently planned are:
51
+
52
+ - [GitHub Flavored Markdown](https://github.github.com/gfm/)
53
+ - [MyST Markdown](https://mystmd.org/guide)
54
+
55
+ ## Getting started
56
+
57
+ First, install using pip:
58
+
59
+ ```bash
60
+ pip install mdforge
61
+ ```
62
+
63
+ Create your first Markdown document:
64
+
65
+ ```python
66
+ from mdforge import Document, Heading, Paragraph, Strong
67
+ from pathlib import Path
68
+
69
+ # create a document
70
+ doc = Document()
71
+
72
+ # add elements
73
+ doc += [
74
+ Heading("My first MDForge document"),
75
+ Paragraph("Hello, ", Strong("world"), "!"),
76
+ ]
77
+
78
+ # render pandoc-flavored Markdown document to a file
79
+ doc.render_file("doc.md", flavor="pandoc")
80
+ ```
81
+
82
+ This is rendered as:
83
+
84
+ ```markdown
85
+ # My first MDForge document
86
+
87
+ Hello, **world**!
88
+
89
+ ```
90
+
91
+ ## Working with documents
92
+
93
+ The `Document` class is the fundamental interface to create a Markdown document. It implements a [Composite pattern](https://en.wikipedia.org/wiki/Composite_pattern) wherein elements are composed using the `+=` operator:
94
+
95
+ ```python
96
+ from mdforge import BulletList, Document, Section
97
+
98
+ # create a document with frontmatter
99
+ doc = Document(frontmatter={"title": "My Document", "author": "Me"})
100
+
101
+ # create sections, automatically adding headings of the appropriate level
102
+ intro = Section("Introduction")
103
+ intro += "This is an introduction paragraph."
104
+
105
+ key_points = Section("Key Points")
106
+ key_points += BulletList(["Point 1", "Point 2", "Point 3"])
107
+
108
+ # add subsections for each point
109
+ key_points += [
110
+ Section(
111
+ "Point 1",
112
+ elements=[
113
+ "Elaboration on point 1:",
114
+ BulletList(["Point 1-a", "Point 1-b"]),
115
+ ],
116
+ ),
117
+ Section("Point 2", elements=["Elaboration on point 2."]),
118
+ Section("Point 3", elements=["Elaboration on point 3."]),
119
+ ]
120
+
121
+ # add sections to document
122
+ doc += [intro, key_points]
123
+
124
+ doc.render_file("doc.md", flavor="pandoc")
125
+ ```
126
+
127
+ This is rendered as:
128
+
129
+ ```markdown
130
+ ---
131
+ title: My Document
132
+ author: Me
133
+ ---
134
+
135
+ # Introduction
136
+
137
+ This is an introduction paragraph.
138
+
139
+ # Key Points
140
+
141
+ - Point 1
142
+ - Point 2
143
+ - Point 3
144
+ <!-- end of list -->
145
+
146
+ ## Point 1
147
+
148
+ Elaboration on point 1:
149
+
150
+ - Point 1-a
151
+ - Point 1-b
152
+ <!-- end of list -->
153
+
154
+ ## Point 2
155
+
156
+ Elaboration on point 2.
157
+
158
+ ## Point 3
159
+
160
+ Elaboration on point 3.
161
+
162
+ ```
163
+
164
+ Note the following observations:
165
+
166
+ - Sections can be nested in other sections, with optional headings
167
+ - Heading levels for sections are automatically set by their placement in the hierarchy
168
+ - Strings added to a document or section via `+=` are treated as raw text
169
+ - A comment is placed at the end of each list; this disambiguates the end of one list and the start of the next, in case there are no other elements in between
170
+
171
+ ## Pythonic Markdown elements
172
+
173
+ MDForge provides a set of Markdown elements that can be composed to create rich documents. Not all common elements are implemented yet, e.g. code blocks. Nonetheless, you can insert raw text (which may be pre-formatted as Markdown) using `+=`.
174
+
175
+ We have already seen basic elements and container elements. The following examples illustrate the use of other common elements.
176
+
177
+ ### Formatting
178
+
179
+ ```python
180
+ from mdforge import (
181
+ Document,
182
+ Emph,
183
+ Paragraph,
184
+ Strikethrough,
185
+ Strong,
186
+ Underline,
187
+ )
188
+
189
+ doc = Document()
190
+
191
+ # basic formatting
192
+ doc += Strong("Bold text")
193
+ doc += Emph("Italicized text")
194
+ doc += Strikethrough("Strikethrough text")
195
+ doc += Underline("Underlined text")
196
+
197
+ # combined formatting
198
+ doc += Strong(Emph("Bold and italicized text"))
199
+
200
+ # mixed formatting in a paragraph, automatically putting spaces between elements
201
+ doc += Paragraph(
202
+ "Normal text with",
203
+ Strong("bold"),
204
+ "and",
205
+ Emph("italic"),
206
+ "segments.",
207
+ auto_space=True,
208
+ )
209
+
210
+ doc.render_file("doc.md", flavor="pandoc")
211
+ ```
212
+
213
+ This is rendered as:
214
+
215
+ ```markdown
216
+ **Bold text**
217
+
218
+ _Italicized text_
219
+
220
+ ~~Strikethrough text~~
221
+
222
+ [Underlined text]{.underline}
223
+
224
+ **_Bold and italicized text_**
225
+
226
+ Normal text with **bold** and _italic_ segments.
227
+
228
+ ```
229
+
230
+ Note the following observations:
231
+
232
+ - Here we pass `auto_space=True` in `Paragraph()`; this will automatically insert spaces between the inline elements passed to it
233
+ - As underline is not supported in CommonMark, pandoc implements its own [underline syntax](https://pandoc.org/MANUAL.html#underline)
234
+
235
+ ### Lists
236
+
237
+ MDForge supports bullet lists, numbered lists, and definition lists:
238
+
239
+ ```python
240
+ from mdforge import (
241
+ BulletList,
242
+ DefinitionItem,
243
+ DefinitionList,
244
+ Document,
245
+ ListItem,
246
+ NumberedList,
247
+ )
248
+
249
+ doc = Document()
250
+
251
+ # bullet list
252
+ doc += BulletList(["Item 1", "Item 2", "Item 3"])
253
+
254
+ # numbered list
255
+ doc += NumberedList(["First", "Second", "Third"])
256
+
257
+ # definition list
258
+ doc += DefinitionList(
259
+ [
260
+ DefinitionItem("Term A", "Definition A"),
261
+ DefinitionItem("Term B", ["Definition B1", "Definition B2"]),
262
+ ],
263
+ compact=True,
264
+ )
265
+
266
+ # nested lists
267
+ doc += BulletList(
268
+ [
269
+ "Item 1",
270
+ ListItem(
271
+ "Item 2",
272
+ [
273
+ "Item 2-1",
274
+ "Item 2-2",
275
+ ],
276
+ ),
277
+ ]
278
+ )
279
+
280
+ # mixed nested lists
281
+ doc += BulletList(
282
+ [
283
+ "Item 1",
284
+ ListItem(
285
+ "Item 2",
286
+ NumberedList(
287
+ [
288
+ "Item 2-1",
289
+ "Item 2-2",
290
+ ]
291
+ ),
292
+ ),
293
+ ]
294
+ )
295
+
296
+ doc.render_file("doc.md", flavor="pandoc")
297
+ ```
298
+
299
+ This is rendered as:
300
+
301
+ ```markdown
302
+ - Item 1
303
+ - Item 2
304
+ - Item 3
305
+ <!-- end of list -->
306
+
307
+ 1. First
308
+ 1. Second
309
+ 1. Third
310
+ <!-- end of list -->
311
+
312
+ Term A
313
+ : Definition A
314
+
315
+ Term B
316
+ : Definition B1
317
+ : Definition B2
318
+
319
+ <!-- end of definition list -->
320
+
321
+ - Item 1
322
+ - Item 2
323
+ - Item 2-1
324
+ - Item 2-2
325
+ <!-- end of list -->
326
+ <!-- end of list -->
327
+
328
+ - Item 1
329
+ - Item 2
330
+ 1. Item 2-1
331
+ 1. Item 2-2
332
+ <!-- end of list -->
333
+ <!-- end of list -->
334
+
335
+ ```
336
+
337
+ Note the following observations:
338
+
339
+ - Here we pass `compact=True` in `DefinitionList()`; this will generate [compact definition lists](https://pandoc.org/MANUAL.html#extension-compact_definition_lists) for pandoc flavor
340
+ - As mentioned above, a comment disambiguates the end of one list and the start of the next
341
+
342
+ ### Tables
343
+
344
+ MDForge provides powerful table support with cell spanning, alignment, and formatting:
345
+
346
+ ```python
347
+ from mdforge import BulletList, Cell, Document, Table
348
+
349
+ doc = Document()
350
+
351
+ # simple table
352
+ doc += Table(
353
+ [
354
+ ["Cell 1-1", "Cell 1-2"],
355
+ ["Cell 2-1", "Cell 2-2"],
356
+ ]
357
+ )
358
+
359
+ # table with alignment
360
+ doc += Table(
361
+ [["Cell 1", "Cell 2", "Cell 3"]],
362
+ align=["left", "center", "right"],
363
+ )
364
+
365
+ # table with header and footer (needs block=True)
366
+ doc += Table(
367
+ [["Cell 1", "Cell 2"]],
368
+ header=["Header 1", "Header 2"],
369
+ footer=["Footer 1", "Footer 2"],
370
+ block=True,
371
+ )
372
+
373
+ # table with row and column spanning (needs block=True)
374
+ doc += Table(
375
+ [
376
+ ["Cell 1-1", "Cell 1-2", "Cell 1-3"],
377
+ ["Cell 2-1", "Cell 2-2", "Cell 2-3"],
378
+ ],
379
+ header=[
380
+ [Cell("Column 1", rspan=2), Cell("Columns 2 & 3", cspan=2)],
381
+ ["Column 2", "Column 3"],
382
+ ],
383
+ block=True,
384
+ )
385
+
386
+ # table with each cell wrapped in an explicit paragraph if it doesn't
387
+ # already contain block content (needs block=True)
388
+ doc += Table(
389
+ [
390
+ [
391
+ BlockContainer(
392
+ "This text is implicitly wrapped in a paragraph",
393
+ BulletList(["Item 1", "Item 2"]),
394
+ ),
395
+ "Cell 2",
396
+ "Cell 3",
397
+ ]
398
+ ],
399
+ align=["left", "center", "right"],
400
+ block=True,
401
+ loose=True,
402
+ )
403
+
404
+ # table with character widths
405
+ doc += Table(
406
+ [["Short text", "This is longer text that will be wrapped"]],
407
+ widths=[15, 20],
408
+ )
409
+
410
+ # table with percentage widths
411
+ doc += Table(
412
+ [["25% width", "75% width"]],
413
+ widths_pct=[25, 75],
414
+ )
415
+
416
+ doc.render_file("doc.md", flavor="pandoc")
417
+ ```
418
+
419
+ This is rendered as:
420
+
421
+ ```markdown
422
+ <!-- table start -->
423
+
424
+ ---------- ----------
425
+ Cell 1-1 Cell 1-2
426
+
427
+ Cell 2-1 Cell 2-2
428
+ ---------------------
429
+
430
+ <!-- table end -->
431
+
432
+ <!-- table start: align=['left', 'center', 'right'] -->
433
+
434
+ -------- -------- --------
435
+ Cell 1 Cell 2 Cell 3
436
+
437
+ --------------------------
438
+
439
+ <!-- table end -->
440
+
441
+ <!-- table start: block=True -->
442
+
443
+ +----------+----------+
444
+ | Header 1 | Header 2 |
445
+ +==========+==========+
446
+ | Cell 1 | Cell 2 |
447
+ +==========+==========+
448
+ | Footer 1 | Footer 2 |
449
+ +==========+==========+
450
+
451
+ <!-- table end -->
452
+
453
+ <!-- table start: block=True -->
454
+
455
+ +----------+---------------------+
456
+ | Column 1 | Columns 2 & 3 |
457
+ | +----------+----------+
458
+ | | Column 2 | Column 3 |
459
+ +==========+==========+==========+
460
+ | Cell 1-1 | Cell 1-2 | Cell 1-3 |
461
+ +----------+----------+----------+
462
+ | Cell 2-1 | Cell 2-2 | Cell 2-3 |
463
+ +----------+----------+----------+
464
+
465
+ <!-- table end -->
466
+
467
+ <!-- table start: align=['left', 'center', 'right'], block=True, loose=True -->
468
+
469
+ +:-----------------------------------------------+:-------------:+--------------:+
470
+ | This text is implicitly wrapped in a paragraph | <p>Cell 2</p> | <p>Cell 3</p> |
471
+ | | | |
472
+ | - Item 1 | | |
473
+ | - Item 2 | | |
474
+ | <!-- end of list --> | | |
475
+ +------------------------------------------------+---------------+---------------+
476
+
477
+ <!-- table end -->
478
+
479
+ <!-- table start: widths=[15, 20] -->
480
+
481
+ ----------------- ----------------------
482
+ Short text This is longer text
483
+ that will be wrapped
484
+
485
+ ----------------------------------------
486
+
487
+ <!-- table end -->
488
+
489
+ <!-- table start: widths_pct=[25, 75] -->
490
+
491
+ ----------- -----------------------------
492
+ 25% width 75% width
493
+
494
+ -----------------------------------------
495
+
496
+ <!-- table end -->
497
+
498
+ ```
499
+
500
+ Note the following observations:
501
+
502
+ - Merged cells and footers are only supported with `block=True`; this causes pandoc flavor to use a [grid table](https://pandoc.org/MANUAL.html#extension-grid_tables) instead of a [multiline table](https://pandoc.org/MANUAL.html#extension-multiline_tables)
503
+ - Pass `loose=True` to ensure all cells are wrapped in a paragraph, even if they don't get parsed as block content (requires `block=True`)
504
+ - This ensures consistently-padded cells
505
+ - Use `BlockContainer` to wrap multiple block elements in a single element
506
+
507
+ ### Images
508
+
509
+ MDForge supports both inline and block images:
510
+
511
+ ```python
512
+ from mdforge import BlockImage, Document, InlineImage, Paragraph
513
+
514
+ doc = Document()
515
+
516
+ # inline image in a paragraph
517
+ doc += Paragraph(
518
+ "Here is an inline image",
519
+ InlineImage("./image.png", alt_text="Inline image"),
520
+ "in a paragraph.",
521
+ auto_space=True,
522
+ )
523
+
524
+ # block image with caption and alignment
525
+ doc += BlockImage("./image.png", caption="Block image", align="center")
526
+
527
+ doc.render_file("doc.md", flavor="pandoc")
528
+ ```
529
+
530
+ This is rendered as:
531
+
532
+ ```markdown
533
+ Here is an inline image ![Inline image](./image.png) in a paragraph.
534
+
535
+ ![Block image](./image.png){fig-align="center"}
536
+
537
+ ```
538
+
539
+ Note the following observations:
540
+
541
+ - Support for caption and alignment depends on flavor
542
+
543
+ ### Rendering standalone elements
544
+
545
+ Any element class can be rendered by itself, even if not placed in a document. This is useful for generating document snippets which may be embedded in a document, e.g. via a templating engine.
546
+
547
+ ```python
548
+ from mdforge import InlineContainer, Strong
549
+
550
+ element = InlineContainer("This is a ", Strong("test element"))
551
+
552
+ assert element.render(flavor="pandoc") == "This is a **test element**"
553
+ ```
554
+
555
+ ## Advanced features
556
+
557
+ ### HTML attributes
558
+
559
+ For pandoc flavor, HTML attributes can be added to various elements:
560
+
561
+ ```python
562
+ from mdforge import Attributes, Document, Heading, Ref, Span
563
+
564
+ doc = Document()
565
+
566
+ # heading with ID, classes, and custom attributes
567
+ my_heading = Heading(
568
+ "Heading with attributes",
569
+ attributes=Attributes(
570
+ html_id="my-heading",
571
+ css_classes=["class1", "class2"],
572
+ attrs={"style": "color: blue;"},
573
+ ),
574
+ )
575
+ doc += my_heading
576
+
577
+ # span with attributes
578
+ doc += Span(
579
+ "Text with attributes",
580
+ attributes=Attributes(html_id="my-span", css_classes="class1"),
581
+ )
582
+
583
+ # reference to heading by id
584
+ doc += Ref(my_heading, "See previous heading")
585
+
586
+ doc.render_file("doc.md", flavor="pandoc")
587
+ ```
588
+
589
+ This is rendered as:
590
+
591
+ ```markdown
592
+ # Heading with attributes {#my-heading .class1 .class2 style="color: blue;"}
593
+
594
+ [Text with attributes]{#my-span .class1}
595
+
596
+ [See previous heading](#my-heading)
597
+
598
+ ```
599
+
600
+ ### Pandoc extensions
601
+
602
+ As MDForge is designed with pandoc compatibility in mind, it automatically tracks required pandoc extensions:
603
+
604
+ ```python
605
+ from mdforge import Attributes, Document, Heading, Strikethrough
606
+
607
+ doc = Document()
608
+
609
+ # requires "header_attributes"
610
+ doc += Heading("Heading 1", attributes=Attributes(html_id="heading-1"))
611
+
612
+ # requires "strikeout"
613
+ doc += Strikethrough("This text is struck through")
614
+
615
+ # get required pandoc extensions
616
+ extensions = doc.get_pandoc_extensions()
617
+ assert extensions == ["header_attributes", "strikeout"]
618
+ ```
619
+