marktripy 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- marktripy/__init__.py +10 -0
- marktripy/_version.py +21 -0
- marktripy/core/__init__.py +7 -0
- marktripy/core/ast.py +274 -0
- marktripy/core/parser.py +199 -0
- marktripy/core/validator.py +266 -0
- marktripy/extensions/__init__.py +2 -0
- marktripy/extensions/base.py +247 -0
- marktripy/extensions/gfm.py +106 -0
- marktripy/extensions/kbd.py +153 -0
- marktripy/extensions/strikethrough.py +124 -0
- marktripy/extensions/tasklist.py +138 -0
- marktripy/parsers/__init__.py +2 -0
- marktripy/parsers/markdown_it.py +352 -0
- marktripy/parsers/mistletoe_parser.py +349 -0
- marktripy/renderers/__init__.py +2 -0
- marktripy/renderers/base.py +214 -0
- marktripy/renderers/html.py +366 -0
- marktripy/renderers/markdown.py +446 -0
- marktripy/transformers/__init__.py +2 -0
- marktripy/transformers/base.py +226 -0
- marktripy/transformers/heading.py +217 -0
- marktripy/transformers/id_generator.py +217 -0
- marktripy/transformers/link_reference.py +258 -0
- marktripy/transformers/toc.py +299 -0
- marktripy/utils/__init__.py +2 -0
- marktripy/utils/slugify.py +177 -0
- marktripy-1.0.3.dist-info/METADATA +320 -0
- marktripy-1.0.3.dist-info/RECORD +31 -0
- marktripy-1.0.3.dist-info/WHEEL +4 -0
- marktripy-1.0.3.dist-info/entry_points.txt +2 -0
marktripy/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# this_file: marktripy/__init__.py
|
|
2
|
+
"""marktripy: A Python package for converting Markdown to AST and back to Markdown."""
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
__author__ = "Adam"
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
# Configure default logger
|
|
10
|
+
logger.disable("marktripy") # Disabled by default, users can enable
|
marktripy/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '1.0.3'
|
|
21
|
+
__version_tuple__ = version_tuple = (1, 0, 3)
|
marktripy/core/ast.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# this_file: marktripy/core/ast.py
|
|
2
|
+
"""Abstract Syntax Tree node definitions for marktripy."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ASTNode(ABC):
|
|
15
|
+
"""Base class for all AST nodes.
|
|
16
|
+
|
|
17
|
+
This represents a node in the Markdown abstract syntax tree. Each node
|
|
18
|
+
has a type, optional children, attributes, content, and metadata.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
type: The type of node (e.g., 'heading', 'paragraph', 'list')
|
|
22
|
+
children: List of child nodes
|
|
23
|
+
attrs: Dictionary of attributes (e.g., 'id', 'class')
|
|
24
|
+
content: Optional text content for leaf nodes
|
|
25
|
+
meta: Parser-specific metadata (e.g., source position)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
type: str
|
|
29
|
+
children: list[ASTNode] = field(default_factory=list)
|
|
30
|
+
attrs: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
content: str | None = None
|
|
32
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
"""Validate node after initialization."""
|
|
36
|
+
if not self.type:
|
|
37
|
+
raise ValueError("Node type cannot be empty")
|
|
38
|
+
logger.debug(f"Created ASTNode: type={self.type}, children={len(self.children)}")
|
|
39
|
+
|
|
40
|
+
def add_child(self, child: ASTNode) -> None:
|
|
41
|
+
"""Add a child node."""
|
|
42
|
+
self.children.append(child)
|
|
43
|
+
logger.debug(f"Added child {child.type} to {self.type}")
|
|
44
|
+
|
|
45
|
+
def remove_child(self, child: ASTNode) -> None:
|
|
46
|
+
"""Remove a child node."""
|
|
47
|
+
self.children.remove(child)
|
|
48
|
+
logger.debug(f"Removed child {child.type} from {self.type}")
|
|
49
|
+
|
|
50
|
+
def get_attr(self, name: str, default: Any = None) -> Any:
|
|
51
|
+
"""Get an attribute value."""
|
|
52
|
+
return self.attrs.get(name, default)
|
|
53
|
+
|
|
54
|
+
def set_attr(self, name: str, value: Any) -> None:
|
|
55
|
+
"""Set an attribute value."""
|
|
56
|
+
self.attrs[name] = value
|
|
57
|
+
logger.debug(f"Set attribute {name}={value} on {self.type}")
|
|
58
|
+
|
|
59
|
+
def walk(self) -> list[ASTNode]:
|
|
60
|
+
"""Walk the tree and return all nodes in depth-first order."""
|
|
61
|
+
nodes = [self]
|
|
62
|
+
for child in self.children:
|
|
63
|
+
nodes.extend(child.walk())
|
|
64
|
+
return nodes
|
|
65
|
+
|
|
66
|
+
def find_all(self, node_type: str) -> list[ASTNode]:
|
|
67
|
+
"""Find all nodes of a specific type in the subtree."""
|
|
68
|
+
return [node for node in self.walk() if node.type == node_type]
|
|
69
|
+
|
|
70
|
+
def replace_child(self, old_child: ASTNode, new_child: ASTNode) -> None:
|
|
71
|
+
"""Replace a child node with another."""
|
|
72
|
+
try:
|
|
73
|
+
index = self.children.index(old_child)
|
|
74
|
+
self.children[index] = new_child
|
|
75
|
+
logger.debug(f"Replaced child {old_child.type} with {new_child.type}")
|
|
76
|
+
except ValueError as e:
|
|
77
|
+
raise ValueError(f"Child {old_child.type} not found in {self.type}") from e
|
|
78
|
+
|
|
79
|
+
def clone(self) -> ASTNode:
|
|
80
|
+
"""Create a deep copy of this node and its subtree."""
|
|
81
|
+
import copy
|
|
82
|
+
|
|
83
|
+
# Create a shallow copy of the node
|
|
84
|
+
cloned = copy.copy(self)
|
|
85
|
+
# Deep copy the mutable attributes
|
|
86
|
+
cloned.children = [child.clone() for child in self.children]
|
|
87
|
+
cloned.attrs = self.attrs.copy()
|
|
88
|
+
cloned.meta = self.meta.copy()
|
|
89
|
+
return cloned
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Concrete node types
|
|
93
|
+
@dataclass
|
|
94
|
+
class Document(ASTNode):
|
|
95
|
+
"""Root document node."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, **kwargs):
|
|
98
|
+
super().__init__(type="document", **kwargs)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class Heading(ASTNode):
|
|
103
|
+
"""Heading node with level."""
|
|
104
|
+
|
|
105
|
+
level: int = 1
|
|
106
|
+
|
|
107
|
+
def __init__(self, level: int = 1, **kwargs):
|
|
108
|
+
super().__init__(type="heading", **kwargs)
|
|
109
|
+
self.level = level
|
|
110
|
+
if not 1 <= level <= 6:
|
|
111
|
+
raise ValueError(f"Heading level must be 1-6, got {level}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class Paragraph(ASTNode):
|
|
116
|
+
"""Paragraph node."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, **kwargs):
|
|
119
|
+
super().__init__(type="paragraph", **kwargs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class Text(ASTNode):
|
|
124
|
+
"""Text leaf node."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, content: str, **kwargs):
|
|
127
|
+
super().__init__(type="text", content=content, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class Emphasis(ASTNode):
|
|
132
|
+
"""Emphasis (italic) node."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, **kwargs):
|
|
135
|
+
super().__init__(type="emphasis", **kwargs)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class Strong(ASTNode):
|
|
140
|
+
"""Strong (bold) node."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, **kwargs):
|
|
143
|
+
super().__init__(type="strong", **kwargs)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class Link(ASTNode):
|
|
148
|
+
"""Link node with href and title."""
|
|
149
|
+
|
|
150
|
+
href: str = ""
|
|
151
|
+
title: str | None = None
|
|
152
|
+
|
|
153
|
+
def __init__(self, href: str = "", title: str | None = None, **kwargs):
|
|
154
|
+
super().__init__(type="link", **kwargs)
|
|
155
|
+
self.href = href
|
|
156
|
+
self.title = title
|
|
157
|
+
self.set_attr("href", href)
|
|
158
|
+
if title:
|
|
159
|
+
self.set_attr("title", title)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class Image(ASTNode):
|
|
164
|
+
"""Image node with src, alt, and title."""
|
|
165
|
+
|
|
166
|
+
src: str = ""
|
|
167
|
+
alt: str = ""
|
|
168
|
+
title: str | None = None
|
|
169
|
+
|
|
170
|
+
def __init__(self, src: str = "", alt: str = "", title: str | None = None, **kwargs):
|
|
171
|
+
super().__init__(type="image", **kwargs)
|
|
172
|
+
self.src = src
|
|
173
|
+
self.alt = alt
|
|
174
|
+
self.title = title
|
|
175
|
+
self.set_attr("src", src)
|
|
176
|
+
self.set_attr("alt", alt)
|
|
177
|
+
if title:
|
|
178
|
+
self.set_attr("title", title)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class CodeBlock(ASTNode):
|
|
183
|
+
"""Code block with optional language."""
|
|
184
|
+
|
|
185
|
+
language: str | None = None
|
|
186
|
+
|
|
187
|
+
def __init__(self, content: str, language: str | None = None, **kwargs):
|
|
188
|
+
super().__init__(type="code_block", content=content, **kwargs)
|
|
189
|
+
self.language = language
|
|
190
|
+
if language:
|
|
191
|
+
self.set_attr("language", language)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class InlineCode(ASTNode):
|
|
196
|
+
"""Inline code node."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, content: str, **kwargs):
|
|
199
|
+
super().__init__(type="inline_code", content=content, **kwargs)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class List(ASTNode):
|
|
204
|
+
"""List node (ordered or unordered)."""
|
|
205
|
+
|
|
206
|
+
ordered: bool = False
|
|
207
|
+
start: int | None = None
|
|
208
|
+
tight: bool = True
|
|
209
|
+
|
|
210
|
+
def __init__(
|
|
211
|
+
self, ordered: bool = False, start: int | None = None, tight: bool = True, **kwargs
|
|
212
|
+
):
|
|
213
|
+
super().__init__(type="list", **kwargs)
|
|
214
|
+
self.ordered = ordered
|
|
215
|
+
self.start = start
|
|
216
|
+
self.tight = tight
|
|
217
|
+
# Don't set ordered as an HTML attribute - it's determined by tag type
|
|
218
|
+
if start is not None and start != 1:
|
|
219
|
+
self.set_attr("start", start)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class ListItem(ASTNode):
|
|
224
|
+
"""List item node."""
|
|
225
|
+
|
|
226
|
+
def __init__(self, **kwargs):
|
|
227
|
+
super().__init__(type="list_item", **kwargs)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class BlockQuote(ASTNode):
|
|
232
|
+
"""Block quote node."""
|
|
233
|
+
|
|
234
|
+
def __init__(self, **kwargs):
|
|
235
|
+
super().__init__(type="blockquote", **kwargs)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class HorizontalRule(ASTNode):
|
|
240
|
+
"""Horizontal rule node."""
|
|
241
|
+
|
|
242
|
+
def __init__(self, **kwargs):
|
|
243
|
+
super().__init__(type="horizontal_rule", **kwargs)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class Table(ASTNode):
|
|
248
|
+
"""Table node."""
|
|
249
|
+
|
|
250
|
+
def __init__(self, **kwargs):
|
|
251
|
+
super().__init__(type="table", **kwargs)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class TableRow(ASTNode):
|
|
256
|
+
"""Table row node."""
|
|
257
|
+
|
|
258
|
+
def __init__(self, **kwargs):
|
|
259
|
+
super().__init__(type="table_row", **kwargs)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class TableCell(ASTNode):
|
|
264
|
+
"""Table cell node."""
|
|
265
|
+
|
|
266
|
+
header: bool = False
|
|
267
|
+
align: str | None = None
|
|
268
|
+
|
|
269
|
+
def __init__(self, header: bool = False, align: str | None = None, **kwargs):
|
|
270
|
+
super().__init__(type="table_cell", **kwargs)
|
|
271
|
+
self.header = header
|
|
272
|
+
self.align = align
|
|
273
|
+
if align:
|
|
274
|
+
self.set_attr("align", align)
|
marktripy/core/parser.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# this_file: marktripy/core/parser.py
|
|
2
|
+
"""Abstract parser interface for marktripy."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from marktripy.core.ast import Document
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Parser(ABC):
|
|
15
|
+
"""Abstract base class for Markdown parsers.
|
|
16
|
+
|
|
17
|
+
This interface defines the contract that all parser implementations
|
|
18
|
+
must follow. Parsers convert Markdown text into an AST representation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
22
|
+
"""Initialize parser with optional configuration.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config: Parser-specific configuration options
|
|
26
|
+
"""
|
|
27
|
+
self.config = config or {}
|
|
28
|
+
logger.debug(f"Initialized {self.__class__.__name__} with config: {self.config}")
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def parse(self, text: str) -> Document:
|
|
32
|
+
"""Parse Markdown text into an AST.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
text: The Markdown text to parse
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The root Document node of the AST
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def get_capabilities(self) -> dict[str, bool]:
|
|
43
|
+
"""Get parser capabilities.
|
|
44
|
+
|
|
45
|
+
Returns a dictionary indicating which features this parser supports.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dictionary with capability flags like:
|
|
49
|
+
- 'tables': Table support
|
|
50
|
+
- 'strikethrough': Strikethrough syntax
|
|
51
|
+
- 'task_lists': Task list checkboxes
|
|
52
|
+
- 'footnotes': Footnote references
|
|
53
|
+
- 'definition_lists': Definition list syntax
|
|
54
|
+
- 'math': Math expressions
|
|
55
|
+
- 'smart_quotes': Smart typography
|
|
56
|
+
- 'custom_extensions': Custom extension support
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def validate_markdown(self, text: str) -> list[str]:
|
|
60
|
+
"""Validate Markdown syntax and return any errors.
|
|
61
|
+
|
|
62
|
+
Default implementation just tries to parse and catches exceptions.
|
|
63
|
+
Subclasses can override for more detailed validation.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
text: The Markdown text to validate
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of validation error messages (empty if valid)
|
|
70
|
+
"""
|
|
71
|
+
errors = []
|
|
72
|
+
try:
|
|
73
|
+
self.parse(text)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
errors.append(str(e))
|
|
76
|
+
return errors
|
|
77
|
+
|
|
78
|
+
def preprocess(self, text: str) -> str:
|
|
79
|
+
"""Preprocess Markdown text before parsing.
|
|
80
|
+
|
|
81
|
+
Can be overridden by subclasses to normalize text, handle
|
|
82
|
+
special cases, or apply transformations.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
text: Raw Markdown text
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Preprocessed text ready for parsing
|
|
89
|
+
"""
|
|
90
|
+
# Normalize line endings
|
|
91
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
92
|
+
|
|
93
|
+
# Ensure text ends with newline
|
|
94
|
+
if text and not text.endswith("\n"):
|
|
95
|
+
text += "\n"
|
|
96
|
+
|
|
97
|
+
logger.debug(f"Preprocessed text: {len(text)} characters")
|
|
98
|
+
return text
|
|
99
|
+
|
|
100
|
+
def postprocess(self, ast: Document) -> Document:
|
|
101
|
+
"""Postprocess AST after parsing.
|
|
102
|
+
|
|
103
|
+
Can be overridden by subclasses to apply transformations,
|
|
104
|
+
validate the AST structure, or enrich nodes with metadata.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
ast: The parsed AST
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The postprocessed AST
|
|
111
|
+
"""
|
|
112
|
+
logger.debug(f"Postprocessed AST with {len(ast.children)} top-level nodes")
|
|
113
|
+
return ast
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ParserError(Exception):
|
|
117
|
+
"""Base exception for parser errors."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, message: str, line: int | None = None, column: int | None = None):
|
|
120
|
+
"""Initialize parser error.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
message: Error description
|
|
124
|
+
line: Line number where error occurred (1-based)
|
|
125
|
+
column: Column number where error occurred (1-based)
|
|
126
|
+
"""
|
|
127
|
+
self.line = line
|
|
128
|
+
self.column = column
|
|
129
|
+
|
|
130
|
+
if line is not None and column is not None:
|
|
131
|
+
full_message = f"{message} at line {line}, column {column}"
|
|
132
|
+
elif line is not None:
|
|
133
|
+
full_message = f"{message} at line {line}"
|
|
134
|
+
else:
|
|
135
|
+
full_message = message
|
|
136
|
+
|
|
137
|
+
super().__init__(full_message)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ParserRegistry:
|
|
141
|
+
"""Registry for available parser implementations."""
|
|
142
|
+
|
|
143
|
+
_parsers: dict[str, type[Parser]] = {}
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def register(cls, name: str, parser_class: type[Parser]) -> None:
|
|
147
|
+
"""Register a parser implementation.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Name to register the parser under
|
|
151
|
+
parser_class: Parser class to register
|
|
152
|
+
"""
|
|
153
|
+
if not issubclass(parser_class, Parser):
|
|
154
|
+
raise TypeError(f"{parser_class} must be a subclass of Parser")
|
|
155
|
+
|
|
156
|
+
cls._parsers[name] = parser_class
|
|
157
|
+
logger.info(f"Registered parser: {name} -> {parser_class.__name__}")
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def get(cls, name: str) -> type[Parser]:
|
|
161
|
+
"""Get a registered parser class.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: Name of the parser to retrieve
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The parser class
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
KeyError: If parser name is not registered
|
|
171
|
+
"""
|
|
172
|
+
if name not in cls._parsers:
|
|
173
|
+
raise KeyError(
|
|
174
|
+
f"Parser '{name}' not registered. Available: {list(cls._parsers.keys())}"
|
|
175
|
+
)
|
|
176
|
+
return cls._parsers[name]
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def create(cls, name: str, config: dict[str, Any] | None = None) -> Parser:
|
|
180
|
+
"""Create a parser instance.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
name: Name of the parser to create
|
|
184
|
+
config: Parser configuration
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Parser instance
|
|
188
|
+
"""
|
|
189
|
+
parser_class = cls.get(name)
|
|
190
|
+
return parser_class(config)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def list_parsers(cls) -> list[str]:
|
|
194
|
+
"""List all registered parser names.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of parser names
|
|
198
|
+
"""
|
|
199
|
+
return list(cls._parsers.keys())
|