Skip to content

Commit 77dd630

Browse files
committed
update
1 parent e82da51 commit 77dd630

8 files changed

Lines changed: 136 additions & 84 deletions

File tree

docs/index.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ Introduction
3636

3737
__ https://sphinx.silverrainz.me/any/
3838

39+
.. template::
40+
:on: parsed
41+
:debug:
42+
43+
I am {{ name }}. I have the following attributes:
44+
45+
{% for k, v in attrs.items() %}
46+
:{{ k }}: {{ v }}
47+
{%- endfor %}
48+
49+
This document "_doc.title" has {{ _doc.sections | length }} section(s).
50+
51+
{{ content }}
52+
53+
.. data:: Shengyu Zhang
54+
:github: SilverRainZ
55+
:homepage: https://silverrainz.me/
3956

4057
.. INTRODUCTION END
4158

src/sphinxnotes/data/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
EXTRACTX_REGISTRY,
3939
)
4040
from .config import Config
41+
from . import extractx
4142

4243
if TYPE_CHECKING:
4344
from sphinx.application import Sphinx
@@ -85,7 +86,6 @@ def extractx(cls) -> ExtraContextRegistry:
8586

8687
REGISTRY = Registry()
8788

88-
8989
def setup(app: Sphinx):
9090
meta.pre_setup(app)
9191

src/sphinxnotes/data/data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def as_context(self) -> dict[str, Any]:
251251
class PendingData(Data):
252252
raw: RawData
253253
schema: Schema
254-
_data: ParsedData | None = dataclass_field(init=False)
254+
_data: ParsedData | None = dataclass_field(init=False, default=None)
255255

256256
def parse(self) -> ParsedData:
257257
if self._data:

src/sphinxnotes/data/extractx.py

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,64 @@
11
from __future__ import annotations
2-
from typing import TYPE_CHECKING
2+
from typing import TYPE_CHECKING, override
33

4-
from docutils import nodes
4+
from sphinx.util.docutils import SphinxDirective, SphinxRole
5+
from sphinx.transforms import SphinxTransform
56

