python-substack 0.1.23__py3-none-any.whl → 0.1.25__py3-none-any.whl
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.25.dist-info/METADATA +381 -0
- python_substack-0.1.25.dist-info/RECORD +13 -0
- python_substack-0.1.25.dist-info/entry_points.txt +6 -0
- substack/__init__.py +1 -1
- substack/api.py +93 -16
- substack/cli.py +224 -0
- substack/mdrender.py +210 -0
- substack/nodes.py +148 -0
- substack/post.py +8 -421
- substack_mcp/mcp_server.py +293 -0
- python_substack-0.1.23.dist-info/METADATA +0 -328
- python_substack-0.1.23.dist-info/RECORD +0 -8
- {python_substack-0.1.23.dist-info → python_substack-0.1.25.dist-info}/WHEEL +0 -0
- {python_substack-0.1.23.dist-info → python_substack-0.1.25.dist-info}/licenses/LICENSE +0 -0
substack/post.py
CHANGED
|
@@ -11,10 +11,7 @@ from typing import Dict, List
|
|
|
11
11
|
__all__ = ["Post", "parse_inline", "tokens_to_text_nodes"]
|
|
12
12
|
|
|
13
13
|
from substack.exceptions import SectionNotExistsException
|
|
14
|
-
|
|
15
|
-
# Markdown footnotes: ``text.[^label]`` references and ``[^label]: definition`` lines.
|
|
16
|
-
FOOTNOTE_REFERENCE_PATTERN = re.compile(r"\[\^([^\]]+)\]")
|
|
17
|
-
FOOTNOTE_DEFINITION_PATTERN = re.compile(r"^\[\^([^\]]+)\]:\s?(.*)$")
|
|
14
|
+
from substack import nodes
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
def tokens_to_text_nodes(tokens: List[Dict]) -> List[Dict]:
|
|
@@ -560,7 +557,7 @@ class Post:
|
|
|
560
557
|
|
|
561
558
|
"""
|
|
562
559
|
content = self.draft_body["content"][-1].get("content", [])
|
|
563
|
-
content += [
|
|
560
|
+
content += [nodes.footnote_anchor(number)]
|
|
564
561
|
self.draft_body["content"][-1]["content"] = content
|
|
565
562
|
return self
|
|
566
563
|
|
|
@@ -586,147 +583,19 @@ class Post:
|
|
|
586
583
|
for chunk in re.split(r"\n\s*\n", content):
|
|
587
584
|
chunk = chunk.strip()
|
|
588
585
|
if chunk:
|
|
589
|
-
paragraphs.append(
|
|
590
|
-
{"type": "paragraph", "content": tokens_to_text_nodes(parse_inline(chunk))}
|
|
591
|
-
)
|
|
586
|
+
paragraphs.append(nodes.paragraph(tokens_to_text_nodes(parse_inline(chunk))))
|
|
592
587
|
elif isinstance(content, list):
|
|
593
588
|
# Accept either parse_inline tokens ({"content": ...}) or text nodes.
|
|
594
589
|
if content and content[0].get("type") == "text":
|
|
595
590
|
text_nodes = content
|
|
596
591
|
else:
|
|
597
592
|
text_nodes = tokens_to_text_nodes(content)
|
|
598
|
-
paragraphs.append(
|
|
599
|
-
|
|
600
|
-
if not paragraphs:
|
|
601
|
-
paragraphs = [{"type": "paragraph", "content": []}]
|
|
593
|
+
paragraphs.append(nodes.paragraph(text_nodes))
|
|
602
594
|
|
|
603
|
-
node: Dict =
|
|
604
|
-
"type": "footnote",
|
|
605
|
-
"attrs": {"number": number},
|
|
606
|
-
"content": paragraphs,
|
|
607
|
-
}
|
|
595
|
+
node: Dict = nodes.footnote(number, paragraphs)
|
|
608
596
|
self.draft_body["content"] = self.draft_body.get("content", []) + [node]
|
|
609
597
|
return self
|
|
610
598
|
|
|
611
|
-
@staticmethod
|
|
612
|
-
def _extract_footnote_definitions(markdown_content: str):
|
|
613
|
-
"""
|
|
614
|
-
|
|
615
|
-
Pull ``[^label]: definition`` lines out of the Markdown.
|
|
616
|
-
|
|
617
|
-
Definitions may wrap onto indented continuation lines and may contain
|
|
618
|
-
multiple paragraphs (blank line followed by an indented block). Returns
|
|
619
|
-
the body with definitions removed plus a {label: definition_text} mapping,
|
|
620
|
-
where paragraphs are separated by a blank line.
|
|
621
|
-
|
|
622
|
-
"""
|
|
623
|
-
lines = markdown_content.split("\n")
|
|
624
|
-
body_lines: List[str] = []
|
|
625
|
-
definitions: Dict[str, str] = {}
|
|
626
|
-
in_code_fence = False
|
|
627
|
-
i = 0
|
|
628
|
-
while i < len(lines):
|
|
629
|
-
# Track fenced code blocks so footnote-like lines inside them are
|
|
630
|
-
# left untouched.
|
|
631
|
-
if lines[i].lstrip().startswith("```"):
|
|
632
|
-
in_code_fence = not in_code_fence
|
|
633
|
-
body_lines.append(lines[i])
|
|
634
|
-
i += 1
|
|
635
|
-
continue
|
|
636
|
-
match = None if in_code_fence else FOOTNOTE_DEFINITION_PATTERN.match(lines[i])
|
|
637
|
-
if match:
|
|
638
|
-
label, first = match.group(1), match.group(2)
|
|
639
|
-
paragraphs: List[str] = []
|
|
640
|
-
current = [first.strip()] if first.strip() else []
|
|
641
|
-
i += 1
|
|
642
|
-
while i < len(lines):
|
|
643
|
-
line = lines[i]
|
|
644
|
-
if line.strip() == "":
|
|
645
|
-
# A blank line stays in the footnote only if the next
|
|
646
|
-
# non-empty line is indented (a further paragraph).
|
|
647
|
-
nxt = i + 1
|
|
648
|
-
if (
|
|
649
|
-
nxt < len(lines)
|
|
650
|
-
and lines[nxt].strip()
|
|
651
|
-
and lines[nxt][:1] in (" ", "\t")
|
|
652
|
-
):
|
|
653
|
-
if current:
|
|
654
|
-
paragraphs.append(" ".join(current))
|
|
655
|
-
current = []
|
|
656
|
-
i += 1
|
|
657
|
-
continue
|
|
658
|
-
break
|
|
659
|
-
if line[:1] in (" ", "\t"):
|
|
660
|
-
current.append(line.strip())
|
|
661
|
-
i += 1
|
|
662
|
-
else:
|
|
663
|
-
break
|
|
664
|
-
if current:
|
|
665
|
-
paragraphs.append(" ".join(current))
|
|
666
|
-
definitions[label] = "\n\n".join(paragraphs)
|
|
667
|
-
else:
|
|
668
|
-
body_lines.append(lines[i])
|
|
669
|
-
i += 1
|
|
670
|
-
return "\n".join(body_lines), definitions
|
|
671
|
-
|
|
672
|
-
@staticmethod
|
|
673
|
-
def _number_footnotes(markdown_content: str, definitions: Dict[str, str]):
|
|
674
|
-
"""Number footnotes by order of first inline reference in the body."""
|
|
675
|
-
order: List[str] = []
|
|
676
|
-
for match in FOOTNOTE_REFERENCE_PATTERN.finditer(markdown_content):
|
|
677
|
-
label = match.group(1)
|
|
678
|
-
if label in definitions and label not in order:
|
|
679
|
-
order.append(label)
|
|
680
|
-
# Defined-but-unreferenced footnotes go last, in definition order.
|
|
681
|
-
for label in definitions:
|
|
682
|
-
if label not in order:
|
|
683
|
-
order.append(label)
|
|
684
|
-
return {label: index + 1 for index, label in enumerate(order)}
|
|
685
|
-
|
|
686
|
-
def _inject_footnote_anchors(self, node: Dict, numbers_by_label: Dict[str, int]):
|
|
687
|
-
"""Recursively replace ``[^label]`` in text nodes with footnoteAnchor nodes."""
|
|
688
|
-
# Never rewrite the contents of a code block.
|
|
689
|
-
if node.get("type") == "codeBlock":
|
|
690
|
-
return
|
|
691
|
-
content = node.get("content")
|
|
692
|
-
if not isinstance(content, list):
|
|
693
|
-
return
|
|
694
|
-
new_content: List[Dict] = []
|
|
695
|
-
for child in content:
|
|
696
|
-
text = child.get("text", "")
|
|
697
|
-
has_code_mark = any(
|
|
698
|
-
mark.get("type") == "code" for mark in (child.get("marks") or [])
|
|
699
|
-
)
|
|
700
|
-
if (
|
|
701
|
-
child.get("type") == "text"
|
|
702
|
-
and not has_code_mark
|
|
703
|
-
and FOOTNOTE_REFERENCE_PATTERN.search(text)
|
|
704
|
-
):
|
|
705
|
-
marks = child.get("marks")
|
|
706
|
-
last = 0
|
|
707
|
-
for match in FOOTNOTE_REFERENCE_PATTERN.finditer(text):
|
|
708
|
-
label = match.group(1)
|
|
709
|
-
if label not in numbers_by_label:
|
|
710
|
-
continue # Unknown label: leave the literal text in place.
|
|
711
|
-
if match.start() > last:
|
|
712
|
-
segment = {"type": "text", "text": text[last:match.start()]}
|
|
713
|
-
if marks:
|
|
714
|
-
segment["marks"] = marks
|
|
715
|
-
new_content.append(segment)
|
|
716
|
-
new_content.append(
|
|
717
|
-
{"type": "footnoteAnchor", "attrs": {"number": numbers_by_label[label]}}
|
|
718
|
-
)
|
|
719
|
-
last = match.end()
|
|
720
|
-
if last < len(text):
|
|
721
|
-
segment = {"type": "text", "text": text[last:]}
|
|
722
|
-
if marks:
|
|
723
|
-
segment["marks"] = marks
|
|
724
|
-
new_content.append(segment)
|
|
725
|
-
else:
|
|
726
|
-
self._inject_footnote_anchors(child, numbers_by_label)
|
|
727
|
-
new_content.append(child)
|
|
728
|
-
node["content"] = new_content
|
|
729
|
-
|
|
730
599
|
def from_markdown(self, markdown_content: str, api=None):
|
|
731
600
|
"""
|
|
732
601
|
Parse Markdown content and add it to the post.
|
|
@@ -760,290 +629,8 @@ class Post:
|
|
|
760
629
|
>>> post = Post("Title", "Subtitle", user_id)
|
|
761
630
|
>>> post.from_markdown("# Heading\\n\\nThis is **bold** text with [a link](https://example.com).")
|
|
762
631
|
"""
|
|
763
|
-
|
|
764
|
-
# order of first reference before parsing the rest of the body.
|
|
765
|
-
markdown_content, footnote_definitions = self._extract_footnote_definitions(
|
|
766
|
-
markdown_content
|
|
767
|
-
)
|
|
768
|
-
footnote_numbers = self._number_footnotes(markdown_content, footnote_definitions)
|
|
769
|
-
|
|
770
|
-
lines = markdown_content.split("\n")
|
|
771
|
-
blocks = []
|
|
772
|
-
current_block: List[str] = []
|
|
773
|
-
in_code_block = False
|
|
774
|
-
code_block_language = None
|
|
775
|
-
|
|
776
|
-
for line in lines:
|
|
777
|
-
# Check for fenced code block start/end
|
|
778
|
-
if line.strip().startswith("```"):
|
|
779
|
-
if in_code_block:
|
|
780
|
-
# End of code block
|
|
781
|
-
if current_block:
|
|
782
|
-
blocks.append({
|
|
783
|
-
"type": "code",
|
|
784
|
-
"language": code_block_language,
|
|
785
|
-
"content": "\n".join(current_block)
|
|
786
|
-
})
|
|
787
|
-
current_block = []
|
|
788
|
-
in_code_block = False
|
|
789
|
-
code_block_language = None
|
|
790
|
-
else:
|
|
791
|
-
# Start of code block
|
|
792
|
-
if current_block:
|
|
793
|
-
blocks.append({"type": "text", "content": "\n".join(current_block)})
|
|
794
|
-
current_block = []
|
|
795
|
-
# Extract language if specified
|
|
796
|
-
language = line.strip()[3:].strip()
|
|
797
|
-
code_block_language = language if language else None
|
|
798
|
-
in_code_block = True
|
|
799
|
-
continue
|
|
800
|
-
|
|
801
|
-
if in_code_block:
|
|
802
|
-
# Inside code block - collect lines as-is
|
|
803
|
-
current_block.append(line)
|
|
804
|
-
else:
|
|
805
|
-
# Regular content
|
|
806
|
-
if line.strip() == "":
|
|
807
|
-
# Empty line - end current block if it has content
|
|
808
|
-
if current_block:
|
|
809
|
-
blocks.append({"type": "text", "content": "\n".join(current_block)})
|
|
810
|
-
current_block = []
|
|
811
|
-
else:
|
|
812
|
-
current_block.append(line)
|
|
813
|
-
|
|
814
|
-
# Add any remaining content
|
|
815
|
-
if current_block:
|
|
816
|
-
if in_code_block:
|
|
817
|
-
blocks.append({
|
|
818
|
-
"type": "code",
|
|
819
|
-
"language": code_block_language,
|
|
820
|
-
"content": "\n".join(current_block)
|
|
821
|
-
})
|
|
822
|
-
else:
|
|
823
|
-
blocks.append({"type": "text", "content": "\n".join(current_block)})
|
|
824
|
-
|
|
825
|
-
# Process blocks
|
|
826
|
-
for block in blocks:
|
|
827
|
-
if block["type"] == "code":
|
|
828
|
-
# Add code block
|
|
829
|
-
code_content = block.get("content", "").strip()
|
|
830
|
-
if code_content:
|
|
831
|
-
# Substack uses "codeBlock" type
|
|
832
|
-
code_attrs = {}
|
|
833
|
-
if block.get("language"):
|
|
834
|
-
code_attrs["language"] = block["language"]
|
|
835
|
-
self.add({
|
|
836
|
-
"type": "codeBlock",
|
|
837
|
-
"content": code_content, # Pass as string, code_block method will handle it
|
|
838
|
-
"attrs": code_attrs
|
|
839
|
-
})
|
|
840
|
-
else:
|
|
841
|
-
# Process text block
|
|
842
|
-
text_content = block.get("content", "").strip()
|
|
843
|
-
if not text_content:
|
|
844
|
-
continue
|
|
845
|
-
|
|
846
|
-
# Check for horizontal rule: ---, ***, ___
|
|
847
|
-
if re.match(r'^(\*{3,}|-{3,}|_{3,})\s*$', text_content):
|
|
848
|
-
self.horizontal_rule()
|
|
849
|
-
continue
|
|
850
|
-
|
|
851
|
-
# Process headings (lines starting with '#' characters)
|
|
852
|
-
if text_content.startswith("#"):
|
|
853
|
-
level = len(text_content) - len(text_content.lstrip("#"))
|
|
854
|
-
heading_text = text_content.lstrip("#").strip()
|
|
855
|
-
if heading_text: # Only add if there's actual text
|
|
856
|
-
self.heading(content=heading_text, level=min(level, 6))
|
|
857
|
-
|
|
858
|
-
# Process images using Markdown image syntax: 
|
|
859
|
-
# Also handle linked images: [](link_url)
|
|
860
|
-
elif text_content.startswith("!") or (text_content.startswith("[") and "](link)
|
|
862
|
-
linked_image_match = re.match(r'\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)', text_content)
|
|
863
|
-
if linked_image_match:
|
|
864
|
-
# Linked image - create image with href
|
|
865
|
-
alt_text = linked_image_match.group(1)
|
|
866
|
-
image_url = linked_image_match.group(2)
|
|
867
|
-
link_url = linked_image_match.group(3)
|
|
868
|
-
|
|
869
|
-
# Adjust image URL if it starts with a slash
|
|
870
|
-
image_url = image_url[1:] if image_url.startswith("/") else image_url
|
|
871
|
-
|
|
872
|
-
# If api is provided and image_url is a local file, upload it
|
|
873
|
-
if api is not None:
|
|
874
|
-
try:
|
|
875
|
-
image = api.get_image(image_url)
|
|
876
|
-
image_url = image.get("url")
|
|
877
|
-
except Exception:
|
|
878
|
-
# If upload fails, use original URL
|
|
879
|
-
pass
|
|
880
|
-
|
|
881
|
-
self.add({
|
|
882
|
-
"type": "captionedImage",
|
|
883
|
-
"src": image_url,
|
|
884
|
-
"alt": alt_text,
|
|
885
|
-
"href": link_url
|
|
886
|
-
})
|
|
887
|
-
else:
|
|
888
|
-
# Regular image: 
|
|
889
|
-
match = re.match(r"!\[.*?\]\((.*?)\)", text_content)
|
|
890
|
-
if match:
|
|
891
|
-
image_url = match.group(1)
|
|
892
|
-
# Adjust image URL if it starts with a slash
|
|
893
|
-
image_url = image_url[1:] if image_url.startswith("/") else image_url
|
|
894
|
-
|
|
895
|
-
# If api is provided and image_url is a local file, upload it
|
|
896
|
-
if api is not None:
|
|
897
|
-
try:
|
|
898
|
-
image = api.get_image(image_url)
|
|
899
|
-
image_url = image.get("url")
|
|
900
|
-
except Exception:
|
|
901
|
-
# If upload fails, use original URL
|
|
902
|
-
pass
|
|
903
|
-
|
|
904
|
-
self.add({"type": "captionedImage", "src": image_url})
|
|
905
|
-
|
|
906
|
-
# Process paragraphs, bullet lists, ordered lists, or blockquotes
|
|
907
|
-
else:
|
|
908
|
-
if "\n" in text_content:
|
|
909
|
-
# Process each line, grouping consecutive bullets/ordered items
|
|
910
|
-
# into list nodes and consecutive blockquote lines into a
|
|
911
|
-
# single blockquote node.
|
|
912
|
-
pending_bullets: List[List[Dict]] = []
|
|
913
|
-
pending_quotes: List[str] = []
|
|
914
|
-
pending_ordered: List[List[Dict]] = []
|
|
915
|
-
|
|
916
|
-
def flush_bullets():
|
|
917
|
-
if not pending_bullets:
|
|
918
|
-
return
|
|
919
|
-
list_items = []
|
|
920
|
-
for bullet_nodes in pending_bullets:
|
|
921
|
-
list_items.append({
|
|
922
|
-
"type": "list_item",
|
|
923
|
-
"content": [{"type": "paragraph", "content": bullet_nodes}],
|
|
924
|
-
})
|
|
925
|
-
self.draft_body["content"].append(
|
|
926
|
-
{"type": "bullet_list", "content": list_items}
|
|
927
|
-
)
|
|
928
|
-
pending_bullets.clear()
|
|
929
|
-
|
|
930
|
-
def flush_quotes():
|
|
931
|
-
if not pending_quotes:
|
|
932
|
-
return
|
|
933
|
-
paragraphs: List[Dict] = []
|
|
934
|
-
for quote_line in pending_quotes:
|
|
935
|
-
tokens = parse_inline(quote_line)
|
|
936
|
-
text_nodes = tokens_to_text_nodes(tokens)
|
|
937
|
-
if text_nodes:
|
|
938
|
-
paragraphs.append({"type": "paragraph", "content": text_nodes})
|
|
939
|
-
node: Dict = {"type": "blockquote"}
|
|
940
|
-
if paragraphs:
|
|
941
|
-
node["content"] = paragraphs
|
|
942
|
-
self.draft_body["content"].append(node)
|
|
943
|
-
pending_quotes.clear()
|
|
944
|
-
|
|
945
|
-
def flush_ordered():
|
|
946
|
-
if not pending_ordered:
|
|
947
|
-
return
|
|
948
|
-
list_items = []
|
|
949
|
-
for item_nodes in pending_ordered:
|
|
950
|
-
list_items.append({
|
|
951
|
-
"type": "list_item",
|
|
952
|
-
"content": [{"type": "paragraph", "content": item_nodes}],
|
|
953
|
-
})
|
|
954
|
-
self.draft_body["content"].append(
|
|
955
|
-
{"type": "ordered_list", "content": list_items}
|
|
956
|
-
)
|
|
957
|
-
pending_ordered.clear()
|
|
958
|
-
|
|
959
|
-
for line in text_content.split("\n"):
|
|
960
|
-
line = line.strip()
|
|
961
|
-
if not line:
|
|
962
|
-
flush_bullets()
|
|
963
|
-
flush_ordered()
|
|
964
|
-
flush_quotes()
|
|
965
|
-
continue
|
|
966
|
-
|
|
967
|
-
# Check for blockquote marker
|
|
968
|
-
if line.startswith("> ") or line == ">":
|
|
969
|
-
flush_bullets()
|
|
970
|
-
flush_ordered()
|
|
971
|
-
quote_text = line[2:] if line.startswith("> ") else ""
|
|
972
|
-
pending_quotes.append(quote_text)
|
|
973
|
-
continue
|
|
974
|
-
|
|
975
|
-
# Check for ordered list marker
|
|
976
|
-
ordered_match = re.match(r'^(\d+)\.\s+(.*)', line)
|
|
977
|
-
if ordered_match:
|
|
978
|
-
flush_bullets()
|
|
979
|
-
flush_quotes()
|
|
980
|
-
item_text = ordered_match.group(2).strip()
|
|
981
|
-
tokens = parse_inline(item_text)
|
|
982
|
-
text_nodes = tokens_to_text_nodes(tokens)
|
|
983
|
-
if text_nodes:
|
|
984
|
-
pending_ordered.append(text_nodes)
|
|
985
|
-
continue
|
|
986
|
-
|
|
987
|
-
# Check for bullet marker
|
|
988
|
-
bullet_text = None
|
|
989
|
-
if line.startswith("* "):
|
|
990
|
-
bullet_text = line[2:].strip()
|
|
991
|
-
elif line.startswith("- "):
|
|
992
|
-
bullet_text = line[2:].strip()
|
|
993
|
-
elif line.startswith("*") and not line.startswith("**"):
|
|
994
|
-
bullet_text = line[1:].strip()
|
|
995
|
-
|
|
996
|
-
if bullet_text is not None:
|
|
997
|
-
flush_ordered()
|
|
998
|
-
flush_quotes()
|
|
999
|
-
tokens = parse_inline(bullet_text)
|
|
1000
|
-
text_nodes = tokens_to_text_nodes(tokens)
|
|
1001
|
-
if text_nodes:
|
|
1002
|
-
pending_bullets.append(text_nodes)
|
|
1003
|
-
else:
|
|
1004
|
-
flush_bullets()
|
|
1005
|
-
flush_ordered()
|
|
1006
|
-
flush_quotes()
|
|
1007
|
-
tokens = parse_inline(line)
|
|
1008
|
-
self.add({"type": "paragraph", "content": tokens})
|
|
1009
|
-
|
|
1010
|
-
flush_bullets()
|
|
1011
|
-
flush_ordered()
|
|
1012
|
-
flush_quotes()
|
|
1013
|
-
else:
|
|
1014
|
-
# Single line — blockquote, ordered list, or paragraph
|
|
1015
|
-
if text_content.startswith("> ") or text_content == ">":
|
|
1016
|
-
quote_text = text_content[2:] if text_content.startswith("> ") else ""
|
|
1017
|
-
tokens = parse_inline(quote_text)
|
|
1018
|
-
text_nodes = tokens_to_text_nodes(tokens)
|
|
1019
|
-
para = {"type": "paragraph", "content": text_nodes} if text_nodes else {"type": "paragraph"}
|
|
1020
|
-
self.draft_body["content"] = self.draft_body.get("content", []) + [
|
|
1021
|
-
{"type": "blockquote", "content": [para]}
|
|
1022
|
-
]
|
|
1023
|
-
|
|
1024
|
-
elif re.match(r'^(\d+)\.\s+(.*)', text_content):
|
|
1025
|
-
ordered_match = re.match(r'^(\d+)\.\s+(.*)', text_content)
|
|
1026
|
-
item_text = ordered_match.group(2).strip()
|
|
1027
|
-
tokens = parse_inline(item_text)
|
|
1028
|
-
text_nodes = tokens_to_text_nodes(tokens)
|
|
1029
|
-
if text_nodes:
|
|
1030
|
-
list_item = {
|
|
1031
|
-
"type": "list_item",
|
|
1032
|
-
"content": [{"type": "paragraph", "content": text_nodes}],
|
|
1033
|
-
}
|
|
1034
|
-
self.draft_body["content"].append(
|
|
1035
|
-
{"type": "ordered_list", "content": [list_item]}
|
|
1036
|
-
)
|
|
1037
|
-
|
|
1038
|
-
else:
|
|
1039
|
-
tokens = parse_inline(text_content)
|
|
1040
|
-
self.add({"type": "paragraph", "content": tokens})
|
|
1041
|
-
|
|
1042
|
-
# Footnotes: turn ``[^label]`` references into inline anchors, then append
|
|
1043
|
-
# the footnote blocks in numbered order.
|
|
1044
|
-
if footnote_numbers:
|
|
1045
|
-
self._inject_footnote_anchors(self.draft_body, footnote_numbers)
|
|
1046
|
-
for label, number in sorted(footnote_numbers.items(), key=lambda item: item[1]):
|
|
1047
|
-
self.footnote(number, footnote_definitions[label])
|
|
632
|
+
from substack import mdrender
|
|
1048
633
|
|
|
634
|
+
rendered = mdrender.markdown_to_doc(markdown_content, api=api)
|
|
635
|
+
self.draft_body["content"] = self.draft_body.get("content", []) + rendered
|
|
1049
636
|
return self
|