python-substack 0.1.20__tar.gz → 0.1.22__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.
- {python_substack-0.1.20 → python_substack-0.1.22}/PKG-INFO +26 -5
- {python_substack-0.1.20 → python_substack-0.1.22}/README.md +22 -0
- {python_substack-0.1.20 → python_substack-0.1.22}/pyproject.toml +10 -5
- {python_substack-0.1.20 → python_substack-0.1.22}/substack/__init__.py +1 -1
- {python_substack-0.1.20 → python_substack-0.1.22}/substack/post.py +127 -25
- {python_substack-0.1.20 → python_substack-0.1.22}/LICENSE +0 -0
- {python_substack-0.1.20 → python_substack-0.1.22}/substack/api.py +0 -0
- {python_substack-0.1.20 → python_substack-0.1.22}/substack/exceptions.py +0 -0
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-substack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.22
|
|
4
4
|
Summary: A Python wrapper around the Substack API.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: substack
|
|
8
8
|
Author: Paolo Mazza
|
|
9
9
|
Author-email: mazzapaolo2019@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
18
|
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
20
|
-
Requires-Dist: python-dotenv (>=
|
|
21
|
-
Requires-Dist: requests (>=2.
|
|
19
|
+
Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
|
|
20
|
+
Requires-Dist: requests (>=2.32.0,<3.0.0)
|
|
22
21
|
Project-URL: Homepage, https://github.com/ma2za/python-substack
|
|
23
22
|
Project-URL: Repository, https://github.com/ma2za/python-substack
|
|
24
23
|
Description-Content-Type: text/markdown
|
|
@@ -38,6 +37,12 @@ You can install python-substack using:
|
|
|
38
37
|
|
|
39
38
|
$ pip install python-substack
|
|
40
39
|
|
|
40
|
+
For the MCP server tools, install the extra dependency set:
|
|
41
|
+
|
|
42
|
+
$ poetry install --with mcp
|
|
43
|
+
|
|
44
|
+
> NOTE: We had to upgrade the package requirements to support Python 3.10 because 3.9 is basically vintage now. If you still run 3.9, please join us in the future (or bring snacks).
|
|
45
|
+
|
|
41
46
|
---
|
|
42
47
|
|
|
43
48
|
# Setup
|
|
@@ -272,6 +277,22 @@ body:
|
|
|
272
277
|
src: "local_image.jpg" # Local images will be uploaded automatically
|
|
273
278
|
```
|
|
274
279
|
|
|
280
|
+
## MCP FastMCP server
|
|
281
|
+
|
|
282
|
+
This package now includes a FastMCP server in `substack/mcp_fastmcp.py` with the following tools:
|
|
283
|
+
|
|
284
|
+
- `post_draft_from_markdown(...)`: create draft from markdown, optional tag/add/prepublish/publish, and control send/share_automatically.
|
|
285
|
+
- `put_draft(draft_id, update_payload)`: update draft fields.
|
|
286
|
+
- `add_tags(draft_id, tags)`: add tags to a draft/post.
|
|
287
|
+
- `prepublish_draft(draft_id)`: prepublish a draft.
|
|
288
|
+
- `publish_draft(draft_id, send=True, share_automatically=False)`: publish a draft.
|
|
289
|
+
|
|
290
|
+
Use via stdio transport:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
python -c "from substack.mcp_fastmcp import main; main()"
|
|
294
|
+
```
|
|
295
|
+
|
|
275
296
|
# Contributing
|
|
276
297
|
|
|
277
298
|
Install pre-commit:
|
|
@@ -13,6 +13,12 @@ You can install python-substack using:
|
|
|
13
13
|
|
|
14
14
|
$ pip install python-substack
|
|
15
15
|
|
|
16
|
+
For the MCP server tools, install the extra dependency set:
|
|
17
|
+
|
|
18
|
+
$ poetry install --with mcp
|
|
19
|
+
|
|
20
|
+
> NOTE: We had to upgrade the package requirements to support Python 3.10 because 3.9 is basically vintage now. If you still run 3.9, please join us in the future (or bring snacks).
|
|
21
|
+
|
|
16
22
|
---
|
|
17
23
|
|
|
18
24
|
# Setup
|
|
@@ -247,6 +253,22 @@ body:
|
|
|
247
253
|
src: "local_image.jpg" # Local images will be uploaded automatically
|
|
248
254
|
```
|
|
249
255
|
|
|
256
|
+
## MCP FastMCP server
|
|
257
|
+
|
|
258
|
+
This package now includes a FastMCP server in `substack/mcp_fastmcp.py` with the following tools:
|
|
259
|
+
|
|
260
|
+
- `post_draft_from_markdown(...)`: create draft from markdown, optional tag/add/prepublish/publish, and control send/share_automatically.
|
|
261
|
+
- `put_draft(draft_id, update_payload)`: update draft fields.
|
|
262
|
+
- `add_tags(draft_id, tags)`: add tags to a draft/post.
|
|
263
|
+
- `prepublish_draft(draft_id)`: prepublish a draft.
|
|
264
|
+
- `publish_draft(draft_id, send=True, share_automatically=False)`: publish a draft.
|
|
265
|
+
|
|
266
|
+
Use via stdio transport:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
python -c "from substack.mcp_fastmcp import main; main()"
|
|
270
|
+
```
|
|
271
|
+
|
|
250
272
|
# Contributing
|
|
251
273
|
|
|
252
274
|
Install pre-commit:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-substack"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.22"
|
|
4
4
|
description = "A Python wrapper around the Substack API."
|
|
5
5
|
authors = ["Paolo Mazza <mazzapaolo2019@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -16,15 +16,20 @@ homepage = "https://github.com/ma2za/python-substack"
|
|
|
16
16
|
keywords = ["substack"]
|
|
17
17
|
|
|
18
18
|
[tool.poetry.dependencies]
|
|
19
|
-
python = "
|
|
19
|
+
python = "<4.0,>=3.10"
|
|
20
20
|
|
|
21
|
-
requests = "^2.
|
|
22
|
-
python-dotenv = "^
|
|
21
|
+
requests = "^2.32.0"
|
|
22
|
+
python-dotenv = "^1.2.1"
|
|
23
23
|
PyYAML = "^6.0"
|
|
24
24
|
|
|
25
|
-
|
|
26
25
|
[tool.poetry.group.dev.dependencies]
|
|
27
26
|
|
|
27
|
+
[tool.poetry.group.mcp]
|
|
28
|
+
|
|
29
|
+
optional = true
|
|
30
|
+
|
|
31
|
+
[tool.poetry.group.mcp.dependencies]
|
|
32
|
+
fastmcp = "^3.1.1"
|
|
28
33
|
|
|
29
34
|
[build-system]
|
|
30
35
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
__author__ = "Paolo Mazza"
|
|
4
4
|
__email__ = "mazzapaolo2019@gmail.com"
|
|
5
5
|
__license__ = "MIT License"
|
|
6
|
-
__version__ = "1.
|
|
6
|
+
__version__ = "0.1.21"
|
|
7
7
|
__url__ = "https://github.com/ma2za/python-substack"
|
|
8
8
|
__download_url__ = "https://pypi.python.org/pypi/python-substack"
|
|
9
9
|
__description__ = "A Python wrapper around the Substack API"
|
|
@@ -8,19 +8,40 @@ import json
|
|
|
8
8
|
import re
|
|
9
9
|
from typing import Dict, List
|
|
10
10
|
|
|
11
|
-
__all__ = ["Post", "parse_inline"]
|
|
11
|
+
__all__ = ["Post", "parse_inline", "tokens_to_text_nodes"]
|
|
12
12
|
|
|
13
13
|
from substack.exceptions import SectionNotExistsException
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def tokens_to_text_nodes(tokens: List[Dict]) -> List[Dict]:
|
|
17
|
+
"""Convert parse_inline() tokens to ProseMirror text nodes.
|
|
18
|
+
|
|
19
|
+
parse_inline() returns {"content": "text", "marks": [...]}.
|
|
20
|
+
ProseMirror expects {"type": "text", "text": "text", "marks": [...]}.
|
|
21
|
+
"""
|
|
22
|
+
nodes = []
|
|
23
|
+
for token in tokens:
|
|
24
|
+
if not token or not token.get("content"):
|
|
25
|
+
continue
|
|
26
|
+
node = {"type": "text", "text": token["content"]}
|
|
27
|
+
marks = token.get("marks")
|
|
28
|
+
if marks:
|
|
29
|
+
node["marks"] = marks
|
|
30
|
+
nodes.append(node)
|
|
31
|
+
return nodes
|
|
32
|
+
|
|
33
|
+
|
|
16
34
|
def parse_inline(text: str) -> List[Dict]:
|
|
17
35
|
"""
|
|
18
36
|
Convert inline Markdown in a text string into a list of tokens
|
|
19
37
|
for use in the post content.
|
|
20
38
|
|
|
21
39
|
Supported formatting:
|
|
40
|
+
- `code`: Text wrapped in backticks.
|
|
22
41
|
- **Bold**: Text wrapped in double asterisks.
|
|
23
42
|
- *Italic*: Text wrapped in single asterisks.
|
|
43
|
+
- ***Bold+Italic***: Text wrapped in triple asterisks.
|
|
44
|
+
- ~~Strikethrough~~: Text wrapped in double tildes.
|
|
24
45
|
- [Links]: Text wrapped in square brackets followed by URL in parentheses.
|
|
25
46
|
|
|
26
47
|
Args:
|
|
@@ -37,33 +58,50 @@ def parse_inline(text: str) -> List[Dict]:
|
|
|
37
58
|
return []
|
|
38
59
|
|
|
39
60
|
tokens = []
|
|
40
|
-
# Process text character by character to handle nested formatting
|
|
41
|
-
# We'll use regex to find all markdown patterns, then process them in order
|
|
42
61
|
|
|
43
|
-
#
|
|
44
|
-
|
|
62
|
+
# Pattern order matters: code > links > bold+italic > bold > italic > strikethrough
|
|
63
|
+
code_pattern = r'`([^`]+)`'
|
|
45
64
|
link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
|
|
65
|
+
bold_italic_pattern = r'\*\*\*([^*]+)\*\*\*'
|
|
46
66
|
bold_pattern = r'\*\*([^*]+)\*\*'
|
|
47
67
|
italic_pattern = r'(?<!\*)\*([^*]+)\*(?!\*)' # Not preceded or followed by *
|
|
68
|
+
strikethrough_pattern = r'~~([^~]+)~~'
|
|
48
69
|
|
|
49
70
|
# Find all matches with their positions
|
|
50
71
|
matches = []
|
|
72
|
+
|
|
73
|
+
# Inline code FIRST -- content inside backticks must not be parsed for other formatting
|
|
74
|
+
for match in re.finditer(code_pattern, text):
|
|
75
|
+
matches.append((match.start(), match.end(), "code", match.group(1), None))
|
|
76
|
+
|
|
77
|
+
# Links
|
|
51
78
|
for match in re.finditer(link_pattern, text):
|
|
52
79
|
# Skip if it's an image link (starts with ![)
|
|
53
80
|
# But do NOT skip normal links at position 0.
|
|
54
81
|
if match.start() == 0 or text[match.start()-1:match.start()+1] != "![":
|
|
55
|
-
|
|
82
|
+
if not any(start <= match.start() < end for start, end, _, _, _ in matches):
|
|
83
|
+
matches.append((match.start(), match.end(), "link", match.group(1), match.group(2)))
|
|
84
|
+
|
|
85
|
+
# Bold+italic combo
|
|
86
|
+
for match in re.finditer(bold_italic_pattern, text):
|
|
87
|
+
if not any(start <= match.start() < end for start, end, _, _, _ in matches):
|
|
88
|
+
matches.append((match.start(), match.end(), "bold_italic", match.group(1), None))
|
|
56
89
|
|
|
90
|
+
# Bold
|
|
57
91
|
for match in re.finditer(bold_pattern, text):
|
|
58
|
-
# Check if this range is already covered by a link
|
|
59
92
|
if not any(start <= match.start() < end for start, end, _, _, _ in matches):
|
|
60
93
|
matches.append((match.start(), match.end(), "bold", match.group(1), None))
|
|
61
94
|
|
|
95
|
+
# Italic
|
|
62
96
|
for match in re.finditer(italic_pattern, text):
|
|
63
|
-
# Check if this range is already covered by a link or bold
|
|
64
97
|
if not any(start <= match.start() < end for start, end, _, _, _ in matches):
|
|
65
98
|
matches.append((match.start(), match.end(), "italic", match.group(1), None))
|
|
66
99
|
|
|
100
|
+
# Strikethrough
|
|
101
|
+
for match in re.finditer(strikethrough_pattern, text):
|
|
102
|
+
if not any(start <= match.start() < end for start, end, _, _, _ in matches):
|
|
103
|
+
matches.append((match.start(), match.end(), "strikethrough", match.group(1), None))
|
|
104
|
+
|
|
67
105
|
# Sort matches by position
|
|
68
106
|
matches.sort(key=lambda x: x[0])
|
|
69
107
|
|
|
@@ -75,11 +113,21 @@ def parse_inline(text: str) -> List[Dict]:
|
|
|
75
113
|
tokens.append({"content": text[last_pos:start]})
|
|
76
114
|
|
|
77
115
|
# Add the formatted content
|
|
78
|
-
if match_type == "
|
|
116
|
+
if match_type == "code":
|
|
117
|
+
tokens.append({
|
|
118
|
+
"content": content,
|
|
119
|
+
"marks": [{"type": "code"}]
|
|
120
|
+
})
|
|
121
|
+
elif match_type == "link":
|
|
79
122
|
tokens.append({
|
|
80
123
|
"content": content,
|
|
81
124
|
"marks": [{"type": "link", "attrs": {"href": url}}]
|
|
82
125
|
})
|
|
126
|
+
elif match_type == "bold_italic":
|
|
127
|
+
tokens.append({
|
|
128
|
+
"content": content,
|
|
129
|
+
"marks": [{"type": "strong"}, {"type": "em"}]
|
|
130
|
+
})
|
|
83
131
|
elif match_type == "bold":
|
|
84
132
|
tokens.append({
|
|
85
133
|
"content": content,
|
|
@@ -90,6 +138,11 @@ def parse_inline(text: str) -> List[Dict]:
|
|
|
90
138
|
"content": content,
|
|
91
139
|
"marks": [{"type": "em"}]
|
|
92
140
|
})
|
|
141
|
+
elif match_type == "strikethrough":
|
|
142
|
+
tokens.append({
|
|
143
|
+
"content": content,
|
|
144
|
+
"marks": [{"type": "strikethrough"}]
|
|
145
|
+
})
|
|
93
146
|
|
|
94
147
|
last_pos = end
|
|
95
148
|
|
|
@@ -503,7 +556,9 @@ class Post:
|
|
|
503
556
|
- Blockquotes: Lines starting with '>' (consecutive lines grouped)
|
|
504
557
|
- Paragraphs: Regular text blocks
|
|
505
558
|
- Bullet lists: Lines starting with '*' or '-'
|
|
506
|
-
-
|
|
559
|
+
- Ordered lists: Lines starting with '1.', '2.', etc.
|
|
560
|
+
- Horizontal rules: Lines with ---, ***, or ___
|
|
561
|
+
- Inline formatting: **bold**, *italic*, ***bold+italic***, `code`, ~~strikethrough~~
|
|
507
562
|
|
|
508
563
|
Args:
|
|
509
564
|
markdown_content: Markdown string to parse and add to the post.
|
|
@@ -593,6 +648,11 @@ class Post:
|
|
|
593
648
|
if not text_content:
|
|
594
649
|
continue
|
|
595
650
|
|
|
651
|
+
# Check for horizontal rule: ---, ***, ___
|
|
652
|
+
if re.match(r'^(\*{3,}|-{3,}|_{3,})\s*$', text_content):
|
|
653
|
+
self.horizontal_rule()
|
|
654
|
+
continue
|
|
655
|
+
|
|
596
656
|
# Process headings (lines starting with '#' characters)
|
|
597
657
|
if text_content.startswith("#"):
|
|
598
658
|
level = len(text_content) - len(text_content.lstrip("#"))
|
|
@@ -648,14 +708,15 @@ class Post:
|
|
|
648
708
|
|
|
649
709
|
self.add({"type": "captionedImage", "src": image_url})
|
|
650
710
|
|
|
651
|
-
# Process paragraphs, bullet lists, or blockquotes
|
|
711
|
+
# Process paragraphs, bullet lists, ordered lists, or blockquotes
|
|
652
712
|
else:
|
|
653
713
|
if "\n" in text_content:
|
|
654
|
-
# Process each line, grouping consecutive bullets
|
|
655
|
-
# into
|
|
656
|
-
#
|
|
714
|
+
# Process each line, grouping consecutive bullets/ordered items
|
|
715
|
+
# into list nodes and consecutive blockquote lines into a
|
|
716
|
+
# single blockquote node.
|
|
657
717
|
pending_bullets: List[List[Dict]] = []
|
|
658
718
|
pending_quotes: List[str] = []
|
|
719
|
+
pending_ordered: List[List[Dict]] = []
|
|
659
720
|
|
|
660
721
|
def flush_bullets():
|
|
661
722
|
if not pending_bullets:
|
|
@@ -677,10 +738,7 @@ class Post:
|
|
|
677
738
|
paragraphs: List[Dict] = []
|
|
678
739
|
for quote_line in pending_quotes:
|
|
679
740
|
tokens = parse_inline(quote_line)
|
|
680
|
-
text_nodes =
|
|
681
|
-
{"type": "text", "text": t["content"]}
|
|
682
|
-
for t in tokens if t
|
|
683
|
-
]
|
|
741
|
+
text_nodes = tokens_to_text_nodes(tokens)
|
|
684
742
|
if text_nodes:
|
|
685
743
|
paragraphs.append({"type": "paragraph", "content": text_nodes})
|
|
686
744
|
node: Dict = {"type": "blockquote"}
|
|
@@ -689,20 +747,48 @@ class Post:
|
|
|
689
747
|
self.draft_body["content"].append(node)
|
|
690
748
|
pending_quotes.clear()
|
|
691
749
|
|
|
750
|
+
def flush_ordered():
|
|
751
|
+
if not pending_ordered:
|
|
752
|
+
return
|
|
753
|
+
list_items = []
|
|
754
|
+
for item_nodes in pending_ordered:
|
|
755
|
+
list_items.append({
|
|
756
|
+
"type": "list_item",
|
|
757
|
+
"content": [{"type": "paragraph", "content": item_nodes}],
|
|
758
|
+
})
|
|
759
|
+
self.draft_body["content"].append(
|
|
760
|
+
{"type": "ordered_list", "content": list_items}
|
|
761
|
+
)
|
|
762
|
+
pending_ordered.clear()
|
|
763
|
+
|
|
692
764
|
for line in text_content.split("\n"):
|
|
693
765
|
line = line.strip()
|
|
694
766
|
if not line:
|
|
695
767
|
flush_bullets()
|
|
768
|
+
flush_ordered()
|
|
696
769
|
flush_quotes()
|
|
697
770
|
continue
|
|
698
771
|
|
|
699
772
|
# Check for blockquote marker
|
|
700
773
|
if line.startswith("> ") or line == ">":
|
|
701
774
|
flush_bullets()
|
|
775
|
+
flush_ordered()
|
|
702
776
|
quote_text = line[2:] if line.startswith("> ") else ""
|
|
703
777
|
pending_quotes.append(quote_text)
|
|
704
778
|
continue
|
|
705
779
|
|
|
780
|
+
# Check for ordered list marker
|
|
781
|
+
ordered_match = re.match(r'^(\d+)\.\s+(.*)', line)
|
|
782
|
+
if ordered_match:
|
|
783
|
+
flush_bullets()
|
|
784
|
+
flush_quotes()
|
|
785
|
+
item_text = ordered_match.group(2).strip()
|
|
786
|
+
tokens = parse_inline(item_text)
|
|
787
|
+
text_nodes = tokens_to_text_nodes(tokens)
|
|
788
|
+
if text_nodes:
|
|
789
|
+
pending_ordered.append(text_nodes)
|
|
790
|
+
continue
|
|
791
|
+
|
|
706
792
|
# Check for bullet marker
|
|
707
793
|
bullet_text = None
|
|
708
794
|
if line.startswith("* "):
|
|
@@ -713,31 +799,47 @@ class Post:
|
|
|
713
799
|
bullet_text = line[1:].strip()
|
|
714
800
|
|
|
715
801
|
if bullet_text is not None:
|
|
802
|
+
flush_ordered()
|
|
716
803
|
flush_quotes()
|
|
717
804
|
tokens = parse_inline(bullet_text)
|
|
718
|
-
|
|
719
|
-
|
|
805
|
+
text_nodes = tokens_to_text_nodes(tokens)
|
|
806
|
+
if text_nodes:
|
|
807
|
+
pending_bullets.append(text_nodes)
|
|
720
808
|
else:
|
|
721
809
|
flush_bullets()
|
|
810
|
+
flush_ordered()
|
|
722
811
|
flush_quotes()
|
|
723
812
|
tokens = parse_inline(line)
|
|
724
813
|
self.add({"type": "paragraph", "content": tokens})
|
|
725
814
|
|
|
726
815
|
flush_bullets()
|
|
816
|
+
flush_ordered()
|
|
727
817
|
flush_quotes()
|
|
728
818
|
else:
|
|
729
|
-
# Single line —
|
|
819
|
+
# Single line — blockquote, ordered list, or paragraph
|
|
730
820
|
if text_content.startswith("> ") or text_content == ">":
|
|
731
821
|
quote_text = text_content[2:] if text_content.startswith("> ") else ""
|
|
732
822
|
tokens = parse_inline(quote_text)
|
|
733
|
-
text_nodes =
|
|
734
|
-
{"type": "text", "text": t["content"]}
|
|
735
|
-
for t in tokens if t
|
|
736
|
-
]
|
|
823
|
+
text_nodes = tokens_to_text_nodes(tokens)
|
|
737
824
|
para = {"type": "paragraph", "content": text_nodes} if text_nodes else {"type": "paragraph"}
|
|
738
825
|
self.draft_body["content"] = self.draft_body.get("content", []) + [
|
|
739
826
|
{"type": "blockquote", "content": [para]}
|
|
740
827
|
]
|
|
828
|
+
|
|
829
|
+
elif re.match(r'^(\d+)\.\s+(.*)', text_content):
|
|
830
|
+
ordered_match = re.match(r'^(\d+)\.\s+(.*)', text_content)
|
|
831
|
+
item_text = ordered_match.group(2).strip()
|
|
832
|
+
tokens = parse_inline(item_text)
|
|
833
|
+
text_nodes = tokens_to_text_nodes(tokens)
|
|
834
|
+
if text_nodes:
|
|
835
|
+
list_item = {
|
|
836
|
+
"type": "list_item",
|
|
837
|
+
"content": [{"type": "paragraph", "content": text_nodes}],
|
|
838
|
+
}
|
|
839
|
+
self.draft_body["content"].append(
|
|
840
|
+
{"type": "ordered_list", "content": [list_item]}
|
|
841
|
+
)
|
|
842
|
+
|
|
741
843
|
else:
|
|
742
844
|
tokens = parse_inline(text_content)
|
|
743
845
|
self.add({"type": "paragraph", "content": tokens})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|