all2md.transforms.hooks

Hook system for AST transformation pipeline.

This module provides a flexible hook system for intercepting and modifying the transformation pipeline at various stages. Hooks can be registered for: - Pipeline stages (pre/post AST, render, etc.) - Specific node types (headings, images, links, etc.)

Examples

Register a hook for all images:

>>> from all2md.transforms import HookManager
>>> manager = HookManager()
>>>
>>> def log_image(node, context):
...     print(f"Found image: {node.url}")
...     return node
>>>
>>> manager.register_hook('image', log_image)

Register a pipeline hook:

>>> def validate_document(doc, context):
...     print(f"Processing document with {len(doc.children)} nodes")
...     return doc
>>>
>>> manager.register_hook('pre_render', validate_document)
class all2md.transforms.hooks.HookContext

Bases: object

Context passed to hook functions.

This class provides hooks with access to document state, metadata, and a shared data dictionary for passing information between hooks and transforms.

Parameters:
  • document (Document) – The current document being processed

  • metadata (dict, default = empty dict) – Document metadata from the source format

  • shared (dict, default = empty dict) – Shared mutable dictionary for passing data between hooks/transforms

  • transform_name (str, optional) – Name of the current transform (for transform hooks)

  • node_path (list[Node], default = empty list) – Path from document root to current node (for node hooks). WARNING: This list is mutated during tree traversal. Not thread-safe.

Examples

Access context in a hook:

>>> def my_hook(node: Image, context: HookContext) -> Image:
...     # Store image count in shared state
...     context.shared['image_count'] = context.shared.get('image_count', 0) + 1
...
...     # Access document metadata
...     if 'author' in context.metadata:
...         print(f"Document by: {context.metadata['author']}")
...
...     return node
document: Document
metadata: dict[str, Any]
shared: dict[str, Any]
transform_name: str | None = None
node_path: list[Node]
get_shared(key: str, default: Any = None) Any

Get a value from shared state.

Parameters:
  • key (str) – Key to retrieve

  • default (Any, optional) – Default value if key not found

Returns:

Value from shared state or default

Return type:

Any

set_shared(key: str, value: Any) None

Set a value in shared state.

Parameters:
  • key (str) – Key to set

  • value (Any) – Value to store

__init__(document: Document, metadata: dict[str, ~typing.Any]=<factory>, shared: dict[str, ~typing.Any]=<factory>, transform_name: str | None = None, node_path: list[Node] = <factory>) None
class all2md.transforms.hooks.HookManager

Bases: object

Manager for registering and executing hooks.

This class provides a central registry for hooks at various pipeline stages and for specific node types.

Parameters:

strict (bool, default = False) – If True, hook exceptions are re-raised and abort the pipeline. If False (default), exceptions are logged and execution continues.

Examples

Create a hook manager:
>>> manager = HookManager()
Create a strict hook manager:
>>> manager = HookManager(strict=True)
Register a pipeline hook:
>>> def pre_render_hook(doc, context):
...     print("About to render")
...     return doc
>>> manager.register_hook('pre_render', pre_render_hook)
Register a node hook:
>>> def image_hook(node, context):
...     print(f"Processing image: {node.url}")
...     return node
>>> manager.register_hook('image', image_hook)
Execute hooks:
>>> context = HookContext(document=my_doc)
>>> result = manager.execute_hooks('pre_render', my_doc, context)

Notes

In strict mode (strict=True), any exception raised by a hook will be re-raised and abort the pipeline. This is useful for debugging or when hook failures should be treated as critical errors.

In non-strict mode (strict=False, the default), exceptions are logged with full traceback but execution continues with subsequent hooks. This provides a fail-safe default that prevents a single problematic hook from breaking the entire pipeline.

Thread Safety

WARNING: HookManager instances are NOT thread-safe. Hook registration and execution use shared mutable state without synchronization.

For safe concurrent usage: - Create a separate HookManager instance per thread/pipeline (recommended) - Each Pipeline instance creates its own HookManager (default behavior) - If sharing across threads, wrap access with external locks (e.g., threading.Lock)

Initialize the hook manager.

param strict:

Enable strict mode for hook exception handling

type strict:

bool, default = False

__init__(strict: bool = False) None

Initialize the hook manager.

Parameters:

strict (bool, default = False) – Enable strict mode for hook exception handling

register_hook(target: Literal['post_ast', 'pre_transform', 'post_transform', 'pre_render', 'post_render', 'document', 'heading', 'paragraph', 'code_block', 'block_quote', 'list', 'list_item', 'table', 'table_row', 'table_cell', 'thematic_break', 'html_block', 'text', 'emphasis', 'strong', 'code', 'link', 'image', 'line_break', 'strikethrough', 'underline', 'superscript', 'subscript', 'html_inline', 'footnote_reference', 'footnote_definition', 'math_inline', 'math_block', 'definition_list', 'definition_term', 'definition_description'], hook: Callable[[Any, HookContext], Any], priority: int = 100) None

Register a hook for a target.

Parameters:
  • target (HookTarget) – Hook point or node type to hook into

  • hook (callable) – Hook function with signature: (obj, context) -> obj

  • priority (int, default = 100) – Execution priority (lower runs first)

Notes

Hooks for the same target are executed in priority order (lower first). If priorities are equal, hooks run in registration order.

Sorting is deferred until execution time for better performance when registering many hooks.

Examples