67
from .utils import find_current_document, find_current_section
7-
from .utils.context_proxy import proxy
8-
from .render import EXTRACTX_REGISTRY
9-
from .template import Phase
10-
11-
if TYPE_CHECKING:
12-
from typing import Any
13-
from sphinx.transforms import SphinxTransform
14-
from sphinx.util.docutils import SphinxDirective, SphinxRole
15-
16-
17-
def markup(v: SphinxDirective | SphinxRole) -> dict[str, Any]:
18-
isdir = isinstance(v, SphinxDirective)
19-
return {
20-
'type': 'directive' if isdir else 'role',
21-
'name': v.name,
22-
'lineno': v.lineno,
23-
'rawtext': v.block_text if isdir else v.rawtext,
24-
}
25-
26-
def doctree(v: SphinxDirective | SphinxRole | nodes.Node) -> dict[str, Any]:
27-
ctx = {}
28-
if isinstance(v, nodes.Node):
29-
ctx['_doc'] = proxy(find_current_document(v))
30-
ctx['_section'] = proxy(find_current_section(v))
31-
else:
32-
isdir = isinstance(v, SphinxDirective)
33-
state = v.state if isdir else v.inliner
34-
ctx['_doc'] = proxy(state.document)
35-
ctx['_section'] = proxy(find_current_section(state.parent))
36-
return ctx
37-
38-
39-
def sphinx(v: SphinxDirective | SphinxRole | SphinxTransform) -> dict[str, Any]:
40-
ctx = {}
41-
ctx['_env'] = proxy(v.env)
42-
ctx['_config'] = proxy(v.config)
43-
return ctx
44-
45-
EXTRACTX_REGISTRY.add_parse_generator(Phase.Parsing, markup)
8+
from .utils.ctxproxy import proxy
9+
from .render import EXTRACTX_REGISTRY, ParsePhaseContextGenerator, FullPhaseContextGenerator, ParseCaller, Caller, pending_node
10+
from .template import Context
11+
12+
13+
class MarkupContextGenerator(ParsePhaseContextGenerator):
14+
@override
15+
def generate(self, caller: ParseCaller, n: pending_node) -> Context:
16+
isdir = isinstance(caller, SphinxDirective)
17+
return {
18+
'type': 'directive' if isdir else 'role',
19+
'name': caller.name,
20+
'lineno': caller.lineno,
21+
'rawtext': caller.block_text if isdir else caller.rawtext,
22+
}
23+
24+
class DocContextGenerator(FullPhaseContextGenerator):
25+
@override
26+
def generate(self, caller: Caller, n: pending_node) -> Context:
27+
if isinstance(caller, SphinxDirective):
28+
return proxy(caller.state.document)
29+
elif isinstance(caller, SphinxRole):
30+
return proxy(caller.inliner.document)
31+
elif isinstance(caller, SphinxTransform):
32+
return proxy(caller.document)
33+
else:
34+
assert False
35+
36+
37+
class SectionContextGenerator(FullPhaseContextGenerator):
38+
@override
39+
def generate(self, caller: Caller, n: pending_node) -> Context:
40+
if n.parent:
41+
return proxy(find_current_section(n.parent))
42+
elif isinstance(caller, SphinxDirective):
43+
return proxy(find_current_section(caller.state.parent))
44+
elif isinstance(caller, SphinxRole):
45+
return proxy(caller.inliner.parent)
46+
else:
47+
assert False
48+
49+
50+
class SphinxEnvContextGenerator(FullPhaseContextGenerator):
51+
@override
52+
def generate(self, caller: Caller, n: pending_node) -> Context:
53+
return proxy(caller.env)
54+
55+
class SphinxConfigContextGenerator(FullPhaseContextGenerator):
56+
@override
57+
def generate(self, caller: Caller, n: pending_node) -> Context:
58+
return proxy(caller.config)
59+
60+
EXTRACTX_REGISTRY.add_parsing_phase_context('markup', MarkupContextGenerator())
61+
EXTRACTX_REGISTRY.add_full_phase_context('doc', DocContextGenerator())
62+
EXTRACTX_REGISTRY.add_full_phase_context('section',SectionContextGenerator())
63+
EXTRACTX_REGISTRY.add_full_phase_context('env', SphinxEnvContextGenerator())
64+
EXTRACTX_REGISTRY.add_full_phase_context('config', SphinxConfigContextGenerator())

src/sphinxnotes/data/render.py

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -197,46 +197,54 @@ def _resolve_external_content(self) -> list[nodes.Node]:
197197
# Extra context register and management.
198198
# ======================================
199199

200-
class ContextGenerator(ABC):
201-
name: str
200+
class ContextGenerator(ABC): ...
202201

203202
class FullPhaseContextGenerator(ContextGenerator):
204203
@abstractmethod
205-
def generate(self, caller: Caller, node: pending_node) -> dict[str, Any]: ...
204+
def generate(self, caller: Caller, n: pending_node) -> Context: ...
206205

207206
class ParsePhaseContextGenerator(ContextGenerator):
208207
@abstractmethod
209-
def generate(self, caller: ParseCaller, node: pending_node) -> dict[str, Any]: ...
208+
def generate(self, caller: ParseCaller, n: pending_node) -> Context: ...
210209

211210
class TransformPhaseContextGenerator(ContextGenerator):
212211
@abstractmethod
213-
def generate(self, caller: TransformCaller, node: pending_node) -> dict[str, Any]: ...
212+
def generate(self, caller: TransformCaller, n: pending_node) -> Context: ...
214213

215214
class ExtraContextRegistry:
215+
names: set[str]
216216
full: dict[str, FullPhaseContextGenerator]
217217
parsing: dict[str, ParsePhaseContextGenerator]
218218
parsed: dict[str, ParsePhaseContextGenerator]
219219
post_transform: dict[str, TransformPhaseContextGenerator]
220220

