wenmode 0.1.0__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.
Files changed (60) hide show
  1. wenmode/__init__.py +17 -0
  2. wenmode/directives/__init__.py +9 -0
  3. wenmode/directives/abbr.py +27 -0
  4. wenmode/directives/admonition.py +36 -0
  5. wenmode/directives/details.py +32 -0
  6. wenmode/directives/figure.py +34 -0
  7. wenmode/directives/toc.py +60 -0
  8. wenmode/directives/util.py +15 -0
  9. wenmode/headings.py +110 -0
  10. wenmode/nodes.py +358 -0
  11. wenmode/parser.py +440 -0
  12. wenmode/presets.py +101 -0
  13. wenmode/py.typed +1 -0
  14. wenmode/renderers/__init__.py +18 -0
  15. wenmode/renderers/base.py +103 -0
  16. wenmode/renderers/html.py +532 -0
  17. wenmode/renderers/markdown.py +421 -0
  18. wenmode/renderers/rst.py +469 -0
  19. wenmode/rules/__init__.py +79 -0
  20. wenmode/rules/base.py +106 -0
  21. wenmode/rules/blocks/__init__.py +30 -0
  22. wenmode/rules/blocks/abbr.py +142 -0
  23. wenmode/rules/blocks/blockquote.py +87 -0
  24. wenmode/rules/blocks/definition_list.py +115 -0
  25. wenmode/rules/blocks/directive.py +153 -0
  26. wenmode/rules/blocks/fenced_code.py +73 -0
  27. wenmode/rules/blocks/heading.py +115 -0
  28. wenmode/rules/blocks/html.py +164 -0
  29. wenmode/rules/blocks/indented_code.py +63 -0
  30. wenmode/rules/blocks/list.py +258 -0
  31. wenmode/rules/blocks/math.py +49 -0
  32. wenmode/rules/blocks/spoiler.py +48 -0
  33. wenmode/rules/blocks/table.py +142 -0
  34. wenmode/rules/blocks/thematic_break.py +34 -0
  35. wenmode/rules/blocks/util.py +38 -0
  36. wenmode/rules/directives.py +136 -0
  37. wenmode/rules/footnotes.py +188 -0
  38. wenmode/rules/inlines/__init__.py +30 -0
  39. wenmode/rules/inlines/code.py +45 -0
  40. wenmode/rules/inlines/directive.py +92 -0
  41. wenmode/rules/inlines/emphasis.py +268 -0
  42. wenmode/rules/inlines/extended_autolink.py +195 -0
  43. wenmode/rules/inlines/formatting.py +139 -0
  44. wenmode/rules/inlines/html.py +95 -0
  45. wenmode/rules/inlines/link.py +350 -0
  46. wenmode/rules/inlines/math.py +83 -0
  47. wenmode/rules/inlines/ruby.py +82 -0
  48. wenmode/rules/inlines/spoiler.py +32 -0
  49. wenmode/rules/inlines/strikethrough.py +57 -0
  50. wenmode/rules/inlines/text.py +106 -0
  51. wenmode/rules/references.py +209 -0
  52. wenmode/rules/transforms.py +31 -0
  53. wenmode/state.py +193 -0
  54. wenmode/toc.py +72 -0
  55. wenmode/utils.py +80 -0
  56. wenmode/wenmode.py +102 -0
  57. wenmode-0.1.0.dist-info/METADATA +269 -0
  58. wenmode-0.1.0.dist-info/RECORD +60 -0
  59. wenmode-0.1.0.dist-info/WHEEL +4 -0
  60. wenmode-0.1.0.dist-info/licenses/LICENSE +28 -0