>>> manager = HookManager()
>>> manager.register_hook('image', my_image_hook, priority=50)
unregister_hook(target: Literal['post_ast', 'pre_transform', 'post_transform', 'pre_render', 'post_render', 'document', 'heading', 'paragraph', 'code_block', 'block_quote', 'list', 'list_item', 'table', 'table_row', 'table_cell', 'thematic_break', 'html_block', 'text', 'emphasis', 'strong', 'code', 'link', 'image', 'line_break', 'strikethrough', 'underline', 'superscript', 'subscript', 'html_inline', 'footnote_reference', 'footnote_definition', 'math_inline', 'math_block', 'definition_list', 'definition_term', 'definition_description'], hook: Callable[[Any, HookContext], Any]) bool

Unregister a hook.

Parameters:
  • target (HookTarget) – Hook point or node type

  • hook (callable) – Hook function to remove

Returns:

True if hook was found and removed

Return type:

bool

execute_hooks(target: Literal['post_ast', 'pre_transform', 'post_transform', 'pre_render', 'post_render', 'document', 'heading', 'paragraph', 'code_block', 'block_quote', 'list', 'list_item', 'table', 'table_row', 'table_cell', 'thematic_break', 'html_block', 'text', 'emphasis', 'strong', 'code', 'link', 'image', 'line_break', 'strikethrough', 'underline', 'superscript', 'subscript', 'html_inline', 'footnote_reference', 'footnote_definition', 'math_inline', 'math_block', 'definition_list', 'definition_term', 'definition_description'], obj: Any, context: HookContext) Any

Execute all hooks for a target.

Hooks are executed in priority order. Each hook receives the result from the previous hook. If a hook returns None, the object is removed (for node hooks).

Parameters:
  • target (HookTarget) – Hook point or node type

  • obj (Any) – Object to process (Document or Node)

  • context (HookContext) – Hook context

Returns:

Processed object (or None if removed by a hook)

Return type:

Any

Raises:

Exception – Any exception from hooks if strict mode is enabled

Examples

>>> context = HookContext(document=doc)
>>> result = manager.execute_hooks('image', image_node, context)

Notes

In strict mode, exceptions from hooks are re-raised and abort execution. In non-strict mode (default), exceptions are logged and execution continues.

Hooks are sorted by priority at execution time for better registration performance when many hooks are registered.

has_hooks(target: Literal['post_ast', 'pre_transform', 'post_transform', 'pre_render', 'post_render', 'document', 'heading', 'paragraph', 'code_block', 'block_quote', 'list', 'list_item', 'table', 'table_row', 'table_cell', 'thematic_break', 'html_block', 'text', 'emphasis', 'strong', 'code', 'link', 'image', 'line_break', 'strikethrough', 'underline', 'superscript', 'subscript', 'html_inline', 'footnote_reference', 'footnote_definition', 'math_inline', 'math_block', 'definition_list', 'definition_term', 'definition_description']) bool

Check if any hooks are registered for a target.

Parameters:

target (HookTarget) – Hook point or node type

Returns:

True if hooks are registered

Return type:

bool

static get_node_type(node: Node) Literal['document', 'heading', 'paragraph', 'code_block', 'block_quote', 'list', 'list_item', 'table', 'table_row', 'table_cell', 'thematic_break', 'html_block', 'text', 'emphasis', 'strong', 'code', 'link', 'image', 'line_break', 'strikethrough', 'underline', 'superscript', 'subscript', 'html_inline', 'footnote_reference', 'footnote_definition', 'math_inline', 'math_block', 'definition_list', 'definition_term', 'definition_description'] | None

Get the node type string for a node instance.

This static method supports subclasses by using isinstance checks rather than exact type matching. If a node is a subclass of a known type, it will be identified by its parent type.

Parameters:

node (Node) – AST node

Returns:

Node type string (e.g., ‘heading’, ‘image’), or None if unknown

Return type:

NodeType or None

Notes

The method iterates through known node types and returns the first match using isinstance checks. This allows custom subclasses to be recognized by their base type. For example, a custom MyImage(Image) subclass will be identified as type ‘image’.

Performance: Uses module-level _NODE_TYPE_MAP constant to avoid reconstructing the mapping on every call (hot path optimization).

This is a static method because it doesn’t depend on instance state, only on the module-level _NODE_TYPE_MAP constant. This allows it to be called without instantiating HookManager.

Examples

>>> from all2md.ast.nodes import Image
>>> img = Image(url="test.png", alt_text="Test")
>>> node_type = HookManager.get_node_type(img)
>>> print(node_type)
'image'
list_hooks() dict[Literal['post_ast', 'pre_transform', 'post_transform', 'pre_render', 'post_render', 'document', 'heading', 'paragraph', 'code_block', 'block_quote', 'list', 'list_item', 'table', 'table_row', 'table_cell', 'thematic_break', 'html_block', 'text', 'emphasis', 'strong', 'code', 'link', 'image', 'line_break', 'strikethrough', 'underline', 'superscript', 'subscript', 'html_inline', 'footnote_reference', 'footnote_definition', 'math_inline', 'math_block', 'definition_list', 'definition_term', 'definition_description'], list[tuple[int, Callable[[Any, HookContext], Any]]]]

List all registered hooks with their priorities.

This method provides a public API for enumerating hooks without exposing the internal _hooks dictionary structure.

Returns:

Dictionary mapping hook targets to lists of (priority, hook) tuples. The returned dictionary is a shallow copy to prevent external modifications to internal state.

Return type:

dict[HookTarget, list[tuple[int, HookCallable]]]

Examples

>>> manager = HookManager()
>>> manager.register_hook('image', my_hook, priority=50)
>>> hooks = manager.list_hooks()
>>> print(hooks)
{'image': [(50, <function my_hook>)]}
clear() None

Clear all registered hooks.

This is primarily useful for testing.