221221
def __init__(self) -> None:
222+
self.names = set()
222223
self.full = {}
223-
self.parsing_ctx = {}
224+
self.parsing= {}
224225
self.parsed = {}
225226
self.post_transform = {}
226227

227-
def add_full_phase_context(self, ctxgen: FullPhaseContextGenerator):
228-
self.full['_'+ctxgen.name] = ctxgen
228+
def _name_dedup(self, name: str) -> None:
229+
if name in self.names:
230+
raise ValueError(f'Context generator {name} already exists')
231+
self.names.add(name)
232+
233+
def add_full_phase_context(self, name: str, ctxgen: FullPhaseContextGenerator):
234+
self._name_dedup(name)
235+
self.full['_'+name] = ctxgen
229236

230-
def add_parse_phase_context(
231-
self, phase: Literal[Phase.Parsing, Phase.Parsed], ctxgen: ParsePhaseContextGenerator,
232-
) -> None:
233-
if phase == Phase.Parsing:
234-
self.parsing['_'+ctxgen.name] = ctxgen
235-
else:
236-
self.parsed['_'+ctxgen.name] = ctxgen
237+
def add_parsing_phase_context(self, name: str, ctxgen: ParsePhaseContextGenerator) -> None:
238+
self._name_dedup(name)
239+
self.parsing['_'+name] = ctxgen
240+
241+
def add_parsed(self, name: str, ctxgen: ParsePhaseContextGenerator) -> None:
242+
self._name_dedup(name)
243+
self.parsed['_'+name] = ctxgen
237244

238-
def add_transform_generator(self, ctxgen: TransformPhaseContextGenerator) -> None:
239-
self.post_transform['_'+ctxgen.name] = ctxgen
245+
def add_transform_generator(self, name: str, ctxgen: TransformPhaseContextGenerator) -> None:
246+
self._name_dedup(name)
247+
self.post_transform['_'+name] = ctxgen
240248

241249

242250
EXTRACTX_REGISTRY = ExtraContextRegistry()
@@ -246,49 +254,40 @@ class _ExtraContextGenerator:
246254
@classmethod
247255
def safe_generate(cls, name: str, gen: Callable[[], None], node: pending_node):
248256
try:
249-
# ctxgen.generate can be user-defined code,
250-
# exception of any kind are possible.
257+
# ctxgen.generate can be user-defined code, exception of any kind are possible.
251258
gen()
252259
except Exception:
253-
reporter = utils.Reporter(f'Failed to generate extra context {name}:')
260+
reporter = utils.Reporter(f'Failed to generate extra context {name}:', '')
254261
reporter.code(traceback.format_exc())
255262
node += reporter
256263

257264
@classmethod
258265
def full(cls, caller: Caller, n: pending_node) -> None:
259-
ctx = {}
260266
for name, ctxgen in EXTRACTX_REGISTRY.full.items():
261267
def gen():
262-
ctx[name] = ctxgen.generate(caller, n),
268+
n.extra[name] = ctxgen.generate(caller, n)
263269
cls.safe_generate(name, gen, n)
264-
n.extra.append(ctx)
265270

266271
@classmethod
267272
def on_parsing(cls, caller: ParseCaller, n: pending_node) -> None:
268-
ctx = {}
269273
for name, ctxgen in EXTRACTX_REGISTRY.parsing.items():
270274
def gen():
271-
ctx[name] = ctxgen.generate(caller, n),
275+
n.extra[name] = ctxgen.generate(caller, n)
272276
cls.safe_generate(name, gen, n)
273-
n.extra.append(ctx)
274277

275278
@classmethod
276279
def on_parsed(cls, caller: ParseCaller, n: pending_node) -> None:
277-
ctx = {}
278280
for name, ctxgen in EXTRACTX_REGISTRY.parsed.items():
279281
def gen():
280-
ctx[name] = ctxgen.generate(caller, n),
282+
n.extra[name] = ctxgen.generate(caller, n)
281283
cls.safe_generate(name, gen, n)
282-
n.extra.append(ctx)
283284