wenmode/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ from .parser import Parser, StreamingUnsupportedError
2
+ from .renderers import HTMLRenderer, MarkdownRenderer, RSTRenderer
3
+ from .wenmode import Wenmode
4
+
5
+ __version__ = '0.1.0'
6
+ __homepage__ = 'https://wenmode.lepture.com/'
7
+ __author__ = 'Hsiaoming Yang <me@lepture.com>'
8
+ __license__ = 'BSD-3-Clause'
9
+
10
+ __all__ = [
11
+ 'HTMLRenderer',
12
+ 'MarkdownRenderer',
13
+ 'RSTRenderer',
14
+ 'Parser',
15
+ 'StreamingUnsupportedError',
16
+ 'Wenmode',
17
+ ]
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from .abbr import Abbreviation
4
+ from .admonition import Admonition
5
+ from .details import Details
6
+ from .figure import Figure
7
+ from .toc import TableOfContents
8
+
9
+ __all__ = ['Abbreviation', 'Admonition', 'Details', 'Figure', 'TableOfContents']
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from wenmode.nodes import TextDirective
7
+
8
+ if TYPE_CHECKING:
9
+ from wenmode.renderers.html import HTMLRenderContext, HTMLRenderer
10
+
11
+
12
+ class Abbreviation:
13
+ """Render text directives as HTML ``abbr`` elements.
14
+
15
+ :param names: Directive names handled by this renderer.
16
+ """
17
+
18
+ node_type = 'textDirective'
19
+
20
+ def __init__(self, names: Iterable[str] = ('abbr',)) -> None:
21
+ self.names = frozenset(names)
22
+
23
+ def render(self, renderer: HTMLRenderer, node: TextDirective, context: HTMLRenderContext) -> str:
24
+ attributes = dict(node.attributes or {})
25
+ if 'title' not in attributes:
26
+ return renderer.render_children(node.children, context)
27
+ return f'<abbr{renderer.render_attrs(attributes)}>{renderer.render_children(node.children, context)}</abbr>'
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from wenmode.nodes import ContainerDirective
7
+
8
+ from .util import append_class, split_directive_label
9
+
10
+ if TYPE_CHECKING:
11
+ from wenmode.renderers.html import HTMLRenderContext, HTMLRenderer
12
+
13
+
14
+ class Admonition:
15
+ """Render container directives as admonition ``aside`` elements.
16
+
17
+ :param names: Directive names handled by this renderer.
18
+ """
19
+
20
+ node_type = 'containerDirective'
21
+
22
+ def __init__(self, names: Iterable[str] = ('note', 'tip', 'caution', 'danger')) -> None:
23
+ self.names = frozenset(names)
24
+
25
+ def render(self, renderer: HTMLRenderer, node: ContainerDirective, context: HTMLRenderContext) -> str:
26
+ label, children = split_directive_label(node)
27
+ class_name = f'admonition admonition-{node.name}'
28
+ attrs = dict(node.attributes or {})
29
+ attrs['class'] = append_class(attrs.get('class'), class_name)
30
+
31
+ parts = [f'<aside{renderer.render_attrs(attrs)}>\n']
32
+ if label is not None:
33
+ parts.append(f'<p class="admonition-title">{renderer.render_children(label.children, context)}</p>\n')
34
+ parts.append(renderer.render_children(children, context))
35
+ parts.append('</aside>\n')
36
+ return ''.join(parts)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from wenmode.nodes import ContainerDirective
7
+
8
+ from .util import split_directive_label
9
+
10
+ if TYPE_CHECKING:
11
+ from wenmode.renderers.html import HTMLRenderContext, HTMLRenderer
12
+
13
+
14
+ class Details:
15
+ """Render ``details`` container directives as HTML ``details`` elements.
16
+
17
+ :param names: Directive names handled by this renderer.
18
+ """
19
+
20
+ node_type = 'containerDirective'
21
+
22
+ def __init__(self, names: Iterable[str] = ('details',)) -> None:
23
+ self.names = frozenset(names)
24
+
25
+ def render(self, renderer: HTMLRenderer, node: ContainerDirective, context: HTMLRenderContext) -> str:
26
+ label, children = split_directive_label(node)
27
+ parts = [f'<details{renderer.render_attrs(node.attributes or {})}>\n']
28
+ if label is not None:
29
+ parts.append(f'<summary>{renderer.render_children(label.children, context)}</summary>\n')
30
+ parts.append(renderer.render_children(children, context))
31
+ parts.append('</details>\n')
32
+ return ''.join(parts)
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from wenmode.nodes import ContainerDirective
7
+
8
+ from .util import split_directive_label
9
+
10
+ if TYPE_CHECKING:
11
+ from wenmode.renderers.html import HTMLRenderContext, HTMLRenderer
12
+
13
+
14
+ class Figure:
15
+ """Render ``figure`` container directives as HTML ``figure`` elements.
16
+
17
+ :param names: Directive names handled by this renderer.
18
+ """
19
+
20
+ node_type = 'containerDirective'
21
+
22
+ def __init__(self, names: Iterable[str] = ('figure',)) -> None:
23
+ self.names = frozenset(names)
24
+
25
+ def render(self, renderer: HTMLRenderer, node: ContainerDirective, context: HTMLRenderContext) -> str:
26
+ label, children = split_directive_label(node)
27
+ parts = [
28
+ f'<figure{renderer.render_attrs(node.attributes or {})}>\n',
29
+ renderer.render_children(children, context),
30
+ ]
31
+ if label is not None:
32
+ parts.append(f'<figcaption>{renderer.render_children(label.children, context)}</figcaption>\n')
33
+ parts.append('</figure>\n')
34
+ return ''.join(parts)
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from wenmode.headings import plain_text
7
+ from wenmode.nodes import LeafDirective
8
+ from wenmode.toc import collect_toc, render_toc_list
9
+
10
+ from .util import append_class
11
+
12
+ if TYPE_CHECKING:
13
+ from wenmode.renderers.html import HTMLRenderContext, HTMLRenderer
14
+
15
+
16
+ class TableOfContents:
17
+ """Render ``toc`` leaf directives as HTML tables of contents.
18
+
19
+ :param names: Directive names handled by this renderer.
20
+ """
21
+
22
+ node_type = 'leafDirective'
23
+
24
+ def __init__(self, names: Iterable[str] = ('toc',)) -> None:
25
+ self.names = frozenset(names)
26
+
27
+ def render(self, renderer: HTMLRenderer, node: LeafDirective, context: HTMLRenderContext) -> str:
28
+ if context.root is None:
29
+ return ''
30
+
31
+ attributes = dict(node.attributes or {})
32
+ min_depth = parse_depth(attributes, ('min', 'min-depth', 'min_depth'), 1)
33
+ max_depth = parse_depth(attributes, ('max', 'max-depth', 'max_depth'), 6)
34
+ label_attribute = attributes.pop('label') if 'label' in attributes else None
35
+ label = plain_text(node.children) or label_attribute or 'Table of contents'
36
+ items = collect_toc(context.root, min_depth=min_depth, max_depth=max_depth)
37
+ if not items:
38
+ return ''
39
+
40
+ attributes.pop('min', None)
41
+ attributes.pop('min-depth', None)
42
+ attributes.pop('min_depth', None)
43
+ attributes.pop('max', None)
44
+ attributes.pop('max-depth', None)
45
+ attributes.pop('max_depth', None)
46
+ attributes['aria-label'] = label
47
+ attributes['class'] = append_class(attributes.get('class'), 'toc')
48
+ return f'<nav{renderer.render_attrs(attributes)}>\n{render_toc_list(items)}</nav>\n'
49
+
50
+
51
+ def parse_depth(attributes: dict[str, str], keys: tuple[str, ...], default: int) -> int:
52
+ for key in keys:
53
+ value = attributes.get(key)
54
+ if value is None:
55
+ continue
56
+ try:
57
+ return max(1, min(6, int(value)))
58
+ except ValueError:
59
+ return default
60
+ return default
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from wenmode.nodes import ContainerDirective, Node, Paragraph
4
+
5
+
6
+ def split_directive_label(node: ContainerDirective) -> tuple[Paragraph | None, list[Node]]:
7
+ """Split a container directive into its label paragraph and body nodes."""
8
+ if node.children and isinstance(node.children[0], Paragraph) and node.children[0].data == {'directiveLabel': True}:
9
+ return node.children[0], node.children[1:]
10
+ return None, node.children
11
+
12
+
13
+ def append_class(current: str | None, value: str) -> str:
14
+ """Append one CSS class value to an existing class attribute."""
15
+ return f'{current} {value}' if current else value
wenmode/headings.py ADDED
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import string
5
+
6
+ from .nodes import Heading, Image, Literal, Node, Parent
7
+
8
+ SLUG_PUNCTUATION = ''.join(char for char in string.punctuation if char not in '-_')
9
+ SLUG_PUNCTUATION_RE = re.compile('[' + re.escape(SLUG_PUNCTUATION) + ']')
10
+ SLUG_SPACE_RE = re.compile(r'\s+')
11
+
12
+
13
+ class Slugger:
14
+ """Generate unique slug IDs for headings."""
15
+
16
+ name = 'default'
17
+
18
+ def __init__(self) -> None:
19
+ self.seen: dict[str, int] = {}
20
+
21
+ def slug(self, value: str) -> str:
22
+ """Return a unique slug for a heading title."""
23
+ base = slugify(value)
24
+ index = self.seen.get(base, 0)
25
+ self.seen[base] = index + 1
26
+ if index == 0:
27
+ return base
28
+ return f'{base}-{index}'
29
+
30
+ def use(self, value: str) -> None:
31
+ """Mark an existing slug as already used."""
32
+ self.seen[value] = self.seen.get(value, 0) + 1
33
+
34
+
35
+ def add_heading_ids(
36
+ node: Node,
37
+ *,
38
+ slugger: Slugger,
39
+ min_depth: int = 1,
40
+ max_depth: int = 6,
41
+ overwrite: bool = False,
42
+ ) -> None:
43
+ """Add generated IDs to heading nodes in a tree.
44
+
45
+ Existing heading IDs are preserved unless ``overwrite`` is ``True``.
46
+
47
+ :param node: Root or subtree to update.
48
+ :param slugger: Slug generator used to create unique IDs.
49
+ :param min_depth: Minimum heading depth to update.
50
+ :param max_depth: Maximum heading depth to update.
51
+ :param overwrite: Whether to replace existing heading IDs.
52
+ """
53
+ for heading in iter_headings(node):
54
+ if not (min_depth <= heading.depth <= max_depth):
55
+ continue
56
+ current_id = heading.data.get('id') if heading.data else None
57
+ if isinstance(current_id, str) and not overwrite:
58
+ slugger.use(current_id)
59
+ continue
60
+ if heading.data is None:
61
+ heading.data = {}
62
+ heading.data['id'] = slugger.slug(plain_text(heading.children))
63
+
64
+
65
+ def iter_headings(node: Node) -> list[Heading]:
66
+ """Return all heading nodes under a node."""
67
+ headings: list[Heading] = []
68
+ collect_headings(node, headings)
69
+ return headings
70
+
71
+
72
+ def collect_headings(node: Node, headings: list[Heading]) -> None:
73
+ """Append heading descendants of ``node`` to ``headings``."""
74
+ if isinstance(node, Heading):
75
+ headings.append(node)
76
+ children = getattr(node, 'children', None)
77
+ if isinstance(children, list):
78
+ for child in children:
79
+ collect_headings(child, headings)
80
+
81
+
82
+ def plain_text(nodes: list[Node]) -> str:
83
+ """Return the plain text content of a node list."""
84
+ return ''.join(plain_text_node(node) for node in nodes)
85
+
86
+
87
+ def plain_text_node(node: Node) -> str:
88
+ """Return the plain text content of one node."""
89
+ if isinstance(node, Image):
90
+ return node.alt
91
+ if isinstance(node, Literal):
92
+ return node.value
93
+ if isinstance(node, Parent):
94
+ return plain_text(node.children)
95
+ label = getattr(node, 'label', None)
96
+ if isinstance(label, str):
97
+ return label
98
+ identifier = getattr(node, 'identifier', None)
99
+ if isinstance(identifier, str):
100
+ return identifier
101
+ return ''
102
+
103
+
104
+ def slugify(value: str) -> str:
105
+ """Convert text into a URL-friendly slug."""
106
+ slug = value.strip().lower()
107
+ slug = SLUG_PUNCTUATION_RE.sub('', slug)
108
+ slug = SLUG_SPACE_RE.sub('-', slug)
109
+ slug = slug.strip('-')
110
+ return slug or 'section'
wenmode/nodes.py ADDED
@@ -0,0 +1,358 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Node:
9
+ """Base class for all Wenmode AST nodes.
10
+
11
+ :param type: mdast-compatible node type name.
12
+ :param data: Optional extension data used by transforms or renderers.
13
+ """
14
+
15
+ type: str
16
+ data: dict[str, Any] | None = None
17
+
18
+ def to_ast(self) -> dict[str, Any]:
19
+ """Convert this node and its children to plain Python data.
20
+
21
+ :returns: A dictionary made from strings, numbers, lists, and nested
22
+ dictionaries.
23
+ """
24
+ data: dict[str, Any] = {'type': self.type}
25
+ for key, value in self.__dict__.items():
26
+ if key == 'type' or key.startswith('_') or value is None:
27
+ continue
28
+ if isinstance(value, list):
29
+ data[key] = [item.to_ast() if isinstance(item, Node) else item for item in value]
30
+ elif isinstance(value, Node):
31
+ data[key] = value.to_ast()
32
+ else:
33
+ data[key] = value
34
+ return data
35
+
36
+
37
+ @dataclass
38
+ class Parent(Node):
39
+ """Base class for nodes that contain child nodes."""
40
+
41
+ children: list[Node] = field(default_factory=list)
42
+
43
+
44
+ @dataclass
45
+ class Literal(Node):
46
+ """Base class for nodes that store literal text."""
47
+
48
+ value: str = ''
49
+
50
+
51
+ @dataclass
52
+ class Root(Parent):
53
+ """Document root node."""
54
+
55
+ _footnote_definitions: dict[str, FootnoteDefinition] | None = field(default=None, repr=False)
56
+ type: str = 'root'
57
+
58
+ @property
59
+ def footnote_definitions(self) -> dict[str, FootnoteDefinition] | None:
60
+ """Collected footnote definitions, if the footnote transform ran."""
61
+ return self._footnote_definitions
62
+
63
+ @footnote_definitions.setter
64
+ def footnote_definitions(self, definitions: dict[str, FootnoteDefinition] | None) -> None:
65
+ self._footnote_definitions = definitions
66
+
67
+
68
+ @dataclass
69
+ class Paragraph(Parent):
70
+ """Paragraph node."""
71
+
72
+ type: str = 'paragraph'
73
+
74
+
75
+ @dataclass
76
+ class Heading(Parent):
77
+ """Heading node.
78
+
79
+ :param depth: Heading depth from 1 through 6.
80
+ """
81
+
82
+ depth: int = 1
83
+ type: str = 'heading'
84
+
85
+
86
+ @dataclass
87
+ class Blockquote(Parent):
88
+ """Block quote container node."""
89
+
90
+ type: str = 'blockquote'
91
+
92
+
93
+ @dataclass
94
+ class BlockSpoiler(Parent):
95
+ """Block spoiler container node."""
96
+
97
+ type: str = 'blockSpoiler'
98
+
99
+
100
+ @dataclass
101
+ class List(Parent):
102
+ """Ordered or unordered list node."""
103
+
104
+ ordered: bool = False
105
+ start: int | None = None
106
+ spread: bool = False
107
+ type: str = 'list'
108
+
109
+
110
+ @dataclass
111
+ class ListItem(Parent):
112
+ """List item node."""
113
+
114
+ checked: bool | None = None
115
+ spread: bool = False
116
+ type: str = 'listItem'
117
+
118
+
119
+ @dataclass
120
+ class DefinitionList(Parent):
121
+ """Definition list node."""
122
+
123
+ type: str = 'definitionList'
124
+
125
+
126
+ @dataclass
127
+ class DefinitionTerm(Parent):
128
+ """Definition term node."""
129
+
130
+ type: str = 'definitionTerm'
131
+
132
+
133
+ @dataclass
134
+ class DefinitionDescription(Parent):
135
+ """Definition description node."""
136
+
137
+ spread: bool = False
138
+ type: str = 'definitionDescription'
139
+
140
+
141
+ @dataclass
142
+ class Code(Literal):
143
+ """Fenced or indented code block node."""
144
+
145
+ lang: str | None = None
146
+ meta: str | None = None
147
+ type: str = 'code'
148
+
149
+
150
+ @dataclass
151
+ class Math(Literal):
152
+ """Display math block node."""
153
+
154
+ type: str = 'math'
155
+
156
+
157
+ @dataclass
158
+ class ThematicBreak(Node):
159
+ """Thematic break node."""
160
+
161
+ type: str = 'thematicBreak'
162
+
163
+
164
+ @dataclass
165
+ class Html(Literal):
166
+ """Raw HTML node."""
167
+
168
+ type: str = 'html'
169
+
170
+
171
+ @dataclass
172
+ class Text(Literal):
173
+ """Plain text node."""
174
+
175
+ _parse_emphasis: bool = True
176
+ type: str = 'text'
177
+
178
+
179
+ @dataclass
180
+ class InlineCode(Literal):
181
+ """Inline code span node."""
182
+
183
+ type: str = 'inlineCode'
184
+
185
+
186
+ @dataclass
187
+ class InlineMath(Literal):
188
+ """Inline math node."""
189
+
190
+ type: str = 'inlineMath'
191
+
192
+
193
+ @dataclass
194
+ class Strong(Parent):
195
+ """Strong emphasis node."""
196
+
197
+ type: str = 'strong'
198
+
199
+
200
+ @dataclass
201
+ class Emphasis(Parent):
202
+ """Emphasis node."""
203
+
204
+ type: str = 'emphasis'
205
+
206
+
207
+ @dataclass
208
+ class Delete(Parent):
209
+ """Deleted text node."""
210
+
211
+ type: str = 'delete'
212
+
213
+
214
+ @dataclass
215
+ class Mark(Parent):
216
+ """Highlighted text node."""
217
+
218
+ type: str = 'mark'
219
+
220
+
221
+ @dataclass
222
+ class Insert(Parent):
223
+ """Inserted text node."""
224
+
225
+ type: str = 'insert'
226
+
227
+
228
+ @dataclass
229
+ class Superscript(Parent):
230
+ """Superscript node."""
231
+
232
+ type: str = 'superscript'
233
+
234
+
235
+ @dataclass
236
+ class Subscript(Parent):
237
+ """Subscript node."""
238
+
239
+ type: str = 'subscript'
240
+
241
+
242
+ @dataclass
243
+ class Ruby(Node):
244
+ """Ruby annotation node."""
245
+
246
+ segments: list[dict[str, str]] = field(default_factory=list)
247
+ type: str = 'ruby'
248
+
249
+
250
+ @dataclass
251
+ class InlineSpoiler(Parent):
252
+ """Inline spoiler node."""
253
+
254
+ type: str = 'inlineSpoiler'
255
+
256
+
257
+ @dataclass
258
+ class Abbreviation(Parent):
259
+ """Abbreviation node."""
260
+
261
+ title: str = ''
262
+ type: str = 'abbreviation'
263
+
264
+
265
+ @dataclass
266
+ class Table(Parent):
267
+ """Table node."""
268
+
269
+ align: list[str | None] = field(default_factory=list)
270
+ type: str = 'table'
271
+
272
+
273
+ @dataclass
274
+ class TableRow(Parent):
275
+ """Table row node."""
276
+
277
+ type: str = 'tableRow'
278
+
279
+
280
+ @dataclass
281
+ class TableCell(Parent):
282
+ """Table cell node."""
283
+
284
+ type: str = 'tableCell'
285
+
286
+
287
+ @dataclass
288
+ class Link(Parent):
289
+ """Link node."""
290
+
291
+ url: str = ''
292
+ title: str | None = None
293
+ type: str = 'link'
294
+
295
+
296
+ @dataclass
297
+ class Image(Node):
298
+ """Image node."""
299
+
300
+ url: str = ''
301
+ alt: str = ''
302
+ title: str | None = None
303
+ type: str = 'image'
304
+
305
+
306
+ @dataclass
307
+ class Break(Node):
308
+ """Hard line break node."""
309
+
310
+ type: str = 'break'
311
+
312
+
313
+ @dataclass
314
+ class FootnoteReference(Node):
315
+ """Footnote reference node."""
316
+
317
+ identifier: str = ''
318
+ label: str = ''
319
+ type: str = 'footnoteReference'
320
+
321
+
322
+ @dataclass
323
+ class FootnoteDefinition(Parent):
324
+ """Footnote definition node."""
325
+
326
+ identifier: str = ''
327
+ label: str = ''
328
+ type: str = 'footnoteDefinition'
329
+
330
+
331
+ @dataclass
332
+ class TextDirective(Parent):
333
+ """Inline directive node."""
334
+
335
+ name: str = ''
336
+ attributes: dict[str, str] | None = None
337
+ type: str = 'textDirective'
338
+
339
+
340
+ @dataclass
341
+ class LeafDirective(Parent):
342
+ """Leaf block directive node."""
343
+
344
+ name: str = ''
345
+ attributes: dict[str, str] | None = None
346
+ type: str = 'leafDirective'
347
+
348
+
349
+ @dataclass
350
+ class ContainerDirective(Parent):
351
+ """Container block directive node."""
352
+
353
+ name: str = ''
354
+ attributes: dict[str, str] | None = None
355
+ type: str = 'containerDirective'
356
+
357
+
358
+ DirectiveNode = TextDirective | LeafDirective | ContainerDirective