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.

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