Skip to content

Commit b6d5211

Browse files
authored
feat: Impl ObjEmbedDirective (#54)
Co-authored-by: MiniMax-M2.1 Co-authored-by: opencode
1 parent a3cbb4e commit b6d5211

3 files changed

Lines changed: 117 additions & 30 deletions

File tree

src/sphinxnotes/any/domain.py

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,31 @@
99
"""
1010

1111
from __future__ import annotations
12+
from dataclasses import dataclass
1213
from typing import TYPE_CHECKING, override, cast, TypeVar
1314
from pprint import pformat
1415

15-
from sphinxnotes.data.data import PendingData
16-
17-
from .indexers import LiteralIndexer, PathIndexer, YearIndexer, MonthIndexer
18-
1916
from docutils import nodes
17+
from docutils.parsers.rst import directives
2018
from sphinx import addnodes
2119
from sphinx.domains import Domain, ObjType, Index, IndexEntry
2220
from sphinx.roles import XRefRole
2321
from sphinx.util import logging
2422
from sphinx.util.nodes import make_id, make_refnode
2523
from sphinx.errors import ExtensionError
2624
from sphinxnotes.data import (
25+
Phase,
2726
PlainValue,
2827
RawData,
2928
Template,
3029
ValueWrapper,
3130
ParsedData,
3231
Schema,
32+
PendingContext,
33+
ResolvedContext,
3334
pending_node,
35+
BaseContextDirective,
36+
UnparsedData,
3437
StrictDataDefineDirective,
3538
)
3639
from sphinxnotes.data.utils import (
@@ -52,6 +55,7 @@
5255
get_object_refs,
5356
)
5457
from .utils import strip_rst_markups
58+
from .indexers import LiteralIndexer, PathIndexer, YearIndexer, MonthIndexer
5559

5660
if TYPE_CHECKING:
5761
from typing import Iterator, Iterable
@@ -169,7 +173,7 @@ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
169173
for (objtype, objid), (docname, anchor, _) in self.data['objects'].items():
170174
yield objid, objid, objtype, docname, anchor, 1
171175

172-
"""Publish methods."""
176+
"""Public methods."""
173177

174178
@classmethod
175179
def add_objtype(cls, objtype: str, typedef: ObjTypeDef) -> None:
@@ -179,7 +183,7 @@ def add_objtype(cls, objtype: str, typedef: ObjTypeDef) -> None:
179183
cls.schemas[objtype] = schema
180184
cls.templates[objtype] = tmpls
181185

182-
# Create a directive for documenting object.
186+
# Create directive ``.. objtype::`` for documenting object.
183187
if typedef.auto:
184188
cls.directives[objtype] = AutoObjDefineDirective.derive(
185189
objtype,
@@ -193,6 +197,9 @@ def add_objtype(cls, objtype: str, typedef: ObjTypeDef) -> None:
193197
tmpls.obj,
194198
)
195199

200+
# Create directive ``.. objtype+embed::`` for embedding object.
201+
cls.directives[objtype + '+embed'] = ObjEmbedDirective
202+
196203
def mkrole(reftype: RefType):
197204
"""Create and register role for referencing object."""
198205
role = XRefRole(
@@ -265,21 +272,34 @@ def note_object(
265272

266273
return otherobj
267274

275+
def get_rendered_object(self, objtype: str, objid: PlainValue) -> list[nodes.Node]:
276+
docname, anchor, _ = self.objects[objtype, objid]
277+
doctree = self.env.get_doctree(docname)
278+
ahrnode = doctree.ids[anchor]
279+
if isinstance(ahrnode, addnodes.desc_signature):
280+
return [ahrnode.parent]
281+
assert False # TODO: mark obj start and objend
282+
268283
"""Methods for inernal use"""
269284

270-
def _get_index_anchor(self, reftype: str, refval: str, location = None) -> tuple[str, str]:
285+
def _get_index_anchor(
286+
self, reftype: str, refval: str, location=None
287+
) -> tuple[str, str]:
271288
"""
272289
Return the docname and anchor name of index page. Can be used for ``make_refnode()``.
273290
274291
.. warning:: This is no public API of sphinx and may broken in future version.
275292
"""
276293
domain = self.name
277294
index = self.indices_for_reftype[reftype]
278-
try:
295+
try:
279296
anchor = index.indexer.anchor(refval)
280297
except Exception as e:
281298
anchor = ''
282-
logger.warning(f'failed to convert {repr(refval)} (xref target) to anchor: {e}', location=location)
299+
logger.warning(
300+
f'failed to convert {repr(refval)} (xref target) to anchor: {e}',
301+
location=location,
302+
)
283303

284304
return f'{domain}-{index.name}', anchor
285305

@@ -318,21 +338,21 @@ def _build_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> N
318338
<desc_content>
319339
<nodes...> # the original ``ns``
320340
"""
321-
domain, objtype = self.get_any_domain_and_type()
341+
domain, objtype = self.get_domain_and_type()
322342

323343
if (hdrtmpl := domain.templates[objtype].header) is None:
324344
# No header template available, no need to generate objdesc.
325345
return
326346
if (
327-
isinstance(pending.data, ParsedData)
328-
and pending.data.name is None
347+
isinstance(pending.ctx, ParsedData)
348+
and pending.ctx.name is None
329349
and '{{ name }}' in hdrtmpl.text
330350
):
331351
# HACK: do not generate signode when name is not given.
332352
return
333353

334354
# Queue a rendering for header, and setup anchor when rendering done.
335-
hdrnode = pending_node(pending.data, hdrtmpl, inline=True)
355+
hdrnode = pending_node(pending.ctx, hdrtmpl, inline=True)
336356
hdrnode.hook_rendered_nodes(self._setup_signode_anchor)
337357
self.queue_pending_node(hdrnode)
338358

@@ -351,28 +371,28 @@ def _setup_signode_anchor(
351371
) -> None:
352372
ahrnode = find_parent(pending, addnodes.desc_signature)
353373
assert ahrnode
354-
obj = pending.data
374+
obj = pending.ctx
355375
assert isinstance(obj, ParsedData)
356376

357377
self.setup_anchor(ahrnode, obj)
358378

359379
"""Helpers methods for self and subclasses."""
360380

361-
def get_any_domain_and_type(self) -> tuple[ObjDomain, str]:
381+
def get_domain_and_type(self) -> tuple[ObjDomain, str]:
362382
domainname, _, objtype = self.name.partition(':')
363383
_domain = self.env.get_domain(domainname)
364384
return cast(ObjDomain, _domain), objtype
365385

366386
def update_domain_atts(self, node: nodes.Element):
367387
"""Attach domain related info to node."""
368-
domain, objtype = self.get_any_domain_and_type()
388+
domain, objtype = self.get_domain_and_type()
369389
node['domain'] = domain.name
370390
# 'desctype' is a backwards compatible attribute
371391
node['objtype'] = node['desctype'] = objtype
372392
node['classes'].extend([domain.name, objtype])
373393

374394
def setup_anchor(self, ahrnode: nodes.Element, obj: Object) -> None:
375-
domain, objtype = self.get_any_domain_and_type()
395+
domain, objtype = self.get_domain_and_type()
376396

377397
objids = get_object_uniq_ids(self.schema, obj)
378398
ahrterm = ValueWrapper(objids[0]).as_str() if objids else None
@@ -426,10 +446,10 @@ def derive(
426446
def process_pending_node(self, n: pending_node) -> bool:
427447
if (
428448
n.template == self.template
429-
and isinstance(n.data, PendingData)
430-
and self._require_external_header(n.data.schema, n.data.raw)
449+
and isinstance(n.ctx, UnparsedData)
450+
and self._require_external_header(n.ctx.schema, n.ctx.raw)
431451
):
432-
n.hook_raw_data(self._resolve_external_header)
452+
n.hook_pending_context(self._resolve_external_header)
433453
return super(StrictDataDefineDirective, self).process_pending_node(n)
434454

435455
return super().process_pending_node(n)
@@ -453,8 +473,10 @@ def _require_external_header(self, schema: Schema, data: RawData) -> bool:
453473
return False
454474
return ValueWrapper(val).as_str() == '_'
455475

456-
def _resolve_external_header(self, pending: pending_node, raw: RawData) -> None:
457-
domain, objtype = self.get_any_domain_and_type()
476+
def _resolve_external_header(
477+
self, pending: pending_node, ctx: PendingContext
478+
) -> None:
479+
domain, objtype = self.get_domain_and_type()
458480

459481
if (hdrtmpl := domain.templates[objtype].header) is None:
460482
# No header template available, no need to generate objdesc.
@@ -468,14 +490,16 @@ def _resolve_external_header(self, pending: pending_node, raw: RawData) -> None:
468490
set(['any', domain.name, 'any-header', objtype + '-header'])
469491
)
470492

493+
raw = cast(UnparsedData, ctx).raw
494+
471495
if raw.name is None:
472496
raw.name = title.astext()
473497
else:
474498
# HACK: See also _require_external_header.
475499
# TODO: Introduce a new extra context?
476500
raw.name = raw.name.replace('_', title.astext(), count=1)
477501

478-
pending_title = pending_node(pending.data, hdrtmpl, inline=True)
502+
pending_title = pending_node(pending.ctx, hdrtmpl, inline=True)
479503
pending_title.hook_rendered_nodes(self._setup_external_anchor)
480504
self.queue_pending_node(pending_title)
481505

@@ -486,11 +510,69 @@ def _resolve_external_header(self, pending: pending_node, raw: RawData) -> None:
486510
def _setup_external_anchor(
487511
self, pending: pending_node, rendered: list[nodes.Node]
488512
) -> None:
489-
assert isinstance(pending.data, ParsedData)
490-
self.setup_anchor(pending.parent, pending.data)
513+
assert isinstance(pending.ctx, ParsedData)
514+
self.setup_anchor(pending.parent, pending.ctx)
515+
516+
517+
@dataclass
518+
class PendingObject(PendingContext):
519+
domain: ObjDomain
520+
objtype: str
521+
objid: str
522+
523+
@override
524+
def resolve(self) -> ResolvedContext:
525+
_, _, obj = self.domain.objects[self.objtype, self.objid]
526+
return obj
527+
528+
def __hash__(self) -> int:
529+
return hash((self.domain.name, self.objtype, self.objid))
530+
491531

532+
class ObjEmbedDirective(BaseContextDirective):
533+
required_arguments = 1
534+
optional_arguments = 0
535+
option_spec = {
536+
'debug': directives.flag,
537+
}
538+
has_content = True
539+
540+
def get_domain_and_type(self) -> tuple[ObjDomain, str]:
541+
domainname, _, dirname = self.name.partition(':')
542+
objtype, _, _ = dirname.partition('+')
543+
_domain = self.env.get_domain(domainname)
544+
return cast(ObjDomain, _domain), objtype
545+
546+
def get_rendered_objects(self) -> list[nodes.Node]:
547+
domain, objtype = self.get_domain_and_type()
548+
objid = self.arguments[0]
549+
return domain.get_rendered_object(objtype, objid)
550+
551+
"""Methods that override from parent."""
552+
553+
@override
554+
def current_context(self) -> PendingContext | ResolvedContext:
555+
domain, objtype = self.get_domain_and_type()
556+
objid = self.arguments[0]
557+
return PendingObject(domain, objtype, objid)
558+
559+
@override
560+
def current_template(self) -> Template:
561+
if self.content:
562+
return Template(
563+
'\n'.join(self.content), Phase.Resolving, 'debug' in self.options
564+
)
492565

493-
# =================
566+
domain, objtype = self.get_domain_and_type()
567+
tmpl = domain.templates[objtype].embed
568+
if tmpl:
569+
return tmpl
570+
571+
self.assert_has_content()
572+
assert False
573+
574+
575+
# ==================
494576
# Index implemention
495577
# ==================
496578
#

src/sphinxnotes/any/meta.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def pre_setup(app: Sphinx) -> None:
3434
def post_setup(app: Sphinx) -> ExtensionMetadata:
3535
return {
3636
'version': __version__,
37-
'parallel_read_safe': True,
38-
'parallel_write_safe': True,
37+
'parallel_read_safe': False,
38+
'parallel_write_safe': False,
3939
}
4040

4141

src/sphinxnotes/any/obj.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ class Templates:
103103
"""Templates for rendering index entry."""
104104
# ...
105105

106+
"""Templates for rendering embedded object."""
107+
embed: Template | None
108+
106109
# TODO: more...?
107110

108111
def __init__(
@@ -111,14 +114,16 @@ def __init__(
111114
header: str | None,
112115
ref: str, # | None
113116
ref_by: dict[str, str] = {},
117+
embed: str | None = None,
114118
debug: bool = False,
115119
):
116120
self.obj = Template(content, Phase.Parsing, debug)
117121
self.header = Template(header, Phase.Parsing, debug) if header else None
118-
self.ref = Template(ref, Phase.PostTranform, debug)
122+
self.ref = Template(ref, Phase.Resolving, debug)
119123
self.ref_by = {
120-
f: Template(t, Phase.PostTranform, debug) for f, t in ref_by.items()
124+
f: Template(t, Phase.Resolving, debug) for f, t in ref_by.items()
121125
}
126+
self.embed = Template(embed, Phase.Resolving, debug) if embed else None
122127

123128
def get_ref_by(self, reftype: RefType) -> Template:
124129
if reftype.field is not None:

0 commit comments

Comments
 (0)