Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/sphinxnotes/any/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
Optional('header', default='{{ name }}'): Or(str, type(None)),
Optional('ref', default='{{ name }}'): str,
Optional('ref_by', default={}): {str: str},
Optional('embed', default=None): Or(str, type(None)),
},
Optional('auto', default=False): bool,
Optional('debug', default=False): bool,
}
)
Expand All @@ -60,10 +60,11 @@ def _validate_objtype_defines_dict(d: dict, config: Config) -> ObjTypeDef:
tmplsdef['header'],
tmplsdef['ref'],
tmplsdef['ref_by'],
tmplsdef['embed'],
debug=objdef['debug'],
)

auto = objdef['auto'] or config.git_object_auto
auto = True # hardcode for now

return ObjTypeDef(schema=schema, templates=tmpls, auto=auto)

Expand All @@ -88,11 +89,12 @@ def setup(app: Sphinx):
"""Sphinx extension entrypoint."""
meta.pre_setup(app)

app.setup_extension('sphinxnotes.data')
# The underlying components that provide rendering functionality.
# See also https://sphinx.silverrainz.me/data
app.setup_extension('sphinxnotes.data.render')

app.add_config_value('any_domain_name', 'obj', 'env', types=str)
app.add_config_value('any_domain_name', 'any', 'env', types=str)
app.add_config_value('any_object_types', {}, 'env', types=dict)
app.add_config_value('git_object_auto', True, 'env', types=bool)

app.connect('config-inited', _config_inited)

