all2md.utils.parser_helpers
Parser helper utilities for reducing code duplication across parsers.
This module provides reusable helper functions for common patterns in document parsers, including zip validation, temp file management, footnote handling, attachment processing, and text formatting.
Functions
validate_zip_input: Validate zip archives across different input types
validated_zip_input: Context manager for validated zip input with cleanup
append_attachment_footnotes: Append attachment footnote definitions to document
attachment_result_to_image_node: Convert process_attachment result to Image AST node
group_and_format_runs: Group text runs by formatting and build formatted AST nodes
parse_delimited_block: Parse delimited blocks with opening/closing delimiters
- all2md.utils.parser_helpers.validate_zip_input(input_data: str | Path | IO[bytes] | bytes, suffix: str = '.zip') None
Validate a zip archive across different input types.
This function handles zip validation for Path, bytes, and IO[bytes] inputs by creating temporary files when necessary and cleaning them up properly.
- Parameters:
input_data (str, Path, IO[bytes], or bytes) – The input data to validate
suffix (str, default '.zip') – File suffix for temporary files (e.g., ‘.docx’, ‘.xlsx’, ‘.epub’)
- Raises:
ZipFileSecurityError – If the zip archive contains security threats
MalformedFileError – If the zip archive is corrupted or invalid
Examples
Validate a file path:
>>> validate_zip_input("/path/to/file.docx", suffix='.docx')
Validate bytes:
>>> with open("file.docx", "rb") as f: ... data = f.read() >>> validate_zip_input(data, suffix='.docx')
Notes
This function creates temporary files for bytes and IO[bytes] inputs to enable validation, and ensures proper cleanup even on errors.
- all2md.utils.parser_helpers.validated_zip_input(input_data: str | Path | IO[bytes] | bytes, suffix: str = '.zip') Generator[str | Path | IO[bytes] | bytes, None, None]
Context manager for validated zip input with automatic cleanup.
This context manager validates zip archives and yields the input for parsing, ensuring proper cleanup of temporary files. For bytes/IO inputs, it creates a temporary file that can be reused, avoiding double-reading.
- Parameters:
input_data (str, Path, IO[bytes], or bytes) – The input data to validate and parse
suffix (str, default '.zip') – File suffix for temporary files (e.g., ‘.docx’, ‘.xlsx’, ‘.epub’)
- Yields:
Union[str, Path, IO[bytes], bytes] – The validated input data (may be a temp file path for bytes/IO inputs)
- Raises:
ZipFileSecurityError – If the zip archive contains security threats
MalformedFileError – If the zip archive is corrupted or invalid
Examples
Use with path input:
>>> with validated_zip_input("/path/to/file.docx", '.docx') as validated: ... doc = docx.Document(validated)
Use with bytes input:
>>> data = open("file.docx", "rb").read() >>> with validated_zip_input(data, '.docx') as validated: ... # validated is a temp file path that's already been validated ... doc = docx.Document(validated)
Notes
This context manager optimizes memory usage by avoiding double-reading of the input data. For bytes/IO inputs, it creates a single temp file that’s validated once and can be reused for parsing.
- all2md.utils.parser_helpers.append_attachment_footnotes(children: list[Node], attachment_footnotes: dict[str, str], section_title: str = 'Attachments') None
Append attachment footnote definitions to document children.
This function appends a heading and footnote definitions for attachments to the document’s children list, following the standard pattern used across all parsers.
- Parameters:
children (list[Node]) – The document’s children list to append footnotes to
attachment_footnotes (dict[str, str]) – Dictionary mapping footnote labels to content text
section_title (str, default "Attachments") – Title for the footnotes section heading
Examples
Append attachment footnotes to a document:
>>> children = [Paragraph(...), ...] >>> footnotes = {"img1": "image1.png", "img2": "image2.jpg"} >>> append_attachment_footnotes(children, footnotes, "Image References")
Notes
This function modifies the children list in-place. If attachment_footnotes is empty, no changes are made.
The footnotes are appended in sorted order by label to ensure consistent output across runs.
- all2md.utils.parser_helpers.attachment_result_to_image_node(attachment_result: dict[str, Any], fallback_alt_text: str = 'image') Node | None
Convert process_attachment result dict to Image AST node.
This helper eliminates the need for parsers to manually parse markdown strings with regex to extract URL and alt-text. It directly uses the structured data from process_attachment.
- Parameters:
attachment_result (dict[str, Any]) – Result dictionary from process_attachment containing: - “url”: str - Image URL or data URI - “markdown”: str - Markdown representation (used as fallback) - “footnote_label”: str | None - Footnote label if applicable - “footnote_content”: str | None - Footnote content if applicable - “source_data”: str | None - Source of image data (e.g., “base64”, “downloaded”)
fallback_alt_text (str, default "image") – Alt text to use if not extractable from markdown
- Returns:
Image AST node, or None if attachment result is empty/invalid
- Return type:
Node or None
Examples
Convert attachment result to Image node:
>>> result = process_attachment( ... attachment_data=image_bytes, ... attachment_name="photo.jpg", ... alt_text="Photo", ... attachment_mode="base64" ... ) >>> image_node = attachment_result_to_image_node(result, "image")
Notes
This function handles all attachment modes (skip, alt_text, save, base64) consistently. It extracts URL directly from the result dict when available, and only falls back to markdown parsing when necessary.
- all2md.utils.parser_helpers.group_and_format_runs(runs: Iterable[Any], text_extractor: Callable[[Any], str], format_extractor: Callable[[Any], tuple[bool, ...]], format_builders: tuple[Callable[[list[Node]], Node], ...] | None = None) list[Node]
Group text runs by formatting and build formatted AST nodes.
This helper consolidates the common pattern across DOCX, PPTX, ODT, and ODP parsers for processing text runs with formatting. It: 1. Groups consecutive runs with identical formatting 2. Builds formatted inline nodes with appropriate wrappers
- Parameters:
runs (Iterable[Any]) – Iterable of run objects (format-specific, e.g., docx.Run, pptx Run)
text_extractor (Callable[[Any], str]) – Function to extract text from a run object
format_extractor (Callable[[Any], tuple[bool, ...]]) – Function to extract formatting flags from a run object. Should return tuple of booleans in order: (bold, italic, underline, …)
format_builders (tuple[Callable, ...] | None, default None) – Optional tuple of functions to build formatted nodes. Each function takes list[Node] and returns Node. Default order: (Strong, Emphasis, Underline). Length must match format_extractor tuple length.
- Returns:
List of inline AST nodes with appropriate formatting applied
- Return type:
list[Node]
Examples
Use with DOCX runs:
>>> def get_text(run): ... return run.text >>> def get_format(run): ... return (bool(run.font.bold), bool(run.font.italic)) >>> nodes = group_and_format_runs( ... paragraph.runs, ... text_extractor=get_text, ... format_extractor=get_format ... )
Use with custom format builders:
>>> from all2md.ast import Strong, Emphasis, Strikethrough >>> nodes = group_and_format_runs( ... runs, ... text_extractor=lambda r: r.text, ... format_extractor=lambda r: (r.bold, r.italic, r.strike), ... format_builders=( ... lambda nodes: Strong(content=nodes), ... lambda nodes: Emphasis(content=nodes), ... lambda nodes: Strikethrough(content=nodes) ... ) ... )
Notes
The function applies formatting layers from outermost to innermost based on the order of format flags. For example, with (bold=True, italic=True): - Text node is created - Wrapped in Emphasis (italic) - Wrapped in Strong (bold)
This matches the rendering order where bold appears before italic in markdown.
- all2md.utils.parser_helpers.parse_delimited_block(current_token_fn: Callable[[], Any], advance_fn: Callable[[], Any], opening_delimiter_type: Any, closing_delimiter_type: Any, eof_type: Any, collect_mode: str = 'lines', parse_block_fn: Callable[[], Any] | None = None) tuple[list[str] | list[Any], bool]
Parse a delimited block with opening and closing delimiters.
This helper consolidates the common pattern in AsciiDoc parser for parsing delimited blocks (code blocks, quote blocks, literal blocks, etc.) that have: - An opening delimiter line - Content (lines or blocks) - A closing delimiter line (or EOF)
- Parameters:
current_token_fn (Callable[[], Any]) – Function that returns the current token
advance_fn (Callable[[], Any]) – Function that advances to the next token and returns the previous one
opening_delimiter_type (Any) – Token type for the opening delimiter (not used, assumed already consumed)
closing_delimiter_type (Any) – Token type for the closing delimiter to watch for
eof_type (Any) – Token type representing end of file
collect_mode (str, default "lines") – Mode for collecting content: - “lines”: Collect text lines (returns list[str]) - “blocks”: Parse blocks using parse_block_fn (returns list[Node])
parse_block_fn (Callable[[], Any] | None, default None) – Function to parse individual blocks (required if collect_mode=”blocks”)
- Returns:
Tuple of (collected content, found_closing_delimiter) - content: List of strings (if mode=”lines”) or list of Nodes (if mode=”blocks”) - found_closing_delimiter: True if closing delimiter was found, False if EOF
- Return type:
tuple[list[str] | list[Any], bool]
- Raises:
ValueError – If collect_mode=”blocks” but parse_block_fn is not provided
Examples
Use with AsciiDoc parser for code blocks:
>>> content, found_closing = parse_delimited_block( ... current_token_fn=self._current_token, ... advance_fn=self._advance, ... opening_delimiter_type=TokenType.CODE_BLOCK_DELIMITER, ... closing_delimiter_type=TokenType.CODE_BLOCK_DELIMITER, ... eof_type=TokenType.EOF, ... collect_mode="lines" ... ) >>> code_content = '\\n'.join(content)
Use for quote blocks:
>>> children, found_closing = parse_delimited_block( ... current_token_fn=self._current_token, ... advance_fn=self._advance, ... opening_delimiter_type=TokenType.QUOTE_BLOCK_DELIMITER, ... closing_delimiter_type=TokenType.QUOTE_BLOCK_DELIMITER, ... eof_type=TokenType.EOF, ... collect_mode="blocks", ... parse_block_fn=self._parse_block ... )
Notes
This helper assumes the opening delimiter has already been consumed before calling this function. It will: 1. Collect content until closing delimiter or EOF 2. Consume the closing delimiter if found 3. Return the collected content and a flag indicating if delimiter was found