|
| 1 | +""" |
| 2 | +sphinxnotes.any.directives |
| 3 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 4 | +
|
| 5 | +Directive implementations. |
| 6 | +
|
| 7 | +:copyright: Copyright 2021 Shengyu Zhang |
| 8 | +:license: BSD, see LICENSE for details. |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | +from typing import Type |
| 13 | + |
| 14 | +from docutils import nodes |
| 15 | +from docutils.nodes import Node, Element, fully_normalize_name |
| 16 | +from docutils.statemachine import StringList |
| 17 | +from docutils.parsers.rst import directives |
| 18 | +from sphinx import addnodes |
| 19 | +from sphinx.util.docutils import SphinxDirective |
| 20 | +from sphinx.util.nodes import make_id, nested_parse_with_titles |
| 21 | +from sphinx.util import logging |
| 22 | + |
| 23 | +from .objects import Schema, Object |
| 24 | + |
| 25 | +logger = logging.getLogger(__name__) |
| 26 | + |
| 27 | + |
| 28 | +def anchor(arg: str): |
| 29 | + return directives.choice(arg, ['no', 'section', 'objdesc']) |
| 30 | + |
| 31 | +class ViewDirective(SphinxDirective): |
| 32 | + # Member of parent |
| 33 | + has_content = True |
| 34 | + required_arguments = 1 |
| 35 | + optional_arguments = 0 |
| 36 | + final_argument_whitespace = True |
| 37 | + option_spec = { |
| 38 | + 'jinja': directives.unchanged, |
| 39 | + 'anchor': anchor, |
| 40 | + } |
| 41 | + |
| 42 | + def _fetch_data_source(self, src: str) -> dict[str, str]: |
| 43 | + return self.env.current_document['sphinxnotes-any-data-src'] |
| 44 | + |
| 45 | + def _setup_nodes( |
| 46 | + self, obj: Object, sectnode: Element, ahrnode: Element | None, contnode: Element |
| 47 | + ) -> None: |
| 48 | + """ |
| 49 | + Attach necessary informations to nodes and note them. |
| 50 | +
|
| 51 | + The necessary information contains: domain info, basic attributes for nodes |
| 52 | + (ids, names, classes...), name of anchor, description content and so on. |
| 53 | +
|
| 54 | + :param sectnode: Section node, used as container of the whole object description |
| 55 | + :param ahrnode: Anchor node, used to mark the location of object description |
| 56 | + :param contnode: Content node, which contains the description content |
| 57 | + """ |
| 58 | + # Setup anchor |
| 59 | + if ahrnode is not None: |
| 60 | + _, objid = self.schema.identifier_of(obj) |
| 61 | + ahrid = make_id(self.env, self.state.document, prefix=objtype, term=objid) |
| 62 | + ahrnode['ids'].append(ahrid) |
| 63 | + # Add object name to node's names attribute. |
| 64 | + # 'names' is space-separated list containing normalized reference |
| 65 | + # names of an element. |
| 66 | + name = self.schema.name_of(obj) |
| 67 | + if isinstance(name, str): |
| 68 | + ahrnode['names'].append(fully_normalize_name(name)) |
| 69 | + elif isinstance(name, list): |
| 70 | + ahrnode['names'].extend([fully_normalize_name(x) for x in name]) |
| 71 | + self.state.document.note_explicit_target(ahrnode) |
| 72 | + # Note object by docu fields |
| 73 | + domain.note_object( |
| 74 | + self.env.docname, ahrid, self.schema, obj |
| 75 | + ) # FIXME: Cast to AnyDomain |
| 76 | + |
| 77 | + # Parse description |
| 78 | + nested_parse_with_titles( |
| 79 | + self.state, StringList(self.schema.render_description(obj)), contnode |
| 80 | + ) |
| 81 | + |
| 82 | + def _run_section(self, obj: Object) -> list[Node]: |
| 83 | + # Get the title of the "section" where the directive is located |
| 84 | + sectnode = self.state.parent |
| 85 | + titlenode = sectnode.next_node(nodes.title) |
| 86 | + if not titlenode or titlenode.parent != sectnode: |
| 87 | + # Title should be direct child of section |
| 88 | + msg = 'Failed to get title of current section' |
| 89 | + logger.warning(msg, location=sectnode) |
| 90 | + sm = nodes.system_message( |
| 91 | + msg, type='WARNING', level=2, backrefs=[], source='' |
| 92 | + ) |
| 93 | + sectnode += sm |
| 94 | + title = '' |
| 95 | + else: |
| 96 | + title = titlenode.astext() |
| 97 | + # Replace the first name "_" with section title |
| 98 | + name = title + obj.name[1:] |
| 99 | + # Object is immutable, so create a new one |
| 100 | + obj = self.schema.object(name=name, attrs=obj.attrs, content=obj.content) |
| 101 | + # NOTE: In _setup_nodes, the anchor node(ahrnode) will be noted by |
| 102 | + # `note_explicit_target` for ahrnode, while `sectnode` is already noted |
| 103 | + # by `note_implicit_target`. |
| 104 | + # Multiple `note_xxx_target` calls to same node causes undefined behavior, |
| 105 | + # so we use `titlenode` as anchor node |
| 106 | + # |
| 107 | + # See https://github.com/sphinx-notes/any/issues/18 |
| 108 | + self._setup_nodes(obj, sectnode, titlenode, sectnode) |
| 109 | + # Add all content to existed section, so return nothing |
| 110 | + return [] |
| 111 | + |
| 112 | + def _run_objdesc(self, obj: Object) -> list[Node]: |
| 113 | + descnode = addnodes.desc() |
| 114 | + |
| 115 | + # Generate signature node |
| 116 | + title = self.schema.title_of(obj) |
| 117 | + if title is None: |
| 118 | + # Use non-generated object ID as replacement of title |
| 119 | + idfield, objid = self.schema.identifier_of(obj) |
| 120 | + title = objid if idfield is not None else None |
| 121 | + if title is not None: |
| 122 | + signode = addnodes.desc_signature(title, '') |
| 123 | + signode += addnodes.desc_name(title, title) |
| 124 | + descnode.append(signode) |
| 125 | + else: |
| 126 | + signode = None |
| 127 | + |
| 128 | + # Generate content node |
| 129 | + contnode = addnodes.desc_content() |
| 130 | + descnode.append(contnode) |
| 131 | + self._setup_nodes(obj, descnode, signode, contnode) |
| 132 | + return [descnode] |
| 133 | + |
| 134 | + def run(self) -> list[Node]: |
| 135 | + obj = self._build_object() |
| 136 | + if self.schema.title_of(obj) == '_': |
| 137 | + # If no argument is given, or the first argument is '_', |
| 138 | + # use the section title as object name and anchor, |
| 139 | + # append content nodes to section node |
| 140 | + return self._run_section(obj) |
| 141 | + else: |
| 142 | + # Else, create Sphinx ObjectDescription(sphinx.addnodes.dsec_*) |
| 143 | + return self._run_objdesc(obj) |
0 commit comments