Expand Down
235 changes: 158 additions & 77 deletions src/sphinxnotes/any/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from sphinxnotes.data import (
Phase,
PlainValue,
RawData,
Template,
ValueWrapper,
ParsedData,
Expand Down Expand Up @@ -58,7 +57,7 @@
from .indexers import LiteralIndexer, PathIndexer, YearIndexer, MonthIndexer

if TYPE_CHECKING:
from typing import Iterator, Iterable
from typing import Iterator, Iterable, Callable, Literal
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment

Expand Down Expand Up @@ -310,24 +309,33 @@ def _get_index_anchor(


class ObjDefineDirective(StrictDataDefineDirective):
"""
Directive for registering new objects in the domain.
Handles ``.. any:objtype::``-like directives and creates object descriptions
(descnode) with anchors.
"""

"""Methods that override from parent."""

@override
def process_pending_node(self, n: pending_node) -> bool:
if n.template == self.template:
n.hook_rendered_nodes(self._build_objdesc)
n.hook_rendered_nodes(self.setup_objdesc)
return super().process_pending_node(n)

"""Methods used internal."""
"""Helpers methods for self and subclasses."""

def _build_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> None:
"""Wrap rendered.children into ObjectDescription.
def get_domain_and_type(self) -> tuple[ObjDomain, str]:
domainname, _, objtype = self.name.partition(':')
_domain = self.env.get_domain(domainname)
return cast(ObjDomain, _domain), objtype

TODO: considier inherit from ObjectDescription directive?
def setup_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> None:
"""Wrap rendered nodes into ObjectDescription.

Before::

<nodes...> # the pass-in argument: ``ns``
<nodes...> # the pass-in argument: ``rendered: list[nodes.Node]``

After::

Expand All @@ -336,7 +344,7 @@ def _build_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> N
<desc_name>
<pending_node> # header, wait for rendering
<desc_content>
<nodes...> # the original ``ns``
<nodes...> # the original ``rendered``
"""
domain, objtype = self.get_domain_and_type()

Expand All @@ -353,7 +361,7 @@ def _build_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> N

# Queue a rendering for header, and setup anchor when rendering done.
hdrnode = pending_node(pending.ctx, hdrtmpl, inline=True)
hdrnode.hook_rendered_nodes(self._setup_signode_anchor)
hdrnode.hook_rendered_nodes(self.setup_signode_anchor)
self.queue_pending_node(hdrnode)

# Construct ObjectDescription.
Expand All @@ -366,23 +374,6 @@ def _build_objdesc(self, pending: pending_node, rendered: list[nodes.Node]) -> N
rendered.clear()
rendered.append(descnode)

def _setup_signode_anchor(
self, pending: pending_node, rendered: list[nodes.Node]
) -> None:
ahrnode = find_parent(pending, addnodes.desc_signature)
assert ahrnode
obj = pending.ctx
assert isinstance(obj, ParsedData)

self.setup_anchor(ahrnode, obj)

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

def get_domain_and_type(self) -> tuple[ObjDomain, str]:
domainname, _, objtype = self.name.partition(':')
_domain = self.env.get_domain(domainname)
return cast(ObjDomain, _domain), objtype

def update_domain_atts(self, node: nodes.Element):
"""Attach domain related info to node."""
domain, objtype = self.get_domain_and_type()
Expand Down Expand Up @@ -426,8 +417,50 @@ def setup_anchor(self, ahrnode: nodes.Element, obj: Object) -> None:
blkparent = find_nearest_block_element(ahrnode) or self.state.document
blkparent += report

def setup_signode_anchor(
self, pending: pending_node, rendered: list[nodes.Node]
) -> None:
ahrnode = find_parent(pending, addnodes.desc_signature)
assert ahrnode
obj = pending.ctx
assert isinstance(obj, ParsedData)

self.setup_anchor(ahrnode, obj)


class AutoObjDefineDirective(ObjDefineDirective):
"""
Directive that extends ObjDefineDirective that allows object to have
header outside of object's nodes rather than a :cls:`addnodse.desc_signature`
in :cls:`addnodes.desc`. For example, if we have the follow templates::

.. code:: python

# ...
'obj': "Hi there, human! I am {{ name }}.",
'header': 'This is {{ name }}',
# ...

Define a cat:

.. code:: rst

Mimi
====

.. cat:: _

The title "Mimi" will be used as cat's name, and the nodes.title will be
rendered by cat's header template, the result will look like:

.. code:: rst

This is Mimi
============

Hi there, human! I am mimi.
"""

"""Methods that override from parent."""

@override
Expand All @@ -444,70 +477,119 @@ def derive(

@override
def process_pending_node(self, n: pending_node) -> bool:
if (
n.template == self.template
and isinstance(n.ctx, UnparsedData)
and self._require_external_header(n.ctx.schema, n.ctx.raw)
):
n.hook_pending_context(self._resolve_external_header)
if n.template == self.template:
n.hook_pending_context(self.setup_external_header)
# Skip ObjDefineDirective.process_pending_node.
return super(StrictDataDefineDirective, self).process_pending_node(n)

return super().process_pending_node(n)

"""Methods used internal."""

def _require_external_header(self, schema: Schema, data: RawData) -> bool:
# If the data.name is not given (None) or a underscore('_'), we think
# the object requires an external name.
#
# The special underscore is for compatible with sphinxnotes-any<3.
# See also https://sphinx.silverrainz.me/any/tips.html#documenting-section-and-documentation
if not schema.name or not schema.name.required:
return False
if schema.name.ctype is None:
return data.name in (None, '_')
# HACK: We have to parse the data.name here.
try:
val = schema.name.parse(data.name)
except ValueError:
return False
return ValueWrapper(val).as_str() == '_'
"""Helpers methods for self and subclasses."""

def _resolve_external_header(
self, pending: pending_node, ctx: PendingContext
def setup_external_header(
self, pending: pending_node, ctx: PendingContext | ResolvedContext
) -> None:
domain, objtype = self.get_domain_and_type()
def fallback():
# If we can not setup external header, fallback to setup objdesc.
pending.hook_rendered_nodes(self.setup_objdesc)

required, update_ctx_name = self.require_external_header(ctx)
if required is not True:
if required == 'Retry':
pending.hook_resolved_context(self.setup_external_header)
else:
fallback()
return
assert update_ctx_name

domain, objtype = self.get_domain_and_type()
if (hdrtmpl := domain.templates[objtype].header) is None:
# No header template available, no need to generate objdesc.
fallback()
return
if not (title := find_titular_node_upward(self.state.parent)):

if not (header := self.resolve_external_header()):
fallback()
return

# Update the name field in ctx.
update_ctx_name(header.astext())

hdrnode = pending_node(ctx, hdrtmpl, inline=True)
hdrnode.hook_rendered_nodes(self.setup_external_header_anchor)
self.queue_pending_node(hdrnode)

# Replace header's children with pending node.
header.clear()
header += hdrnode

def require_external_header(
self, ctx: PendingContext | ResolvedContext
) -> tuple[Literal[True, False, 'Retry'], Callable[[str]] | None]:
"""
Check whether the context require a external name.

Returns:
:Literal:
:True: Required
:False: Don't required
:Retry: Don't know yet, retry on ``hook_resolved_context`` plz.
:Callable: A function for updating name, used by :meth:`_resolve_external_name`.
"""

if isinstance(ctx, UnparsedData):
# If the schema of RawData.name is a plain value (no a list)
# and RawData.name is not given (None) or a underscore('_'),
# we consider the object requires an external name.
#
# The special underscore is for compatible with sphinxnotes-any<3.
# See also https://sphinx.silverrainz.me/any/tips.html#documenting-section-and-documentation
if not ctx.schema.name or not ctx.schema.name.required:
return False, None
if ctx.schema.name.ctype is list and ctx.schema.name.etype is str:
return 'Retry', None
if ctx.schema.name.ctype is not None:
return False, None
if ctx.raw.name not in (None, '_'):
return False, None

def update_raw_name(name: str) -> None:
ctx.raw.name = name

return True, update_raw_name

elif isinstance(ctx, ParsedData):
# Most of the judgment is already done in the UnparseData branch.
# When the ctx.name is a list[str] and the first element is '_',
# we also consider it requires an external name.
if (
not isinstance(ctx.name, list)
or len(ctx.name) == 0
or ctx.name[0] != '_'
):
return False, None

def update_parsed_name(name: str) -> None:
assert isinstance(ctx.name, list)
ctx.name[0] = name

return True, update_parsed_name

return False, None

def resolve_external_header(self) -> nodes.Element | None:
domain, objtype = self.get_domain_and_type()

if not (title := find_titular_node_upward(self.state.parent)):
return None
if 'any-header' in title['classes']:
# Already header of other object.
return
return None
title['classes'].extend(
set(['any', domain.name, 'any-header', objtype + '-header'])
)
return title

raw = cast(UnparsedData, ctx).raw

if raw.name is None:
raw.name = title.astext()
else:
# HACK: See also _require_external_header.
# TODO: Introduce a new extra context?
raw.name = raw.name.replace('_', title.astext(), count=1)

pending_title = pending_node(pending.ctx, hdrtmpl, inline=True)
pending_title.hook_rendered_nodes(self._setup_external_anchor)
self.queue_pending_node(pending_title)

# Replace title's children with pending node.
title.clear()
title += pending_title

def _setup_external_anchor(
def setup_external_header_anchor(
self, pending: pending_node, rendered: list[nodes.Node]
) -> None:
assert isinstance(pending.ctx, ParsedData)
Expand Down Expand Up @@ -564,8 +646,7 @@ def current_template(self) -> Template:
)

domain, objtype = self.get_domain_and_type()
tmpl = domain.templates[objtype].embed
if tmpl:
if tmpl := domain.templates[objtype].embed:
return tmpl

self.assert_has_content()
Expand Down
4 changes: 2 additions & 2 deletions src/sphinxnotes/any/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def pre_setup(app: Sphinx) -> None:
def post_setup(app: Sphinx) -> ExtensionMetadata:
return {
'version': __version__,
'parallel_read_safe': False,
'parallel_write_safe': False,
'parallel_read_safe': True,
'parallel_write_safe': True,
}


Expand Down
2 changes: 0 additions & 2 deletions src/sphinxnotes/any/obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ class Templates:
obj: Template
header: Template | None

# embed: Templates

"""Templates for rendering corss references."""
ref: Template
ref_by: dict[str, Template]
Expand Down