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.
- sphinxnotes/render/__init__.py +93 -0
- sphinxnotes/render/ctx.py +76 -0
- sphinxnotes/render/ctxnodes.py +254 -0
- sphinxnotes/render/data.py +546 -0
- sphinxnotes/render/extractx.py +184 -0
- sphinxnotes/render/markup.py +122 -0
- sphinxnotes/render/meta.py +35 -0
- sphinxnotes/render/pipeline.py +300 -0
- sphinxnotes/render/py.typed +0 -0
- sphinxnotes/render/render.py +60 -0
- sphinxnotes/render/sources.py +129 -0
- sphinxnotes/render/template.py +146 -0
- sphinxnotes/render/utils/__init__.py +211 -0
- sphinxnotes/render/utils/ctxproxy.py +144 -0
- sphinxnotes/render/utils/freestyle.py +93 -0
- sphinxnotes_render-1.0a42.dist-info/METADATA +78 -0
- sphinxnotes_render-1.0a42.dist-info/RECORD +20 -0
- sphinxnotes_render-1.0a42.dist-info/WHEEL +5 -0
- sphinxnotes_render-1.0a42.dist-info/licenses/LICENSE +29 -0
- sphinxnotes_render-1.0a42.dist-info/top_level.txt +1 -0
|
@@ -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()
|