Skip to content

Commit a37540e

Browse files
yf-chauYF Chauclaude
authored
Render bullet lists as proper ProseMirror nodes in from_markdown() (#35)
* Render bullet lists as proper ProseMirror nodes in from_markdown() from_markdown() stripped bullet markers (* / -) but emitted each item as an independent paragraph node. Substack's ProseMirror editor expects a single bullet_list node wrapping list_item children. Without this structure, bullet lists render as disconnected paragraphs in the published post. Consecutive bullet lines are now accumulated and flushed as a single bullet_list node with proper list_item > paragraph nesting. Non-bullet lines and blank lines flush the pending list, so mixed content works correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add before/after screenshots for PR Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: YF Chau <yfchau@YFs-MacBook-Air.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3277368 commit a37540e

File tree

3 files changed

+34
-7
lines changed

3 files changed

+34
-7
lines changed

docs/after.png

63 KB
Loading

docs/before.png

49.1 KB
Loading

substack/post.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -614,22 +614,49 @@ def from_markdown(self, markdown_content: str, api=None):
614614
# Process paragraphs or bullet lists
615615
else:
616616
if "\n" in text_content:
617-
# Process each line separately (for bullet lists)
617+
# Process each line, grouping consecutive bullets
618+
# into a single bullet_list node
619+
pending_bullets: List[List[Dict]] = []
620+
621+
def flush_bullets():
622+
if not pending_bullets:
623+
return
624+
list_items = []
625+
for bullet_nodes in pending_bullets:
626+
list_items.append({
627+
"type": "list_item",
628+
"content": [{"type": "paragraph", "content": bullet_nodes}],
629+
})
630+
self.draft_body["content"].append(
631+
{"type": "bullet_list", "content": list_items}
632+
)
633+
pending_bullets.clear()
634+
618635
for line in text_content.split("\n"):
619636
line = line.strip()
620637
if not line:
638+
flush_bullets()
621639
continue
622-
# Remove bullet marker if present
640+
641+
# Check for bullet marker
642+
bullet_text = None
623643
if line.startswith("* "):
624-
line = line[2:].strip()
644+
bullet_text = line[2:].strip()
625645
elif line.startswith("- "):
626-
line = line[2:].strip()
646+
bullet_text = line[2:].strip()
627647
elif line.startswith("*") and not line.startswith("**"):
628-
line = line[1:].strip()
629-
630-
if line:
648+
bullet_text = line[1:].strip()
649+
650+
if bullet_text is not None:
651+
tokens = parse_inline(bullet_text)
652+
if tokens:
653+
pending_bullets.append(tokens)
654+
else:
655+
flush_bullets()
631656
tokens = parse_inline(line)
632657
self.add({"type": "paragraph", "content": tokens})
658+
659+
flush_bullets()
633660
else:
634661
# Single paragraph
635662
tokens = parse_inline(text_content)

0 commit comments

Comments
 (0)