284285
@classmethod
285286
def on_post_transform(cls, caller: TransformCaller, n: pending_node) -> None:
286-
ctx = {}
287287
for name, ctxgen in EXTRACTX_REGISTRY.post_transform.items():
288288
def gen():
289-
ctx[name] = ctxgen.generate(caller, n),
289+
n.extra[name] = ctxgen.generate(caller, n)
290290
cls.safe_generate(name, gen, n)
291-
n.extra.append(ctx)
292291

293292
# ===============
294293
# Render workflow

src/sphinxnotes/data/template.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
from __future__ import annotations
22
from dataclasses import dataclass
33
from pprint import pformat
4-
from typing import Any, Callable
4+
from typing import TYPE_CHECKING
55
from enum import Enum
66

77
from docutils import nodes
88
from docutils.parsers.rst import directives
99
from sphinx.util import logging
10-
from sphinx.application import Sphinx
11-
from sphinx.builders import Builder
1210
from jinja2.sandbox import SandboxedEnvironment
1311
from jinja2 import StrictUndefined, DebugUndefined
1412

1513
from .data import Data
1614
from .utils import Reporter
15+
from .utils.ctxproxy import Proxy
16+
17+
if TYPE_CHECKING:
18+
from typing import Any, Callable
19+
from .utils.ctxproxy import Proxy
20+
from sphinx.builders import Builder
21+
from sphinx.application import Sphinx
22+
1723

1824
logger = logging.getLogger(__name__)
1925

@@ -36,7 +42,7 @@ def option_spec(cls, arg):
3642
return cls[choice.title()]
3743

3844

39-
type Context = Data | dict[str, Any]
45+
type Context = Data | dict[str, Any] | Proxy
4046

4147

4248
@dataclass
@@ -58,7 +64,7 @@ def render(
5864
continue
5965
finalctx[name] = self._resolve(ectx)
6066

61-
text = self._render(mainctx)
67+
text = self._render(finalctx)
6268
ns = parser(text)
6369

6470
if self.debug:
@@ -86,11 +92,13 @@ def render(
8692

8793
return ns
8894

89-
def _resolve(self, ctx: Context) -> dict[str, Any]:
95+
def _resolve(self, ctx: Context) -> dict[str, Any] | Proxy:
9096
if isinstance(ctx, Data):
9197
return ctx.as_context()
9298
elif isinstance(ctx, dict):
9399
return ctx
100+
if isinstance(ctx, Proxy):
101+
return ctx
94102

95103
def _render(self, ctx: dict[str, Any]) -> str:
96104
extensions = [

src/sphinxnotes/data/utils/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,25 @@ def find_titular_node_upward(node: nodes.Element | None) -> nodes.Element | None
8383

8484

8585
class Reporter(nodes.system_message):
86+
level: str
87+
8688
def __init__(
8789
self,
8890
title: str,
8991
level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR'] = 'DEBUG',
9092
) -> None:
9193
super().__init__(title + ':', type=level, level=2, source='')
94+
self.log(title)
9295

9396
def report(self, node: nodes.Node) -> None:
9497
self += node
95-
# TODO: print log here
98+
self.log(f'report: {node.astext()}')
99+
100+
def log(self, msg: str) -> None:
101+
if self['type'] in 'ERROR':
102+
logger.error(msg)
103+
elif self['type'] in 'WARNING':
104+
logger.warning(msg)
96105

97106
def text(self, text: str) -> None:
98107
self.report(nodes.paragraph(text, text))

src/sphinxnotes/data/utils/context_proxy.py renamed to src/sphinxnotes/data/utils/ctxproxy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,5 @@ class Config(Proxy):
145145
}
146146

147147

148-
def proxy(v: Any) -> Proxy | None:
149-
return Proxy._wrap(v) if v is not None else None
148+
def proxy(v: Any) -> Proxy:
149+
return Proxy._wrap(v)

0 commit comments

Comments
 (0)