1212from typing import TYPE_CHECKING , override , cast , TypeVar
1313from pprint import pformat
1414
15+ from sphinxnotes .data .data import PendingData
16+
1517from .indexers import LiteralIndexer , PathIndexer , YearIndexer , MonthIndexer
1618
1719from docutils import nodes
2325from sphinx .errors import ExtensionError
2426from sphinxnotes .data import (
2527 PlainValue ,
28+ RawData ,
29+ Template ,
2630 ValueWrapper ,
2731 ParsedData ,
2832 Schema ,
2933 pending_node ,
30- rendered_node ,
3134 StrictDataDefineDirective ,
3235)
33- from sphinxnotes .data .utils import Report , find_nearest_block_element , find_parent
36+ from sphinxnotes .data .utils import (
37+ Report ,
38+ find_nearest_block_element ,
39+ find_parent ,
40+ find_titular_node_upward ,
41+ )
3442
3543from .obj import (
3644 Object ,
3745 RefType ,
46+ ObjTypeDef ,
3847 Templates ,
3948 Category ,
4049 Indexer ,
@@ -163,14 +172,26 @@ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
163172 """Publish methods."""
164173
165174 @classmethod
166- def add_objtype (cls , objtype : str , schema : Schema , tmpls : Templates ) -> None :
175+ def add_objtype (cls , objtype : str , typedef : ObjTypeDef ) -> None :
176+ schema = typedef .schema
177+ tmpls = typedef .templates
178+
167179 cls .schemas [objtype ] = schema
168180 cls .templates [objtype ] = tmpls
169181
170- # Create a directive for defining object.
171- cls .directives [objtype ] = ObjDefineDirective .derive (
172- objtype , schema , tmpls .content
173- )
182+ # Create a directive for documenting object.
183+ if typedef .auto :
184+ cls .directives [objtype ] = AutoObjDefineDirective .derive (
185+ objtype ,
186+ schema ,
187+ tmpls .obj ,
188+ )
189+ else :
190+ cls .directives [objtype ] = ObjDefineDirective .derive (
191+ objtype ,
192+ schema ,
193+ tmpls .obj ,
194+ )
174195
175196 def mkrole (reftype : RefType ):
176197 """Create and register role for referencing object."""
@@ -265,73 +286,83 @@ def _get_index_anchor(self, reftype: str, refval: str) -> tuple[str, str]:
265286
266287
267288class ObjDefineDirective (StrictDataDefineDirective ):
289+ """Methods that override from parent."""
290+
268291 @override
269292 def process_pending_node (self , n : pending_node ) -> bool :
270293 if n .template == self .template :
271- n .hook_rendered_node (self ._setup_objdesc )
272-
294+ n .hook_rendered_nodes (self ._build_objdesc )
273295 return super ().process_pending_node (n )
274296
275- def _setup_objdesc (self , pending : pending_node , rendered : rendered_node ) -> None :
297+ """Methods used internal."""
298+
299+ def _build_objdesc (self , pending : pending_node , rendered : list [nodes .Node ]) -> None :
276300 """Wrap rendered.children into ObjectDescription.
277301
278302 TODO: considier inherit from ObjectDescription directive?
279303
280304 Before::
281305
282- <rendered_node>
283- <...> # children
306+ <nodes...> # the pass-in argument: ``ns``
284307
285308 After::
286309
287- <rendered_node>
288- <desc>
289- <desc_signature>
290- <desc_name>
291- <pending_node> # header, wait for rendering
292- <desc_content>
293- <...> # the original children
310+ <desc>
311+ <desc_signature>
312+ <desc_name>
313+ <pending_node> # header, wait for rendering
314+ <desc_content>
315+ <nodes...> # the original ``ns``
294316 """
295-
296- domain , objtype = self ._get_obj_domain_and_type ()
297-
298- def update_domaon_atts (node : nodes .Element ):
299- """Attach domain related info to node."""
300- node ['domain' ] = domain .name
301- # 'desctype' is a backwards compatible attribute
302- node ['objtype' ] = node ['desctype' ] = objtype
303- node ['classes' ].extend ([domain .name , objtype ])
317+ domain , objtype = self .get_obj_domain_and_type ()
304318
305319 if (hdrtmpl := domain .templates [objtype ].header ) is None :
306320 # No header template available, no need to generate objdesc.
307- update_domaon_atts (rendered )
308321 return
309322
310323 # Queue a rendering for header, and setup anchor when rendering done.
311324 hdrnode = pending_node (pending .data , hdrtmpl , inline = True )
312- hdrnode .hook_rendered_node (self ._setup_objdesc_anchor )
325+ hdrnode .hook_rendered_nodes (self ._setup_signode_anchor )
313326 self .queue_pending_node (hdrnode )
314327
315328 # Construct ObjectDescription.
316329 signode = addnodes .desc_signature ('' , '' , addnodes .desc_name ('' , '' , hdrnode ))
317- contnode = addnodes .desc_content ('' , * rendered . children )
330+ contnode = addnodes .desc_content ('' , * rendered )
318331 descnode = addnodes .desc ('' , signode , contnode )
319- update_domaon_atts (descnode )
332+ self . update_domain_atts (descnode )
320333
321- # Add descnode as child of rendered .
334+ # Replace the pass-in node list .
322335 rendered .clear ()
323- rendered += descnode
336+ rendered . append ( descnode )
324337
325- def _setup_objdesc_anchor (
326- self , pending : pending_node , rendered : rendered_node
338+ def _setup_signode_anchor (
339+ self , pending : pending_node , rendered : list [ nodes . Node ]
327340 ) -> None :
328- domain , objtype = self ._get_obj_domain_and_type ()
329-
330341 ahrnode = find_parent (pending , addnodes .desc_signature )
331342 assert ahrnode
332- obj = rendered .data
343+ obj = pending .data
333344 assert isinstance (obj , ParsedData )
334345
346+ self .setup_anchor (ahrnode , obj )
347+
348+ """Helpers methods for self and subclasses."""
349+
350+ def get_obj_domain_and_type (self ) -> tuple [ObjDomain , str ]:
351+ domainname , _ , objtype = self .name .partition (':' )
352+ _domain = self .env .get_domain (domainname )
353+ return cast (ObjDomain , _domain ), objtype
354+
355+ def update_domain_atts (self , node : nodes .Element ):
356+ """Attach domain related info to node."""
357+ domain , objtype = self .get_obj_domain_and_type ()
358+ node ['domain' ] = domain .name
359+ # 'desctype' is a backwards compatible attribute
360+ node ['objtype' ] = node ['desctype' ] = objtype
361+ node ['classes' ].extend ([domain .name , objtype ])
362+
363+ def setup_anchor (self , ahrnode : nodes .Element , obj : Object ) -> None :
364+ domain , objtype = self .get_obj_domain_and_type ()
365+
335366 objids = get_object_uniq_ids (self .schema , obj )
336367 ahrterm = ValueWrapper (objids [0 ]).as_str () if objids else None
337368 ahrid = make_id (self .env , self .state .document , prefix = objtype , term = ahrterm )
@@ -351,8 +382,8 @@ def _setup_objdesc_anchor(
351382 report = Report (
352383 'Duplicate identifier' ,
353384 'INFO' ,
354- source = pending .source ,
355- line = pending .line ,
385+ source = ahrnode .source ,
386+ line = ahrnode .line ,
356387 )
357388 report .text (
358389 f'Duplicate object identifier at { self .env .docname } #{ ahrid } , '
@@ -364,10 +395,82 @@ def _setup_objdesc_anchor(
364395 blkparent = find_nearest_block_element (ahrnode ) or self .state .document
365396 blkparent += report
366397
367- def _get_obj_domain_and_type (self ) -> tuple [ObjDomain , str ]:
368- domainname , objtype = self .name .split (':' , 1 )
369- _domain = self .env .get_domain (domainname )
370- return cast (ObjDomain , _domain ), objtype
398+
399+ class AutoObjDefineDirective (ObjDefineDirective ):
400+ """Methods that override from parent."""
401+
402+ @override
403+ @classmethod
404+ def derive (
405+ cls , name : str , schema : Schema , tmpl : Template
406+ ) -> type [StrictDataDefineDirective ]:
407+ subcls = super ().derive (name , schema , tmpl )
408+ if schema .name and schema .name .required :
409+ # data.name is resolved from external.
410+ subcls .required_arguments = 0
411+ subcls .optional_arguments = 1
412+ return subcls
413+
414+ @override
415+ def process_pending_node (self , n : pending_node ) -> bool :
416+ if (
417+ n .template == self .template
418+ and isinstance (n .data , PendingData )
419+ and self ._require_external_header (n .data .schema , n .data .raw )
420+ ):
421+ n .hook_raw_data (self ._resolve_external_header )
422+ return super (StrictDataDefineDirective , self ).process_pending_node (n )
423+
424+ return super ().process_pending_node (n )
425+
426+ """Methods used internal."""
427+
428+ def _require_external_header (self , schema : Schema , data : RawData ) -> bool :
429+ # If the data.name is not given (None) or a underscore('_'), we think
430+ # the object requires an external name.
431+ #
432+ # The special underscore is for compatible with sphinxnotes-any<3.
433+ # See also https://sphinx.silverrainz.me/any/tips.html#documenting-section-and-documentation
434+ if not schema .name :
435+ return False
436+ if schema .name .ctype is None :
437+ return data .name in (None , '_' )
438+ # HACK: We have to parse the data.name here.
439+ try :
440+ val = schema .name .parse (data .name )
441+ except ValueError :
442+ return False
443+ return ValueWrapper (val ).as_str () == '_'
444+
445+ def _resolve_external_header (self , pending : pending_node , raw : RawData ) -> None :
446+ domain , objtype = self .get_obj_domain_and_type ()
447+
448+ if (hdrtmpl := domain .templates [objtype ].header ) is None :
449+ # No header template available, no need to generate objdesc.
450+ return
451+ if not (title := find_titular_node_upward (self .state .parent )):
452+ return
453+
454+ if raw .name is None :
455+ raw .name = title .astext ()
456+ else :
457+ # HACK: See also _require_external_header.
458+ # TODO: Introduce a new extra context?
459+ raw .name = raw .name .replace ('_' , title .astext (), count = 1 )
460+
461+ pending_title = pending_node (pending .data , hdrtmpl , inline = True )
462+ pending_title .hook_rendered_nodes (self ._setup_external_anchor )
463+ self .queue_pending_node (pending_title )
464+
465+ # Replace title's children with pending node.
466+ title .clear ()
467+ title += pending_title
468+
469+ def _setup_external_anchor (
470+ self , pending : pending_node , rendered : list [nodes .Node ]
471+ ) -> None :
472+ assert isinstance (pending .data , ParsedData )
473+ self .setup_anchor (pending .parent , pending .data )
371474
372475
373476# =================
0 commit comments