Module

parsing.containers

Container Stack Architecture for CommonMark Parsing.

This module implements a Container Stack for managing nested block-level containers during Markdown parsing. The stack provides:

  1. Explicit indent ownership - Each container "claims" an indent range
  2. Automatic state propagation - Looseness propagates upward on pop
  3. Centralized indent queries - find_owner() replaces scattered logic

Usage:

stack = ContainerStack()  # Initializes with DOCUMENT frame

# Push a list container
stack.push(ContainerFrame(
    container_type=ContainerType.LIST,
    start_indent=0,
    content_indent=2,
))

# Find owner for a given indent
owner, depth = stack.find_owner(4)

# Pop and propagate state
frame = stack.pop()

Classes

ContainerType 0 ▼
Types of block-level containers in CommonMark.
Bases: Enum

Types of block-level containers in CommonMark.

ContainerFrame 14 ▼
A frame on the container stack representing a parsing context. Each container "claims" an indent r…

A frame on the container stack representing a parsing context.

Each container "claims" an indent range. Tokens are routed to the deepest container that claims their indent.

Attributes

Name Type Description
container_type ContainerType

The type of container (LIST, LIST_ITEM, etc.)

start_indent int

Where this container started (marker position)

content_indent int

Minimum indent for content continuation

marker_width int

Width of the marker (e.g., 2 for "- ")

max_sibling_indent int

For lists, marker siblings can appear at start_indent to start_indent+3

is_loose bool

Whether the container has blank lines between content

saw_blank_line bool

Whether a blank line was seen in this container

ordered bool

For lists, whether the list is ordered

bullet_char str

For unordered lists, the bullet character

start_number int

For ordered lists, the starting number

Methods

owns_content 1 bool ▼
Does this container own content at this indent level?
def owns_content(self, indent: int) -> bool
Parameters
Name Type Description
indent —

The indent level to check

Returns
bool True if content at this indent belongs to this container
owns_marker 1 bool ▼
Could a list marker at this indent be a sibling in this container? Only valid …
def owns_marker(self, indent: int) -> bool

Could a list marker at this indent be a sibling in this container?

Only valid for LIST containers.

Parameters
Name Type Description
indent —

The indent level of the marker

Returns
bool True if a marker at this indent is a sibling item
is_nested_marker 1 bool ▼
Would a marker at this indent start a nested list?
def is_nested_marker(self, indent: int) -> bool
Parameters
Name Type Description
indent —

The indent level of the marker

Returns
bool True if a marker at this indent starts a nested list
Internal Methods 1 ▼
__post_init__ 0 ▼
Set default max_sibling_indent if not provided.
def __post_init__(self) -> None
ContainerStack 13 ▼
Manages the stack of active containers during parsing. Invariant: stack[0] is always DOCUMENT, sta…

Manages the stack of active containers during parsing.

Invariant: stack[0] is always DOCUMENT, stack[-1] is innermost container.

Usage:

stack = ContainerStack()  # Initializes with DOCUMENT frame
stack.push(frame)         # Push new container
stack.pop()               # Pop and propagate state
stack.find_owner(indent)  # Find container owning this indent

Attributes

Name Type Description
_stack list[ContainerFrame] —

Methods

push 1 ▼
Push a new container onto the stack.
def push(self, frame: ContainerFrame) -> None
Parameters
Name Type Description
frame —

The container frame to push

pop 0 ContainerFrame ▼
Pop the innermost container. Propagates state (looseness, blank lines) to pare…
def pop(self) -> ContainerFrame

Pop the innermost container.

Propagates state (looseness, blank lines) to parent if applicable.

  • When popping a LIST_ITEM with blank lines, propagates to parent LIST

Note: For blank lines BETWEEN items (sibling separation), use mark_parent_list_loose() directly instead of relying on propagation. This propagation handles blank lines WITHIN item content.

Note: Nested LIST does NOT propagate to parent LIST_ITEM, as nested list looseness should not affect the outer list's tightness.

Returns
ContainerFrame The popped container frame
current 0 ContainerFrame ▼
Get the innermost container.
def current(self) -> ContainerFrame
Returns
ContainerFrame The current (innermost) container frame
depth 0 int ▼
Current nesting depth (document = 0).
def depth(self) -> int
Returns
int The number of containers above DOCUMENT
find_owner 1 tuple[ContainerFrame, in… ▼
Find which container owns content at this indent. Walks from innermost to oute…
def find_owner(self, indent: int) -> tuple[ContainerFrame, int]

Find which container owns content at this indent.

Walks from innermost to outermost, returns first container that claims the indent.

Parameters
Name Type Description
indent —

The indent level to check

Returns
tuple[ContainerFrame, int] (owner_frame, stack_index) - the frame and its position in stack
find_sibling_list 1 tuple[ContainerFrame, in… ▼
Find a list container where a marker at this indent would be a sibling.
def find_sibling_list(self, marker_indent: int) -> tuple[ContainerFrame, int] | None
Parameters
Name Type Description
marker_indent —

The indent level of the marker

Returns
tuple[ContainerFrame, int] | None (frame, index) if found, None if marker starts new list at document level
pop_until 1 list[ContainerFrame] ▼
Pop containers until stack has target_index + 1 elements.
def pop_until(self, target_index: int) -> list[ContainerFrame]
Parameters
Name Type Description
target_index —

The index to stop at (inclusive)

Returns
list[ContainerFrame] List of popped frames (innermost first)
mark_loose 0 ▼
Mark current container as loose (saw blank line with content after).
def mark_loose(self) -> None
mark_blank_line 0 ▼
Mark that a blank line was seen in current container.
def mark_blank_line(self) -> None
mark_parent_list_loose 0 ▼
Mark the parent LIST container as loose. Called when a blank line between sibl…
def mark_parent_list_loose(self) -> None

Mark the parent LIST container as loose.

Called when a blank line between sibling items is detected. The current frame is LIST_ITEM, and the parent should be LIST.

update_content_indent 1 ▼
Update the current container's content_indent to the actual value. Called when…
def update_content_indent(self, actual_content_indent: int) -> None

Update the current container's content_indent to the actual value.

Called when the first content line is parsed and we know the actual column position where content starts. This allows find_owner() to use the correct indent for subsequent content.

Parameters
Name Type Description
actual_content_indent —

The actual content indent from first line

Internal Methods 1 ▼
__post_init__ 0 ▼
Initialize with DOCUMENT frame.
def __post_init__(self) -> None