mdforge 0.2.3__tar.gz → 0.3.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.
- {mdforge-0.2.3 → mdforge-0.3.0}/PKG-INFO +47 -22
- {mdforge-0.2.3 → mdforge-0.3.0}/README.md +46 -21
- mdforge-0.3.0/pyproject.toml +109 -0
- mdforge-0.2.3/pyproject.toml → mdforge-0.3.0/pyproject.toml.orig +4 -4
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/__init__.py +8 -2
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/_utils.py +36 -3
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/container.py +85 -55
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/document.py +7 -11
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/element.py +112 -32
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/__init__.py +6 -2
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/_image.py +5 -6
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/__init__.py +9 -2
- mdforge-0.3.0/src/mdforge/elements/block/basic.py +74 -0
- mdforge-0.3.0/src/mdforge/elements/block/container.py +68 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/lists/definition.py +23 -10
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/lists/item.py +73 -70
- mdforge-0.3.0/src/mdforge/elements/block/section.py +122 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_context.py +8 -4
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_flavors/flavors.py +5 -3
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_flavors/frame/context.py +6 -4
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/cell.py +9 -7
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/table.py +12 -6
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/text.py +4 -5
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/inline/basic.py +8 -9
- mdforge-0.3.0/src/mdforge/elements/inline/text.py +198 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/exceptions.py +8 -0
- mdforge-0.2.3/src/mdforge/elements/block/basic.py +0 -105
- mdforge-0.2.3/src/mdforge/elements/block/section.py +0 -47
- mdforge-0.2.3/src/mdforge/elements/inline/text.py +0 -149
- {mdforge-0.2.3 → mdforge-0.3.0}/LICENSE +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/_norm.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/lists/__init__.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/__init__.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_flavors/frame/__init__.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_flavors/frame/variant.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_flavors/frame/variants.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_flavors/list/__init__.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_params.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_variant.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/block/table/_vcell.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/elements/inline/__init__.py +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/py.typed +0 -0
- {mdforge-0.2.3 → mdforge-0.3.0}/src/mdforge/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pythonic multi-flavor Markdown generator
|
|
5
5
|
Author: mm21
|
|
6
6
|
Author-email: mm21 <mm21.dev@gmail.com>
|
|
@@ -64,17 +64,17 @@ pip install mdforge
|
|
|
64
64
|
Create your first Markdown document:
|
|
65
65
|
|
|
66
66
|
```python
|
|
67
|
-
from mdforge import Document,
|
|
67
|
+
from mdforge import Document, Paragraph, Section, Strong
|
|
68
68
|
from pathlib import Path
|
|
69
69
|
|
|
70
70
|
# create a document
|
|
71
71
|
doc = Document()
|
|
72
72
|
|
|
73
73
|
# add elements
|
|
74
|
-
doc +=
|
|
75
|
-
|
|
76
|
-
Paragraph("Hello, ", Strong("world"), "!"),
|
|
77
|
-
|
|
74
|
+
doc += Section(
|
|
75
|
+
"My first MDForge document",
|
|
76
|
+
elements=Paragraph("Hello, ", Strong("world"), "!"),
|
|
77
|
+
)
|
|
78
78
|
|
|
79
79
|
# render pandoc-flavored Markdown document to a file
|
|
80
80
|
doc.render_file("doc.md", flavor="pandoc")
|
|
@@ -164,11 +164,33 @@ Elaboration on point 3.
|
|
|
164
164
|
|
|
165
165
|
Note the following observations:
|
|
166
166
|
|
|
167
|
-
-
|
|
168
|
-
-
|
|
167
|
+
- Headings are created from section titles; content belonging to a heading is placed in its section
|
|
168
|
+
- Sections can be nested in other sections, with optional titles
|
|
169
|
+
- Heading levels for sections are automatically set by their placement in the hierarchy; see [Heading levels](#heading-levels)
|
|
169
170
|
- Strings added to a document or section via `+=` are treated as raw text
|
|
170
171
|
- 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
|
|
171
172
|
|
|
173
|
+
### Heading levels
|
|
174
|
+
|
|
175
|
+
By default (`level=None`), the level of a section is relative to the render root: the element on which `render()` is called. The render root is level 1 and each nested section is one level deeper.
|
|
176
|
+
|
|
177
|
+
As a result, a section rendered by itself is a self-contained tree starting at level 1, even if it's also placed in a document.
|
|
178
|
+
|
|
179
|
+
To generate a fragment to be placed in a larger document, pass an explicit `level`. Explicit levels are invariant of placement, and nested sections are relative to it:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from mdforge import Section
|
|
183
|
+
|
|
184
|
+
fragment = Section("Fragment", level=3, elements=Section("Subsection"))
|
|
185
|
+
|
|
186
|
+
assert fragment.render(flavor="pandoc") == "### Fragment\n\n#### Subsection"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
When an element with an explicit level is rendered as part of a larger tree, its level is checked against its placement:
|
|
190
|
+
|
|
191
|
+
- A level not nested in the containing level, e.g. `level=1` in a level-1 section, raises `RenderError`
|
|
192
|
+
- A level which skips levels, e.g. `level=3` in a level-1 section, issues `RenderWarning`
|
|
193
|
+
|
|
172
194
|
## Pythonic Markdown elements
|
|
173
195
|
|
|
174
196
|
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 `+=`.
|
|
@@ -557,32 +579,33 @@ assert element.render(flavor="pandoc") == "This is a **test element**"
|
|
|
557
579
|
|
|
558
580
|
### HTML attributes
|
|
559
581
|
|
|
560
|
-
For pandoc flavor, HTML attributes can be added to various elements:
|
|
582
|
+
For pandoc flavor, HTML attributes can be added to various elements, including section headings:
|
|
561
583
|
|
|
562
584
|
```python
|
|
563
|
-
from mdforge import Attributes, Document,
|
|
585
|
+
from mdforge import Attributes, Document, Ref, Section, Span
|
|
564
586
|
|
|
565
587
|
doc = Document()
|
|
566
588
|
|
|
567
|
-
#
|
|
568
|
-
|
|
569
|
-
"
|
|
589
|
+
# section with heading ID, classes, and custom attributes
|
|
590
|
+
my_section = Section(
|
|
591
|
+
"Section with attributes",
|
|
570
592
|
attributes=Attributes(
|
|
571
|
-
html_id="my-
|
|
593
|
+
html_id="my-section",
|
|
572
594
|
css_classes=["class1", "class2"],
|
|
573
595
|
attrs={"style": "color: blue;"},
|
|
574
596
|
),
|
|
575
597
|
)
|
|
576
|
-
doc +=
|
|
598
|
+
doc += my_section
|
|
577
599
|
|
|
578
600
|
# span with attributes
|
|
579
|
-
|
|
601
|
+
my_section += Span(
|
|
580
602
|
"Text with attributes",
|
|
581
603
|
attributes=Attributes(html_id="my-span", css_classes="class1"),
|
|
582
604
|
)
|
|
583
605
|
|
|
584
|
-
#
|
|
585
|
-
|
|
606
|
+
# references to section, by object and by id
|
|
607
|
+
my_section += Ref(my_section, "See this section")
|
|
608
|
+
my_section += Ref("my-section")
|
|
586
609
|
|
|
587
610
|
doc.render_file("doc.md", flavor="pandoc")
|
|
588
611
|
```
|
|
@@ -590,11 +613,13 @@ doc.render_file("doc.md", flavor="pandoc")
|
|
|
590
613
|
This is rendered as:
|
|
591
614
|
|
|
592
615
|
```markdown
|
|
593
|
-
#
|
|
616
|
+
# Section with attributes {#my-section .class1 .class2 style="color: blue;"}
|
|
594
617
|
|
|
595
618
|
[Text with attributes]{#my-span .class1}
|
|
596
619
|
|
|
597
|
-
[See
|
|
620
|
+
[See this section](#my-section)
|
|
621
|
+
|
|
622
|
+
[Section with attributes](#my-section)
|
|
598
623
|
|
|
599
624
|
```
|
|
600
625
|
|
|
@@ -603,12 +628,12 @@ This is rendered as:
|
|
|
603
628
|
As MDForge is designed with pandoc compatibility in mind, it automatically tracks required pandoc extensions:
|
|
604
629
|
|
|
605
630
|
```python
|
|
606
|
-
from mdforge import Attributes, Document,
|
|
631
|
+
from mdforge import Attributes, Document, Section, Strikethrough
|
|
607
632
|
|
|
608
633
|
doc = Document()
|
|
609
634
|
|
|
610
635
|
# requires "header_attributes"
|
|
611
|
-
doc +=
|
|
636
|
+
doc += Section("Section 1", attributes=Attributes(html_id="section-1"))
|
|
612
637
|
|
|
613
638
|
# requires "strikeout"
|
|
614
639
|
doc += Strikethrough("This text is struck through")
|
|
@@ -41,17 +41,17 @@ pip install mdforge
|
|
|
41
41
|
Create your first Markdown document:
|
|
42
42
|
|
|
43
43
|
```python
|
|
44
|
-
from mdforge import Document,
|
|
44
|
+
from mdforge import Document, Paragraph, Section, Strong
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
47
|
# create a document
|
|
48
48
|
doc = Document()
|
|
49
49
|
|
|
50
50
|
# add elements
|
|
51
|
-
doc +=
|
|
52
|
-
|
|
53
|
-
Paragraph("Hello, ", Strong("world"), "!"),
|
|
54
|
-
|
|
51
|
+
doc += Section(
|
|
52
|
+
"My first MDForge document",
|
|
53
|
+
elements=Paragraph("Hello, ", Strong("world"), "!"),
|
|
54
|
+
)
|
|
55
55
|
|
|
56
56
|
# render pandoc-flavored Markdown document to a file
|
|
57
57
|
doc.render_file("doc.md", flavor="pandoc")
|
|
@@ -141,11 +141,33 @@ Elaboration on point 3.
|
|
|
141
141
|
|
|
142
142
|
Note the following observations:
|
|
143
143
|
|
|
144
|
-
-
|
|
145
|
-
-
|
|
144
|
+
- Headings are created from section titles; content belonging to a heading is placed in its section
|
|
145
|
+
- Sections can be nested in other sections, with optional titles
|
|
146
|
+
- Heading levels for sections are automatically set by their placement in the hierarchy; see [Heading levels](#heading-levels)
|
|
146
147
|
- Strings added to a document or section via `+=` are treated as raw text
|
|
147
148
|
- 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
|
|
148
149
|
|
|
150
|
+
### Heading levels
|
|
151
|
+
|
|
152
|
+
By default (`level=None`), the level of a section is relative to the render root: the element on which `render()` is called. The render root is level 1 and each nested section is one level deeper.
|
|
153
|
+
|
|
154
|
+
As a result, a section rendered by itself is a self-contained tree starting at level 1, even if it's also placed in a document.
|
|
155
|
+
|
|
156
|
+
To generate a fragment to be placed in a larger document, pass an explicit `level`. Explicit levels are invariant of placement, and nested sections are relative to it:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from mdforge import Section
|
|
160
|
+
|
|
161
|
+
fragment = Section("Fragment", level=3, elements=Section("Subsection"))
|
|
162
|
+
|
|
163
|
+
assert fragment.render(flavor="pandoc") == "### Fragment\n\n#### Subsection"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When an element with an explicit level is rendered as part of a larger tree, its level is checked against its placement:
|
|
167
|
+
|
|
168
|
+
- A level not nested in the containing level, e.g. `level=1` in a level-1 section, raises `RenderError`
|
|
169
|
+
- A level which skips levels, e.g. `level=3` in a level-1 section, issues `RenderWarning`
|
|
170
|
+
|
|
149
171
|
## Pythonic Markdown elements
|
|
150
172
|
|
|
151
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 `+=`.
|
|
@@ -534,32 +556,33 @@ assert element.render(flavor="pandoc") == "This is a **test element**"
|
|
|
534
556
|
|
|
535
557
|
### HTML attributes
|
|
536
558
|
|
|
537
|
-
For pandoc flavor, HTML attributes can be added to various elements:
|
|
559
|
+
For pandoc flavor, HTML attributes can be added to various elements, including section headings:
|
|
538
560
|
|
|
539
561
|
```python
|
|
540
|
-
from mdforge import Attributes, Document,
|
|
562
|
+
from mdforge import Attributes, Document, Ref, Section, Span
|
|
541
563
|
|
|
542
564
|
doc = Document()
|
|
543
565
|
|
|
544
|
-
#
|
|
545
|
-
|
|
546
|
-
"
|
|
566
|
+
# section with heading ID, classes, and custom attributes
|
|
567
|
+
my_section = Section(
|
|
568
|
+
"Section with attributes",
|
|
547
569
|
attributes=Attributes(
|
|
548
|
-
html_id="my-
|
|
570
|
+
html_id="my-section",
|
|
549
571
|
css_classes=["class1", "class2"],
|
|
550
572
|
attrs={"style": "color: blue;"},
|
|
551
573
|
),
|
|
552
574
|
)
|
|
553
|
-
doc +=
|
|
575
|
+
doc += my_section
|
|
554
576
|
|
|
555
577
|
# span with attributes
|
|
556
|
-
|
|
578
|
+
my_section += Span(
|
|
557
579
|
"Text with attributes",
|
|
558
580
|
attributes=Attributes(html_id="my-span", css_classes="class1"),
|
|
559
581
|
)
|
|
560
582
|
|
|
561
|
-
#
|
|
562
|
-
|
|
583
|
+
# references to section, by object and by id
|
|
584
|
+
my_section += Ref(my_section, "See this section")
|
|
585
|
+
my_section += Ref("my-section")
|
|
563
586
|
|
|
564
587
|
doc.render_file("doc.md", flavor="pandoc")
|
|
565
588
|
```
|
|
@@ -567,11 +590,13 @@ doc.render_file("doc.md", flavor="pandoc")
|
|
|
567
590
|
This is rendered as:
|
|
568
591
|
|
|
569
592
|
```markdown
|
|
570
|
-
#
|
|
593
|
+
# Section with attributes {#my-section .class1 .class2 style="color: blue;"}
|
|
571
594
|
|
|
572
595
|
[Text with attributes]{#my-span .class1}
|
|
573
596
|
|
|
574
|
-
[See
|
|
597
|
+
[See this section](#my-section)
|
|
598
|
+
|
|
599
|
+
[Section with attributes](#my-section)
|
|
575
600
|
|
|
576
601
|
```
|
|
577
602
|
|
|
@@ -580,12 +605,12 @@ This is rendered as:
|
|
|
580
605
|
As MDForge is designed with pandoc compatibility in mind, it automatically tracks required pandoc extensions:
|
|
581
606
|
|
|
582
607
|
```python
|
|
583
|
-
from mdforge import Attributes, Document,
|
|
608
|
+
from mdforge import Attributes, Document, Section, Strikethrough
|
|
584
609
|
|
|
585
610
|
doc = Document()
|
|
586
611
|
|
|
587
612
|
# requires "header_attributes"
|
|
588
|
-
doc +=
|
|
613
|
+
doc += Section("Section 1", attributes=Attributes(html_id="section-1"))
|
|
589
614
|
|
|
590
615
|
# requires "strikeout"
|
|
591
616
|
doc += Strikethrough("This text is struck through")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
classifiers = [
|
|
3
|
+
"Development Status :: 3 - Alpha",
|
|
4
|
+
"Natural Language :: English",
|
|
5
|
+
"Operating System :: OS Independent",
|
|
6
|
+
"Programming Language :: Python :: 3",
|
|
7
|
+
"Programming Language :: Python :: 3.12",
|
|
8
|
+
"Programming Language :: Python :: 3.13",
|
|
9
|
+
"Programming Language :: Python :: 3.14",
|
|
10
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
11
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
12
|
+
"Typing :: Typed",
|
|
13
|
+
]
|
|
14
|
+
dependencies = ["pyyaml>=6.0.2,<7"]
|
|
15
|
+
description = "Pythonic multi-flavor Markdown generator"
|
|
16
|
+
license = "MIT"
|
|
17
|
+
license-files = ["LICENSE"]
|
|
18
|
+
name = "mdforge"
|
|
19
|
+
readme = "README.md"
|
|
20
|
+
requires-python = ">=3.12,<4.0"
|
|
21
|
+
version = "0.3.0"
|
|
22
|
+
|
|
23
|
+
[[project.authors]]
|
|
24
|
+
name = "mm21"
|
|
25
|
+
email = "mm21.dev@gmail.com"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
homepage = "https://github.com/mm21/mdforge"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pyprojkit[all]>=0.9.0,<0.10.0",
|
|
33
|
+
"pytest-powerpack>=0.1.5,<0.2",
|
|
34
|
+
"types-pyyaml>=6.0.12,<7",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
build-backend = "uv_build"
|
|
39
|
+
requires = ["uv_build>=0.13.5,<0.14.0"]
|
|
40
|
+
|
|
41
|
+
[tool.autoflake]
|
|
42
|
+
in_place = true
|
|
43
|
+
recursive = true
|
|
44
|
+
remove-all-unused-imports = true
|
|
45
|
+
remove-unused-variables = true
|
|
46
|
+
|
|
47
|
+
[tool.black]
|
|
48
|
+
quiet = true
|
|
49
|
+
target-version = [
|
|
50
|
+
"py312",
|
|
51
|
+
"py313",
|
|
52
|
+
"py314",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.coverage.report]
|
|
56
|
+
exclude_lines = [
|
|
57
|
+
"if TYPE_CHECKING:",
|
|
58
|
+
'\.\.\.$',
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.coverage.run]
|
|
62
|
+
data_file = "__cache__/.coverage"
|
|
63
|
+
|
|
64
|
+
[tool.docfmt]
|
|
65
|
+
force-reflow = true
|
|
66
|
+
in-place = true
|
|
67
|
+
recursive = true
|
|
68
|
+
summary-on-own-line = true
|
|
69
|
+
|
|
70
|
+
[tool.doit]
|
|
71
|
+
dep_file = "__cache__/.doit.db"
|
|
72
|
+
verbosity = 2
|
|
73
|
+
|
|
74
|
+
[tool.isort]
|
|
75
|
+
profile = "black"
|
|
76
|
+
quiet = true
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
check_untyped_defs = true
|
|
80
|
+
python_version = "3.12"
|
|
81
|
+
|
|
82
|
+
[[tool.mypy.overrides]]
|
|
83
|
+
ignore_missing_imports = true
|
|
84
|
+
module = [
|
|
85
|
+
"pytest_powerpack",
|
|
86
|
+
"pytest_powerpack.*",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[tool.nox]
|
|
90
|
+
default_venv_backend = "uv"
|
|
91
|
+
envdir = "__cache__/nox"
|
|
92
|
+
|
|
93
|
+
[tool.pyright]
|
|
94
|
+
pythonVersion = "3.12"
|
|
95
|
+
|
|
96
|
+
[tool.pytest.ini_options]
|
|
97
|
+
addopts = "--import-mode=importlib -s -v -rA"
|
|
98
|
+
cache_dir = "__cache__/pytest"
|
|
99
|
+
powerpack_underline = true
|
|
100
|
+
testpaths = ["test"]
|
|
101
|
+
|
|
102
|
+
[tool.tomlsort]
|
|
103
|
+
sort_first = [
|
|
104
|
+
"project",
|
|
105
|
+
"dependency-groups",
|
|
106
|
+
"build-system",
|
|
107
|
+
]
|
|
108
|
+
sort_table_keys = true
|
|
109
|
+
spaces_before_inline_comment = 2
|
|
@@ -23,21 +23,21 @@ license-files = ["LICENSE"]
|
|
|
23
23
|
name = "mdforge"
|
|
24
24
|
readme = "README.md"
|
|
25
25
|
requires-python = ">=3.12,<4.0" # pyprojkit-managed
|
|
26
|
-
version = "0.
|
|
26
|
+
version = "0.3.0"
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
homepage = "https://github.com/mm21/mdforge"
|
|
30
30
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
|
-
"pyprojkit[all]>=0.
|
|
33
|
+
"pyprojkit[all]>=0.9.0,<0.10.0",
|
|
34
34
|
"pytest-powerpack>=0.1.5,<0.2",
|
|
35
35
|
"types-pyyaml>=6.0.12,<7"
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[build-system]
|
|
39
|
-
build-backend = "uv_build"
|
|
40
|
-
requires = ["uv_build>=0.
|
|
39
|
+
build-backend = "uv_build" # pyprojkit-tracked
|
|
40
|
+
requires = ["uv_build>=0.13.5,<0.14.0"] # pyprojkit-tracked
|
|
41
41
|
|
|
42
42
|
[tool.autoflake]
|
|
43
43
|
in_place = true # pyprojkit-managed
|
|
@@ -32,8 +32,10 @@ from .container import (
|
|
|
32
32
|
)
|
|
33
33
|
from .elements import (
|
|
34
34
|
Section,
|
|
35
|
-
Heading,
|
|
36
35
|
BlockImage,
|
|
36
|
+
HorizontalRule,
|
|
37
|
+
Quote,
|
|
38
|
+
AlignedBlock,
|
|
37
39
|
Paragraph,
|
|
38
40
|
BlockText,
|
|
39
41
|
ListItemType,
|
|
@@ -61,6 +63,7 @@ from .elements import (
|
|
|
61
63
|
from .exceptions import (
|
|
62
64
|
ValidationError,
|
|
63
65
|
RenderError,
|
|
66
|
+
RenderWarning,
|
|
64
67
|
)
|
|
65
68
|
|
|
66
69
|
__all__ = [
|
|
@@ -74,8 +77,10 @@ __all__ = [
|
|
|
74
77
|
"InlineContainer",
|
|
75
78
|
"BlockContainer",
|
|
76
79
|
"Section",
|
|
77
|
-
"Heading",
|
|
78
80
|
"BlockImage",
|
|
81
|
+
"HorizontalRule",
|
|
82
|
+
"Quote",
|
|
83
|
+
"AlignedBlock",
|
|
79
84
|
"Paragraph",
|
|
80
85
|
"BlockText",
|
|
81
86
|
"ListItemType",
|
|
@@ -101,5 +106,6 @@ __all__ = [
|
|
|
101
106
|
"InlineImage",
|
|
102
107
|
"ValidationError",
|
|
103
108
|
"RenderError",
|
|
109
|
+
"RenderWarning",
|
|
104
110
|
]
|
|
105
111
|
# </AUTOGEN_INIT>
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
from .element import BaseElement, BaseInlineElement
|
|
2
|
-
from .exceptions import ValidationError
|
|
4
|
+
from .exceptions import RenderError, RenderWarning, ValidationError
|
|
3
5
|
|
|
4
6
|
__all__ = [
|
|
5
7
|
"coerce_text",
|
|
6
8
|
"coerce_inline_text",
|
|
7
9
|
"wrap_para_cond",
|
|
10
|
+
"validate_level",
|
|
11
|
+
"check_level",
|
|
8
12
|
]
|
|
9
13
|
|
|
14
|
+
MAX_LEVEL: int = 6
|
|
15
|
+
"""
|
|
16
|
+
Maximum heading level supported by markdown.
|
|
17
|
+
"""
|
|
18
|
+
|
|
10
19
|
|
|
11
20
|
def coerce_text(obj: str) -> BaseElement:
|
|
12
21
|
"""
|
|
@@ -29,8 +38,8 @@ def coerce_inline_text(obj: str, allow_multiline: bool = False) -> BaseInlineEle
|
|
|
29
38
|
# get lines
|
|
30
39
|
lines = obj.split("\n")
|
|
31
40
|
|
|
32
|
-
# check for blank lines
|
|
33
|
-
if any(line.strip() == "" for line in lines):
|
|
41
|
+
# check for blank lines, allowing single-line text to be only whitespace
|
|
42
|
+
if len(lines) > 1 and any(line.strip() == "" for line in lines):
|
|
34
43
|
raise ValidationError(f"Input text cannot contain blank lines: {obj}")
|
|
35
44
|
|
|
36
45
|
if len(lines) > 1 and allow_multiline:
|
|
@@ -58,3 +67,27 @@ def wrap_para_cond(lines: list[str]):
|
|
|
58
67
|
|
|
59
68
|
lines[0] = f"<p>{lines[0]}"
|
|
60
69
|
lines[-1] = f"{lines[-1]}</p>"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def validate_level(level: int | None):
|
|
73
|
+
"""
|
|
74
|
+
Ensure the explicit level, if given, is supported by markdown.
|
|
75
|
+
"""
|
|
76
|
+
if level is not None and not 1 <= level <= MAX_LEVEL:
|
|
77
|
+
raise ValidationError(f"Level must be between 1 and {MAX_LEVEL}, got {level}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_level(desc: str, level: int, expected: int):
|
|
81
|
+
"""
|
|
82
|
+
Check the level of an element against the level expected from its placement: fail if
|
|
83
|
+
it's not nested in its containing level, or warn if it skips levels.
|
|
84
|
+
"""
|
|
85
|
+
if level < expected:
|
|
86
|
+
raise RenderError(
|
|
87
|
+
f"Level {level} of {desc} must be at least {expected} based on its placement"
|
|
88
|
+
)
|
|
89
|
+
if level > expected:
|
|
90
|
+
warnings.warn(
|
|
91
|
+
f"Level {level} of {desc} skips levels, expected {expected} based on its placement",
|
|
92
|
+
RenderWarning,
|
|
93
|
+
)
|