sphinxnotes.render 1.0a42__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.
@@ -0,0 +1,93 @@
1
+ """
2
+ sphinxnotes.render
3
+ ~~~~~~~~~~~~~~~~~~
4
+
5
+ :copyright: Copyright 2026 by the Shengyu Zhang.
6
+ :license: BSD, see LICENSE for details.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from typing import TYPE_CHECKING
11
+
12
+ from . import meta
13
+ from .data import (
14
+ Registry as DataRegistry,
15
+ REGISTRY as DATA_REGISTRY,
16
+ PlainValue,
17
+ Value,
18
+ ValueWrapper,
19
+ RawData,
20
+ ParsedData,
21
+ Field,
22
+ Schema,
23
+ )
24
+
25
+ from .render import (
26
+ Phase,
27
+ Template,
28
+ Host,
29
+ )
30
+ from .ctx import PendingContext, ResolvedContext
31
+ from .ctxnodes import pending_node
32
+ from .extractx import ExtraContextRegistry, ExtraContextGenerator
33
+ from .pipeline import BaseContextRole, BaseContextDirective
34
+ from .sources import (
35
+ UnparsedData,
36
+ BaseDataDefineRole,
37
+ BaseDataDefineDirective,
38
+ StrictDataDefineDirective,
39
+ )
40
+
41
+ if TYPE_CHECKING:
42
+ from sphinx.application import Sphinx
43
+
44
+
45
+ """Python API for other Sphinx extesions."""
46
+ __all__ = [
47
+ 'Registry',
48
+ 'PlainValue',
49
+ 'Value',
50
+ 'ValueWrapper',
51
+ 'RawData',
52
+ 'ParsedData',
53
+ 'Field',
54
+ 'Schema',
55
+ 'Phase',
56
+ 'Template',
57
+ 'Host',
58
+ 'PendingContext',
59
+ 'ResolvedContext',
60
+ 'pending_node',
61
+ 'BaseContextRole',
62
+ 'BaseContextDirective',
63
+ 'UnparsedData',
64
+ 'BaseDataDefineRole',
65
+ 'BaseDataDefineDirective',
66
+ 'StrictDataDefineDirective',
67
+ ]
68
+
69
+
70
+ class Registry:
71
+ """The global, all-in-one registry for user."""
72
+
73
+ @property
74
+ def data(self) -> DataRegistry:
75
+ return DATA_REGISTRY
76
+
77
+ @property
78
+ def extra_context(cls) -> ExtraContextRegistry:
79
+ return ExtraContextGenerator.registry
80
+
81
+
82
+ REGISTRY = Registry()
83
+
84
+ def setup(app: Sphinx):
85
+ meta.pre_setup(app)
86
+
87
+ from . import pipeline, extractx, template
88
+
89
+ pipeline.setup(app)
90
+ extractx.setup(app)
91
+ template.setup(app)
92
+
93
+ return meta.post_setup(app)
@@ -0,0 +1,76 @@
1
+ """
2
+ sphinxnotes.render.ctx
3
+ ~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ :copyright: Copyright 2026 by the Shengyu Zhang.
6
+ :license: BSD, see LICENSE for details.
7
+
8
+ This module wraps the :mod:`data` into context for rendering the template.
9
+ """
10
+
11
+ from typing import TYPE_CHECKING
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Hashable
14
+ from dataclasses import dataclass
15
+
16
+ from .utils import Unpicklable
17
+
18
+ if TYPE_CHECKING:
19
+ from typing import Any
20
+ from .data import ParsedData
21
+
22
+ type ResolvedContext = ParsedData | dict[str, Any]
23
+
24
+
25
+ @dataclass
26
+ class PendingContextRef:
27
+ """A abstract class that references to :class:`PendingCtx`."""
28
+
29
+ ref: int
30
+ chksum: int
31
+
32
+ def __hash__(self) -> int:
33
+ return hash((self.ref, self.chksum))
34
+
35
+
36
+ class PendingContext(ABC, Unpicklable, Hashable):
37
+ """A abstract representation of context that is not currently available.
38
+
39
+ Call :meth:`resolve` at the right time (depends on the implment) to get
40
+ context available.
41
+ """
42
+
43
+ @abstractmethod
44
+ def resolve(self) -> ResolvedContext: ...
45
+
46
+
47
+ class PendingContextStorage:
48
+ """Area for temporarily storing PendingContext.
49
+
50
+ This class is indented to resolve the problem that:
51
+
52
+ Some of the PendingContext are :class:Unpicklable` and they can not be hold
53
+ by :class:`pending_node` (as ``pending_node`` will be pickled along with
54
+ the docutils doctree)
55
+
56
+ This class maintains a mapping from :class:`PendingContextRef` -> :cls:`PendingContext`.
57
+ ``pending_node`` owns the ``PendingContextRef``, and can retrieve the context
58
+ by calling :meth:`retrieve`.
59
+ """
60
+
61
+ _next_id: int
62
+ _data: dict[PendingContextRef, PendingContext] = {}
63
+
64
+ def __init__(self) -> None:
65
+ self._next_id = 0
66
+ self._data = {}
67
+
68
+ def stash(self, pending: PendingContext) -> PendingContextRef:
69
+ ref = PendingContextRef(self._next_id, hash(pending))
70
+ self._next_id += 1
71
+ self._data[ref] = pending
72
+ return ref
73
+
74
+ def retrieve(self, ref: PendingContextRef) -> PendingContext | None:
75
+ data = self._data.pop(ref, None)
76
+ return data
@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, override
3
+ from pprint import pformat
4
+
5
+ from docutils import nodes
6
+ from docutils.parsers.rst.states import Inliner
7
+
8
+ from .render import Template
9
+ from .ctx import PendingContextRef, PendingContext, PendingContextStorage
10
+ from .markup import MarkupRenderer
11
+ from .template import TemplateRenderer
12
+ from .utils import (
13
+ Report,
14
+ Reporter,
15
+ find_nearest_block_element,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from typing import Any, Callable, ClassVar
20
+ from .markup import Host
21
+ from .ctx import ResolvedContext
22
+
23
+
24
+ class pending_node(nodes.Element):
25
+ # The context to be rendered by Jinja template.
26
+ ctx: PendingContextRef | ResolvedContext
27
+ # The extra context as supplement to ctx.
28
+ extra: dict[str, Any]
29
+ #: Jinja template for rendering the context.
30
+ template: Template
31
+ #: Whether rendering to inline nodes.
32
+ inline: bool
33
+ #: Whether the rendering pipeline is finished (failed is also finished).
34
+ rendered: bool
35
+
36
+ #: Mapping of PendingContextRef -> PendingContext.
37
+ #:
38
+ #: NOTE: ``PendingContextStorage`` holds Unpicklable data (``PendingContext``)
39
+ #: but it is doesn't matters :-), cause pickle doesn't deal with ClassVar.
40
+ _PENDING_CONTEXTS: ClassVar[PendingContextStorage] = PendingContextStorage()
41
+
42
+ def __init__(
43
+ self,
44
+ ctx: PendingContext | PendingContextRef | ResolvedContext,
45
+ tmpl: Template,
46
+ inline: bool = False,
47
+ rawsource='',
48
+ *children,
49
+ **attributes,
50
+ ) -> None:
51
+ super().__init__(rawsource, *children, **attributes)
52
+ if not isinstance(ctx, PendingContext):
53
+ self.ctx = ctx
54
+ else:
55
+ self.ctx = self._PENDING_CONTEXTS.stash(ctx)
56
+ self.extra = {}
57
+ self.template = tmpl
58
+ self.inline = inline
59
+ self.rendered = False
60
+
61
+ # Init hook lists.
62
+ self._pending_context_hooks = []
63
+ self._resolved_data_hooks = []
64
+ self._markup_text_hooks = []
65
+ self._rendered_nodes_hooks = []
66
+
67
+ def render(self, host: Host) -> None:
68
+ """
69
+ The core function for rendering context to docutils nodes.
70
+
71
+ 1. PendingContextRef -> PendingContext -> ResolvedContext
72
+ 2. TemplateRenderer.render(ResolvedContext) -> Markup Text (``str``)
73
+ 3. MarkupRenderer.render(Markup Text) -> doctree Nodes (list[nodes.Node])
74
+ """
75
+
76
+ # Make sure the function is called once.
77
+ assert not self.rendered
78
+ self.rendered = True
79
+
80
+ # Clear previous empty reports.
81
+ Reporter(self).clear_empty()
82
+ # Create debug report.
83
+ report = Report('Render Report', 'DEBUG', source=self.source, line=self.line)
84
+
85
+ # Constructor for error report.
86
+ def err_report() -> Report:
87
+ if self.template.debug:
88
+ # Reuse the render report as possible.
89
+ report['type'] = 'ERROR'
90
+ return report
91
+ return Report('Render Report', 'ERROR', source=self.source, line=self.line)
92
+
93
+ # 1. Prepare context for Jinja template.
94
+ if isinstance(self.ctx, PendingContextRef):
95
+ report.text('Pending context ref:')
96
+ report.code(pformat(self.ctx), lang='python')
97
+
98
+ pdata = self._PENDING_CONTEXTS.retrieve(self.ctx)
99
+ if pdata is None:
100
+ report = err_report()
101
+ report.text(f'Failed to retrieve pending context from ref {self.ctx}')
102
+ self += report
103
+ return None
104
+
105
+ report.text('Pending context:')
106
+ report.code(pformat(pdata), lang='python')
107
+
108
+ for hook in self._pending_context_hooks:
109
+ hook(self, pdata)
110
+
111
+ try:
112
+ ctx = self.ctx = pdata.resolve()
113
+ except Exception as e:
114
+ report = err_report()
115
+ report.text('Failed to resolve pending context:')
116
+ report.exception(e)
117
+ self += report
118
+ return None
119
+ else:
120
+ ctx = self.ctx
121
+
122
+ for hook in self._resolved_data_hooks:
123
+ hook(self, ctx)
124
+
125
+ report.text(f'Resolved context (type: {type(ctx)}):')
126
+ report.code(pformat(ctx), lang='python')
127
+ report.text('Extra context (only keys):')
128
+ report.code(pformat(list(self.extra.keys())), lang='python')
129
+ report.text(f'Template (phase: {self.template.phase}):')
130
+ report.code(self.template.text, lang='jinja')
131
+
132
+ # 2. Render the template and context to markup text.
133
+ try:
134
+ markup = TemplateRenderer(self.template.text).render(ctx, extra=self.extra)
135
+ except Exception as e:
136
+ report = err_report()
137
+ report.text('Failed to render Jinja template:')
138
+ report.exception(e)
139
+ self += report
140
+ return
141
+
142
+ for hook in self._markup_text_hooks:
143
+ markup = hook(self, markup)
144
+
145
+ report.text('Rendered markup text:')
146
+ report.code(markup, lang='rst')
147
+
148
+ # 3. Render the markup text to doctree nodes.
149
+ try:
150
+ ns, msgs = MarkupRenderer(host).render(markup, inline=self.inline)
151
+ except Exception as e:
152
+ report = err_report()
153
+ report.text(
154
+ 'Failed to render markup text '
155
+ f'to {"inline " if self.inline else ""}nodes:'
156
+ )
157
+ report.exception(e)
158
+ self += report
159
+ return
160
+
161
+ report.text(f'Rendered nodes (inline: {self.inline}):')
162
+ report.code('\n\n'.join([n.pformat() for n in ns]), lang='xml')
163
+ if msgs:
164
+ report.text('Systemd messages:')
165
+ [report.node(msg) for msg in msgs]
166
+
167
+ # 4. Add rendered nodes to container.
168
+ for hook in self._rendered_nodes_hooks:
169
+ hook(self, ns)
170
+
171
+ # TODO: set_source_info?
172
+ self += ns
173
+
174
+ if self.template.debug:
175
+ self += report
176
+
177
+ return
178
+
179
+ def unwrap(self) -> list[nodes.Node]:
180
+ children = self.children
181
+ self.clear()
182
+ return children
183
+
184
+ def unwrap_inline(
185
+ self, inliner: Report.Inliner
186
+ ) -> tuple[list[nodes.Node], list[nodes.system_message]]:
187
+ # Report (nodes.system_message subclass) is not inline node,
188
+ # should be removed before inserting to doctree.
189
+ reports = Reporter(self).clear()
190
+ for report in reports:
191
+ self.append(report.problematic(inliner))
192
+
193
+ children = self.children
194
+ self.clear()
195
+
196
+ return children, [x for x in reports]
197
+
198
+ def unwrap_and_replace_self(self) -> None:
199
+ children = self.unwrap()
200
+ # Replace self with children.
201
+ self.replace_self(children)
202
+
203
+ def unwrap_and_replace_self_inline(self, inliner: Report.Inliner) -> None:
204
+ # Unwrap inline nodes and system_message noeds from node.
205
+ ns, msgs = self.unwrap_inline(inliner)
206
+
207
+ # Insert reports to nearst block elements (usually nodes.paragraph).
208
+ doctree = inliner.document if isinstance(inliner, Inliner) else inliner[1]
209
+ blkparent = find_nearest_block_element(self.parent) or doctree
210
+ blkparent += msgs
211
+
212
+ # Replace self with inline nodes.
213
+ self.replace_self(ns)
214
+
215
+ """Hooks for procssing render intermediate products. """
216
+
217
+ type PendingContextHook = Callable[[pending_node, PendingContext], None]
218
+ type ResolvedContextHook = Callable[[pending_node, ResolvedContext], None]
219
+ type MarkupTextHook = Callable[[pending_node, str], str]
220
+ type RenderedNodesHook = Callable[[pending_node, list[nodes.Node]], None]
221
+
222
+ _pending_context_hooks: list[PendingContextHook]
223
+ _resolved_data_hooks: list[ResolvedContextHook]
224
+ _markup_text_hooks: list[MarkupTextHook]
225
+ _rendered_nodes_hooks: list[RenderedNodesHook]
226
+
227
+ def hook_pending_context(self, hook: PendingContextHook) -> None:
228
+ self._pending_context_hooks.append(hook)
229
+
230
+ def hook_resolved_context(self, hook: ResolvedContextHook) -> None:
231
+ self._resolved_data_hooks.append(hook)
232
+
233
+ def hook_markup_text(self, hook: MarkupTextHook) -> None:
234
+ self._markup_text_hooks.append(hook)
235
+
236
+ def hook_rendered_nodes(self, hook: RenderedNodesHook) -> None:
237
+ self._rendered_nodes_hooks.append(hook)
238
+
239
+ """Methods override from parent."""
240
+
241
+ @override
242
+ def copy(self) -> Any:
243
+ # NOTE: pending_node is no supposed to be copy as it does not make sense.
244
+ #
245
+ # For example: ablog extension may copy this node.
246
+ if self.inline:
247
+ return nodes.Text('')
248
+ else:
249
+ return nodes.paragraph()
250
+
251
+ @override
252
+ def deepcopy(self) -> Any:
253
+ # NOTE: Same to :meth:`copy`.
254
+ return self.copy()