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,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, override
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from sphinx.util.docutils import SphinxDirective
|
|
6
|
+
from docutils.parsers.rst.directives import _directives
|
|
7
|
+
from docutils.parsers.rst.roles import _roles
|
|
8
|
+
|
|
9
|
+
from .render import HostWrapper
|
|
10
|
+
from .ctxnodes import pending_node
|
|
11
|
+
from .utils import find_current_section, Report, Reporter
|
|
12
|
+
from .utils.ctxproxy import proxy
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Any, Callable, ClassVar
|
|
16
|
+
from sphinx.application import Sphinx
|
|
17
|
+
from .render import ParseHost, TransformHost
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GlobalExtraContxt(ABC):
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def generate(self) -> Any: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ParsePhaseExtraContext(ABC):
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def generate(self, host: ParseHost) -> Any: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TransformPhaseExtraContext(ABC):
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def generate(self, host: TransformHost) -> Any: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =======================
|
|
36
|
+
# Extra context registion
|
|
37
|
+
# =======================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExtraContextRegistry:
|
|
41
|
+
names: set[str]
|
|
42
|
+
parsing: dict[str, ParsePhaseExtraContext]
|
|
43
|
+
parsed: dict[str, TransformPhaseExtraContext]
|
|
44
|
+
post_transform: dict[str, TransformPhaseExtraContext]
|
|
45
|
+
global_: dict[str, GlobalExtraContxt]
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.names = set()
|
|
49
|
+
self.parsing = {}
|
|
50
|
+
self.parsed = {}
|
|
51
|
+
self.post_transform = {}
|
|
52
|
+
self.global_ = {}
|
|
53
|
+
|
|
54
|
+
self.add_global_context('sphinx', _SphinxExtraContext())
|
|
55
|
+
self.add_global_context('docutils', _DocutilsExtraContext())
|
|
56
|
+
self.add_parsing_phase_context('markup', _MarkupExtraContext())
|
|
57
|
+
self.add_parsing_phase_context('section', _SectionExtraContext())
|
|
58
|
+
self.add_parsing_phase_context('doc', _DocExtraContext())
|
|
59
|
+
|
|
60
|
+
def _name_dedup(self, name: str) -> None:
|
|
61
|
+
# TODO: allow dup
|
|
62
|
+
if name in self.names:
|
|
63
|
+
raise ValueError(f'Context generator {name} already exists')
|
|
64
|
+
self.names.add(name)
|
|
65
|
+
|
|
66
|
+
def add_parsing_phase_context(
|
|
67
|
+
self, name: str, ctxgen: ParsePhaseExtraContext
|
|
68
|
+
) -> None:
|
|
69
|
+
self._name_dedup(name)
|
|
70
|
+
self.parsing['_' + name] = ctxgen
|
|
71
|
+
|
|
72
|
+
def add_parsed_phase_context(
|
|
73
|
+
self, name: str, ctxgen: TransformPhaseExtraContext
|
|
74
|
+
) -> None:
|
|
75
|
+
self._name_dedup(name)
|
|
76
|
+
self.parsed['_' + name] = ctxgen
|
|
77
|
+
|
|
78
|
+
def add_post_transform_phase_context(
|
|
79
|
+
self, name: str, ctxgen: TransformPhaseExtraContext
|
|
80
|
+
) -> None:
|
|
81
|
+
self._name_dedup(name)
|
|
82
|
+
self.post_transform['_' + name] = ctxgen
|
|
83
|
+
|
|
84
|
+
def add_global_context(self, name: str, ctxgen: GlobalExtraContxt):
|
|
85
|
+
self._name_dedup(name)
|
|
86
|
+
self.global_['_' + name] = ctxgen
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ===================================
|
|
90
|
+
# Bulitin extra context implementions
|
|
91
|
+
# ===================================
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _MarkupExtraContext(ParsePhaseExtraContext):
|
|
95
|
+
@override
|
|
96
|
+
def generate(self, host: ParseHost) -> Any:
|
|
97
|
+
isdir = isinstance(host, SphinxDirective)
|
|
98
|
+
return {
|
|
99
|
+
'type': 'directive' if isdir else 'role',
|
|
100
|
+
'name': host.name,
|
|
101
|
+
'lineno': host.lineno,
|
|
102
|
+
'rawtext': host.block_text if isdir else host.rawtext,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _DocExtraContext(ParsePhaseExtraContext):
|
|
107
|
+
@override
|
|
108
|
+
def generate(self, host: ParseHost) -> Any:
|
|
109
|
+
return proxy(HostWrapper(host).doctree)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _SectionExtraContext(ParsePhaseExtraContext):
|
|
113
|
+
@override
|
|
114
|
+
def generate(self, host: ParseHost) -> Any:
|
|
115
|
+
parent = HostWrapper(host).parent
|
|
116
|
+
return proxy(find_current_section(parent))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _SphinxExtraContext(GlobalExtraContxt):
|
|
120
|
+
app: ClassVar[Sphinx]
|
|
121
|
+
|
|
122
|
+
@override
|
|
123
|
+
def generate(self) -> Any:
|
|
124
|
+
return proxy(self.app)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _DocutilsExtraContext(GlobalExtraContxt):
|
|
128
|
+
@override
|
|
129
|
+
def generate(self) -> Any:
|
|
130
|
+
# FIXME: use unexported api
|
|
131
|
+
return {
|
|
132
|
+
'directives': _directives,
|
|
133
|
+
'roles': _roles,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ========================
|
|
138
|
+
# Extra Context Management
|
|
139
|
+
# ========================
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ExtraContextGenerator:
|
|
143
|
+
node: pending_node
|
|
144
|
+
report: Report
|
|
145
|
+
|
|
146
|
+
registry: ClassVar[ExtraContextRegistry] = ExtraContextRegistry()
|
|
147
|
+
|
|
148
|
+
def __init__(self, node: pending_node) -> None:
|
|
149
|
+
self.node = node
|
|
150
|
+
self.report = Report(
|
|
151
|
+
'Extra Context Generation Report',
|
|
152
|
+
'ERROR',
|
|
153
|
+
source=node.source,
|
|
154
|
+
line=node.line,
|
|
155
|
+
)
|
|
156
|
+
Reporter(node).append(self.report)
|
|
157
|
+
|
|
158
|
+
def on_anytime(self) -> None:
|
|
159
|
+
for name, ctxgen in self.registry.global_.items():
|
|
160
|
+
self._safegen(name, lambda: ctxgen.generate())
|
|
161
|
+
|
|
162
|
+
def on_parsing(self, host: ParseHost) -> None:
|
|
163
|
+
for name, ctxgen in self.registry.parsing.items():
|
|
164
|
+
self._safegen(name, lambda: ctxgen.generate(host))
|
|
165
|
+
|
|
166
|
+
def on_parsed(self, host: TransformHost) -> None:
|
|
167
|
+
for name, ctxgen in self.registry.parsed.items():
|
|
168
|
+
self._safegen(name, lambda: ctxgen.generate(host))
|
|
169
|
+
|
|
170
|
+
def on_post_transform(self, host: TransformHost) -> None:
|
|
171
|
+
for name, ctxgen in self.registry.post_transform.items():
|
|
172
|
+
self._safegen(name, lambda: ctxgen.generate(host))
|
|
173
|
+
|
|
174
|
+
def _safegen(self, name: str, gen: Callable[[], Any]):
|
|
175
|
+
try:
|
|
176
|
+
# ctxgen.generate can be user-defined code, exception of any kind are possible.
|
|
177
|
+
self.node.extra[name] = gen()
|
|
178
|
+
except Exception:
|
|
179
|
+
self.report.text(f'Failed to generate extra context "{name}":')
|
|
180
|
+
self.report.traceback()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def setup(app: Sphinx):
|
|
184
|
+
_SphinxExtraContext.app = app
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sphinxnotes.markup
|
|
3
|
+
~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Rendering markup text to doctree nodes.
|
|
6
|
+
|
|
7
|
+
:copyright: Copyright 2025 by the Shengyu Zhang.
|
|
8
|
+
:license: BSD, see LICENSE for details.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from docutils import nodes
|
|
16
|
+
from docutils.parsers.rst.states import Struct
|
|
17
|
+
from docutils.utils import new_document
|
|
18
|
+
from sphinx import version_info
|
|
19
|
+
from sphinx.util.docutils import SphinxDirective, SphinxRole
|
|
20
|
+
from sphinx.transforms import SphinxTransform
|
|
21
|
+
from sphinx.environment.collectors.asset import ImageCollector
|
|
22
|
+
|
|
23
|
+
from .render import Host
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from docutils.nodes import Node, system_message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class MarkupRenderer:
|
|
31
|
+
host: Host
|
|
32
|
+
|
|
33
|
+
def render(
|
|
34
|
+
self, text: str, inline: bool = False
|
|
35
|
+
) -> tuple[list[Node], list[system_message]]:
|
|
36
|
+
if inline:
|
|
37
|
+
return self._render_inline(text)
|
|
38
|
+
else:
|
|
39
|
+
return self._render(text), []
|
|
40
|
+
|
|
41
|
+
def _render(self, text: str) -> list[Node]:
|
|
42
|
+
if isinstance(self.host, SphinxDirective):
|
|
43
|
+
return self.host.parse_text_to_nodes(text)
|
|
44
|
+
elif isinstance(self.host, SphinxTransform):
|
|
45
|
+
# TODO: dont create parser for every time
|
|
46
|
+
if version_info[0] >= 9:
|
|
47
|
+
parser = self.host.app.registry.create_source_parser(
|
|
48
|
+
'rst', env=self.host.env, config=self.host.config
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
parser = self.host.app.registry.create_source_parser(
|
|
52
|
+
self.host.app, 'rst'
|
|
53
|
+
)
|
|
54
|
+
settings = self.host.document.settings
|
|
55
|
+
doc = new_document('<generated text>', settings=settings)
|
|
56
|
+
parser.parse(text, doc)
|
|
57
|
+
|
|
58
|
+
# NOTE: Nodes produced by standalone source parser should be fixed
|
|
59
|
+
# before returning, cause they missed the processing by certain
|
|
60
|
+
# Sphinx transforms.
|
|
61
|
+
self._fix_document(doc)
|
|
62
|
+
|
|
63
|
+
return doc.children
|
|
64
|
+
else:
|
|
65
|
+
assert False
|
|
66
|
+
|
|
67
|
+
def _render_inline(self, text: str) -> tuple[list[Node], list[system_message]]:
|
|
68
|
+
if isinstance(self.host, SphinxDirective):
|
|
69
|
+
return self.host.parse_inline(text)
|
|
70
|
+
if isinstance(self.host, SphinxRole):
|
|
71
|
+
inliner = self.host.inliner
|
|
72
|
+
memo = Struct(
|
|
73
|
+
document=inliner.document,
|
|
74
|
+
reporter=inliner.reporter,
|
|
75
|
+
language=inliner.language,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return inliner.parse(text, self.host.lineno, memo, inliner.parent)
|
|
79
|
+
elif isinstance(self.host, SphinxTransform):
|
|
80
|
+
# Fallback to normal non-inline render then extract inline
|
|
81
|
+
# elements by self.
|
|
82
|
+
# FIXME: error seems be ignored?
|
|
83
|
+
ns = self._render(text)
|
|
84
|
+
if ns and isinstance(ns[0], nodes.paragraph):
|
|
85
|
+
ns = ns[0].children
|
|
86
|
+
return ns, []
|
|
87
|
+
else:
|
|
88
|
+
assert False
|
|
89
|
+
|
|
90
|
+
def _fix_document(self, document: nodes.document) -> None:
|
|
91
|
+
assert isinstance(self.host, SphinxTransform)
|
|
92
|
+
|
|
93
|
+
"""For documents generated by a separate source parser, some preprocessing
|
|
94
|
+
may be missing. For example:
|
|
95
|
+
|
|
96
|
+
- the lack of an ImageCollector result in an incorrect "node['uri']" and
|
|
97
|
+
a missing "node['candidates']" :class:`nodes.images
|
|
98
|
+
|
|
99
|
+
.. note::
|
|
100
|
+
|
|
101
|
+
Since this depends on Sphinx's internal implementation, there may be
|
|
102
|
+
many cases that have not been considered. We can only do our best
|
|
103
|
+
and fix each error we encounter as possible.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
ImageCollector().process_doc(self.host.app, document)
|
|
107
|
+
|
|
108
|
+
def _fix_image_candidates(self, node: nodes.image) -> None:
|
|
109
|
+
"""This is another way for fixing images node instead of calling
|
|
110
|
+
ImageCollector().process_doc. Keep it for now.
|
|
111
|
+
"""
|
|
112
|
+
assert isinstance(self.host, SphinxTransform)
|
|
113
|
+
|
|
114
|
+
# NOTE:
|
|
115
|
+
# :meth:`sphinx.environment.collectors.ImageCollector.process_doc` add
|
|
116
|
+
# a 'candidates' key to nodes.image, and subclasses of
|
|
117
|
+
# :class:`sphinx.transforms.post_transforms.BaseImageConverter` require
|
|
118
|
+
# the key.
|
|
119
|
+
node['candidates'] = {}
|
|
120
|
+
|
|
121
|
+
# Update `node['uri']` to a relative path from srcdir.
|
|
122
|
+
node['uri'], _ = self.host.env.relfn2path(node['uri'])
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# This file is generated from sphinx-notes/cookiecutter.
|
|
2
|
+
# DO NOT EDIT!!!
|
|
3
|
+
|
|
4
|
+
################################################################################
|
|
5
|
+
# Project meta infos.
|
|
6
|
+
################################################################################
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
|
|
11
|
+
__project__ = 'sphinxnotes.render'
|
|
12
|
+
__author__ = 'Shengyu Zhang'
|
|
13
|
+
__desc__ = 'A framework to define, constrain, and render data in Sphinx documentation'
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
__version__ = metadata.version('sphinxnotes.render')
|
|
17
|
+
except metadata.PackageNotFoundError:
|
|
18
|
+
__version__ = 'unknown'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
################################################################################
|
|
22
|
+
# Sphinx extension utils.
|
|
23
|
+
################################################################################
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def pre_setup(app):
|
|
27
|
+
app.require_sphinx('7.0')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def post_setup(app):
|
|
31
|
+
return {
|
|
32
|
+
'version': __version__,
|
|
33
|
+
'parallel_read_safe': True,
|
|
34
|
+
'parallel_write_safe': True,
|
|
35
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sphinxnotes.render.pipeline
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
:copyright: Copyright 2026 by the Shengyu Zhang.
|
|
6
|
+
:license: BSD, see LICENSE for details.
|
|
7
|
+
|
|
8
|
+
This module defines pipeline for rendering data to nodes.
|
|
9
|
+
|
|
10
|
+
The Pipline
|
|
11
|
+
===========
|
|
12
|
+
|
|
13
|
+
1. Define context: BaseDataSource generates a :class:`pending_node`, which contains:
|
|
14
|
+
|
|
15
|
+
- Context
|
|
16
|
+
- Template for rendering data to markup text
|
|
17
|
+
- Possible extra contexts
|
|
18
|
+
|
|
19
|
+
See also :class:`BaseDataSource`.
|
|
20
|
+
|
|
21
|
+
2. Render data: the ``pending_node`` nodes will be rendered
|
|
22
|
+
(by calling :meth:`pending_node.render`) at some point, depending on
|
|
23
|
+
:attr:`pending_node.template.phase`.
|
|
24
|
+
|
|
25
|
+
The one who calls ``pending_node.render`` is called ``Host``.
|
|
26
|
+
The ``Host`` host is responsible for rendering the markup text into docutils
|
|
27
|
+
nodes (See :class:`MarkupRenderer`).
|
|
28
|
+
|
|
29
|
+
Phases:
|
|
30
|
+
|
|
31
|
+
:``Phase.Parsing``:
|
|
32
|
+
Called by BaseDataSource ('s subclasses)
|
|
33
|
+
|
|
34
|
+
:``Phase.Parsed``:
|
|
35
|
+
Called by :class:`ParsedHookTransform`.
|
|
36
|
+
|
|
37
|
+
:``Phase.Resolving``:
|
|
38
|
+
Called by :class:`ResolvingHookTransform`.
|
|
39
|
+
|
|
40
|
+
How context be rendered ``list[nodes.Node]``
|
|
41
|
+
============================================
|
|
42
|
+
|
|
43
|
+
.. seealso:: :meth:`.ctxnodes.pending_node.render`.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
from typing import TYPE_CHECKING, override, final, cast
|
|
49
|
+
from abc import abstractmethod, ABC
|
|
50
|
+
|
|
51
|
+
from docutils import nodes
|
|
52
|
+
from sphinx.util import logging
|
|
53
|
+
from sphinx.util.docutils import SphinxDirective, SphinxRole
|
|
54
|
+
from sphinx.transforms import SphinxTransform
|
|
55
|
+
from sphinx.transforms.post_transforms import SphinxPostTransform, ReferencesResolver
|
|
56
|
+
|
|
57
|
+
from .render import HostWrapper, Phase, Template, Host, ParseHost, TransformHost
|
|
58
|
+
from .ctx import PendingContext, ResolvedContext
|
|
59
|
+
from .ctxnodes import pending_node
|
|
60
|
+
from .extractx import ExtraContextGenerator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from sphinx.application import Sphinx
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Pipeline(ABC):
|
|
70
|
+
"""
|
|
71
|
+
The core class defines the pipleing of rendering :class:`pending_node`s.
|
|
72
|
+
|
|
73
|
+
Subclass is responsible to:
|
|
74
|
+
|
|
75
|
+
- call ``queue_xxx`` to add pendin nodes into queue.
|
|
76
|
+
- override :meth:`process_pending_node` to control when a pending node gets
|
|
77
|
+
rendered. In this method subclass can also call ``queue_xxx`` to add more
|
|
78
|
+
pending nodes.
|
|
79
|
+
- call :meth:`render_queue` to process all queued nodes and
|
|
80
|
+
returns any that couldn't be rendered in the current phase.
|
|
81
|
+
|
|
82
|
+
See Also:
|
|
83
|
+
|
|
84
|
+
- :class:`BaseDataSource`: Context source implementation and hook for Phase.Parsing
|
|
85
|
+
- :class:`ParsedHookTransform`: Built-in hook for Phase.Parsed
|
|
86
|
+
- :class:`ResolvingHookTransform`: Built-in hook for Phase.Resolving
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
#: Queue of pending node to be rendered.
|
|
90
|
+
_q: list[pending_node] | None = None
|
|
91
|
+
|
|
92
|
+
"""Methods to be overrided."""
|
|
93
|
+
|
|
94
|
+
def process_pending_node(self, n: pending_node) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
You can add hooks to pending node here.
|
|
97
|
+
|
|
98
|
+
Return ``true`` if you want to render the pending node *now*,
|
|
99
|
+
otherwise it will be inserted to doctree directly andwaiting to later
|
|
100
|
+
rendering
|
|
101
|
+
"""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
"""Helper method for subclasses."""
|
|
105
|
+
|
|
106
|
+
@final
|
|
107
|
+
def queue_pending_node(self, n: pending_node) -> None:
|
|
108
|
+
if not self._q:
|
|
109
|
+
self._q = []
|
|
110
|
+
self._q.append(n)
|
|
111
|
+
|
|
112
|
+
@final
|
|
113
|
+
def queue_context(
|
|
114
|
+
self, ctx: PendingContext | ResolvedContext, tmpl: Template
|
|
115
|
+
) -> pending_node:
|
|
116
|
+
pending = pending_node(ctx, tmpl)
|
|
117
|
+
self.queue_pending_node(pending)
|
|
118
|
+
return pending
|
|
119
|
+
|
|
120
|
+
@final
|
|
121
|
+
def render_queue(self) -> list[pending_node]:
|
|
122
|
+
"""
|
|
123
|
+
Try rendering all pending nodes in queue.
|
|
124
|
+
|
|
125
|
+
If the timing(Phase) is ok, :class:`pending_node` will be rendered
|
|
126
|
+
(pending.rendered = True); otherwise, the pending node is unchanged.
|
|
127
|
+
|
|
128
|
+
If the pending node is already inserted to document, it will not be return.
|
|
129
|
+
And the corrsponding rendered node will replace it too.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
logger.debug(
|
|
133
|
+
f'{type(self)} is running its render queue, '
|
|
134
|
+
f'{len(self._q or [])} node(s) to render'
|
|
135
|
+
)
|
|
136
|
+
ns = []
|
|
137
|
+
while self._q:
|
|
138
|
+
pending = self._q.pop()
|
|
139
|
+
|
|
140
|
+
ok = self.process_pending_node(pending)
|
|
141
|
+
logger.debug(
|
|
142
|
+
f'{type(self)} is trying to render '
|
|
143
|
+
f'{pending.source}:{pending.line}, ok? {ok}'
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if not ok:
|
|
147
|
+
ns.append(pending)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Generate global extra context for later use.
|
|
151
|
+
ExtraContextGenerator(pending).on_anytime()
|
|
152
|
+
|
|
153
|
+
host = cast(Host, self)
|
|
154
|
+
pending.render(host)
|
|
155
|
+
|
|
156
|
+
if pending.parent is None:
|
|
157
|
+
ns.append(pending)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
if pending.inline:
|
|
161
|
+
host_ = HostWrapper(host)
|
|
162
|
+
pending.unwrap_and_replace_self_inline((host_.doctree, pending.parent))
|
|
163
|
+
else:
|
|
164
|
+
pending.unwrap_and_replace_self()
|
|
165
|
+
|
|
166
|
+
logger.debug(
|
|
167
|
+
f'{type(self)} runs out of its render queue, '
|
|
168
|
+
f'{len(self._q or [])} node(s) hanging'
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return ns
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class BaseContextSource(Pipeline):
|
|
175
|
+
"""
|
|
176
|
+
Abstract base class for generateing context, as the source of the rendering
|
|
177
|
+
pipeline.
|
|
178
|
+
|
|
179
|
+
This class also responsible to render context in Phase.Parsing. So the final
|
|
180
|
+
implementations MUST be subclass of :class:`SphinxDirective` or
|
|
181
|
+
:class:`SphinxRole`, which provide the execution context and interface for
|
|
182
|
+
processing reStructuredText markup.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
"""Methods to be implemented."""
|
|
186
|
+
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def current_context(self) -> PendingContext | ResolvedContext:
|
|
189
|
+
"""Return the context to be rendered."""
|
|
190
|
+
...
|
|
191
|
+
|
|
192
|
+
@abstractmethod
|
|
193
|
+
def current_template(self) -> Template:
|
|
194
|
+
"""
|
|
195
|
+
Return the template for rendering the context.
|
|
196
|
+
|
|
197
|
+
This method should be implemented to provide the Jinja2 template
|
|
198
|
+
that will render the context into markup text. The template determines
|
|
199
|
+
the phase at which rendering occurs.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The template to use for rendering.
|
|
203
|
+
"""
|
|
204
|
+
...
|
|
205
|
+
|
|
206
|
+
"""Methods override from parent."""
|
|
207
|
+
|
|
208
|
+
@override
|
|
209
|
+
def process_pending_node(self, n: pending_node) -> bool:
|
|
210
|
+
host = cast(ParseHost, self)
|
|
211
|
+
|
|
212
|
+
# Set source and line.
|
|
213
|
+
host.set_source_info(n)
|
|
214
|
+
# Generate and save parsing phase extra context for later use.
|
|
215
|
+
ExtraContextGenerator(n).on_parsing(host)
|
|
216
|
+
|
|
217
|
+
return n.template.phase == Phase.Parsing
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class BaseContextDirective(BaseContextSource, SphinxDirective):
|
|
221
|
+
@override
|
|
222
|
+
def run(self) -> list[nodes.Node]:
|
|
223
|
+
self.queue_context(self.current_context(), self.current_template())
|
|
224
|
+
|
|
225
|
+
ns = []
|
|
226
|
+
for x in self.render_queue():
|
|
227
|
+
if not x.rendered:
|
|
228
|
+
ns.append(x)
|
|
229
|
+
continue
|
|
230
|
+
ns += x.unwrap()
|
|
231
|
+
|
|
232
|
+
return ns
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class BaseContextRole(BaseContextSource, SphinxRole):
|
|
236
|
+
@override
|
|
237
|
+
def process_pending_node(self, n: pending_node) -> bool:
|
|
238
|
+
n.inline = True
|
|
239
|
+
return super().process_pending_node(n)
|
|
240
|
+
|
|
241
|
+
@override
|
|
242
|
+
def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
|
|
243
|
+
pending = self.queue_context(self.current_context(), self.current_template())
|
|
244
|
+
pending.inline = True
|
|
245
|
+
|
|
246
|
+
ns, msgs = [], []
|
|
247
|
+
for n in self.render_queue():
|
|
248
|
+
if not n.rendered:
|
|
249
|
+
ns.append(n)
|
|
250
|
+
continue
|
|
251
|
+
ns_, msgs_ = n.unwrap_inline(self.inliner)
|
|
252
|
+
ns += ns_
|
|
253
|
+
msgs += msgs_
|
|
254
|
+
|
|
255
|
+
return ns, msgs
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class ParsedHookTransform(SphinxTransform, Pipeline):
|
|
259
|
+
# Before almost all others.
|
|
260
|
+
default_priority = 100
|
|
261
|
+
|
|
262
|
+
@override
|
|
263
|
+
def process_pending_node(self, n: pending_node) -> bool:
|
|
264
|
+
ExtraContextGenerator(n).on_parsed(cast(TransformHost, self))
|
|
265
|
+
return n.template.phase == Phase.Parsed
|
|
266
|
+
|
|
267
|
+
@override
|
|
268
|
+
def apply(self, **kwargs):
|
|
269
|
+
for pending in self.document.findall(pending_node):
|
|
270
|
+
self.queue_pending_node(pending)
|
|
271
|
+
|
|
272
|
+
for n in self.render_queue():
|
|
273
|
+
...
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ResolvingHookTransform(SphinxPostTransform, Pipeline):
|
|
277
|
+
# After resolving pending_xref
|
|
278
|
+
default_priority = (ReferencesResolver.default_priority or 10) + 5
|
|
279
|
+
|
|
280
|
+
@override
|
|
281
|
+
def process_pending_node(self, n: pending_node) -> bool:
|
|
282
|
+
ExtraContextGenerator(n).on_post_transform(cast(TransformHost, self))
|
|
283
|
+
return n.template.phase == Phase.Resolving
|
|
284
|
+
|
|
285
|
+
@override
|
|
286
|
+
def apply(self, **kwargs):
|
|
287
|
+
for pending in self.document.findall(pending_node):
|
|
288
|
+
self.queue_pending_node(pending)
|
|
289
|
+
ns = self.render_queue()
|
|
290
|
+
|
|
291
|
+
# NOTE: Should no node left.
|
|
292
|
+
assert len(ns) == 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def setup(app: Sphinx) -> None:
|
|
296
|
+
# Hook for Phase.Parsed.
|
|
297
|
+
app.add_transform(ParsedHookTransform)
|
|
298
|
+
|
|
299
|
+
# Hook for Phase.Resolving.
|
|
300
|
+
app.add_post_transform(ResolvingHookTransform)
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from docutils import nodes
|
|
6
|
+
from sphinx.transforms import SphinxTransform
|
|
7
|
+
from sphinx.util.docutils import SphinxDirective, SphinxRole
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Phase(Enum):
|
|
11
|
+
Parsing = 'parsing'
|
|
12
|
+
Parsed = 'parsed'
|
|
13
|
+
Resolving = 'resolving'
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def default(cls) -> Phase:
|
|
17
|
+
return cls.Parsing
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Template:
|
|
22
|
+
#: Jinja template for rendering the context.
|
|
23
|
+
text: str
|
|
24
|
+
#: The render phase.
|
|
25
|
+
phase: Phase = Phase.default()
|
|
26
|
+
#: Enable debug output (shown as :class:`nodes.system_message` in document.)
|
|
27
|
+
debug: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Possible render host of :meth:`pending_node.render`.
|
|
31
|
+
type Host = ParseHost | TransformHost
|
|
32
|
+
# Host of source parse phase (Phase.Parsing, Phase.Parsed).
|
|
33
|
+
type ParseHost = SphinxDirective | SphinxRole
|
|
34
|
+
# Host of source parse phase (Phase.Parsing, Phase.Parsed).
|
|
35
|
+
type TransformHost = SphinxTransform
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class HostWrapper:
|
|
40
|
+
v: Host
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def doctree(self) -> nodes.document:
|
|
44
|
+
if isinstance(self.v, SphinxDirective):
|
|
45
|
+
return self.v.state.document
|
|
46
|
+
elif isinstance(self.v, SphinxRole):
|
|
47
|
+
return self.v.inliner.document
|
|
48
|
+
elif isinstance(self.v, SphinxTransform):
|
|
49
|
+
return self.v.document
|
|
50
|
+
else:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def parent(self) -> nodes.Element | None:
|
|
55
|
+
if isinstance(self.v, SphinxDirective):
|
|
56
|
+
return self.v.state.parent
|
|
57
|
+
elif isinstance(self.v, SphinxRole):
|
|
58
|
+
return self.v.inliner.parent
|
|
59
|
+
else:
|
|
60
|
+
return None
|