diff --git a/docs/api.rst b/docs/api.rst index a77c0de..2e7d12b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -78,18 +78,26 @@ See :doc:`tmpl` for the higher-level guide. Extra Context ------------- -See :doc:`tmpl` for built-in extra-context names such as ``_doc`` and -``_sphinx``, plus usage examples. +See :doc:`tmpl` for built-in extra-context names such as ``doc`` and +``sphinx``, plus usage examples. -.. autoclass:: sphinxnotes.render.GlobalExtraContxt +.. autofunction:: sphinxnotes.render.extra_context -.. autoclass:: sphinxnotes.render.ParsePhaseExtraContext +.. autoclass:: sphinxnotes.render.ParsingPhaseExtraContext + :members: phase, generate + :undoc-members: -.. autoclass:: sphinxnotes.render.ResolvePhaseExtraContext +.. autoclass:: sphinxnotes.render.ParsedPhaseExtraContext + :members: phase, generate + :undoc-members: -.. autoclass:: sphinxnotes.render.ExtraContextRegistry - :members: +.. autoclass:: sphinxnotes.render.ResolvingPhaseExtraContext + :members: phase, generate + :undoc-members: +.. autoclass:: sphinxnotes.render.GlobalExtraContext + :members: phase, generate + :undoc-members: Base Roles and Directives ------------------------- @@ -161,4 +169,3 @@ or add new extra context) by adding new items to .. autoclass:: sphinxnotes.render.Registry .. autoproperty:: data - .. autoproperty:: extra_context diff --git a/docs/tmpl.rst b/docs/tmpl.rst index 8349261..0734eef 100644 --- a/docs/tmpl.rst +++ b/docs/tmpl.rst @@ -153,54 +153,73 @@ the :rst:dir:`data.schema` directive. Extra Context ------------- -Templates may also receive extra context entries in addition to the main data -context. These entries are stored under names prefixed with ``_``. - -Built-in extra context -...................... - -.. list-table:: - :header-rows: 1 - - * - Name - - Available in - - Description - * - ``_sphinx`` - - all phases - - A proxy to the Sphinx application object. - * - ``_docutils`` - - all phases - - A mapping that exposes registered docutils directives and roles. - * - ``_markup`` - - parsing and later - - Information about the current directive or role invocation, such as its - type, name, source text, and line number. - * - ``_section`` - - parsing and later - - A proxy to the current section node, when one exists. - * - ``_doc`` - - parsing and later - - A proxy to the current document node. - -These values are wrapped for safer template access. In practice this means -templates can read public, non-callable attributes, but should not rely on -arbitrary Python object behavior. +Templates can access additional context through **extra context**. Extra context +must be explicitly declared using the :rst:dir:`templat:extra` option and loaded in the +template using the ``load_extra()`` function. + +Built-in Extra Contexts +~~~~~~~~~~~~~~~~~~~~~~~ + +``sphinx`` +.......... + +:Phase: all + +A proxy to the :py:class:`sphinx.application.Sphinx` object. + +``env`` +....... + +:Phase: all + +A proxy to the :py:class:`sphinx.environment.BuildEnvironment` object. + +``markup`` +.......... + +:Phase: :term:`parsing` and later + +Information about the current directive or role invocation, such as its +type, name, source text, and line number. + +``section`` +............ + +:Phase: :term:`parsing` and later + +A proxy to the current :py:class:`docutils.nodes.section` node, when one exists. + +``doc`` +....... + +:Phase: :term:`parsing` and later + +A proxy to the current :py:class:`docutils.notes.document` node. .. example:: :style: grid .. data.render:: + :extra: doc - Current document title is - "{{ _doc.title }}". + + Title of current document is + "{{ load_extra('doc').title }}". Extending extra context ....................... -Extension authors can register more context generators through -:py:data:`sphinxnotes.render.REGISTRY`. +Extension authors can register custom extra context using the +:py:func:`~sphinxnotes.render.extra_context` decorator. + +.. code-block:: python + + from sphinxnotes.render import extra_context, ParsingPhaseExtraContext -TODO. + @extra_context('custom') + class CustomExtraContext(ParsingPhaseExtraContext): + def generate(self, directive): + return {'info': 'custom data'} Template ======== @@ -213,64 +232,81 @@ Render Phases Each :py:class:`~sphinxnotes.render.Template` has a render phase controlled by :py:class:`~sphinxnotes.render.Phase`. -``parsing`` (:py:data:`sphinxnotes.render.Phase.Parsing`) - Render immediately while the directive or role is running. +.. glossary:: - This is the default render phase. - Choose this when the template only needs local information and does not rely - on the final doctree or cross-document state. + ``parsing`` + Corresponding to :py:data:`sphinxnotes.render.Phase.Parsing`. + Render immediately while the directive or role is running. - .. example:: - :style: grid + This is the default render phase. + Choose this when the template only needs local information and does not rely + on the final doctree or cross-document state. - .. data.render:: - :on: parsing + .. example:: + :style: grid - - The current document has - {{ _doc.sections | length }} - section(s). - - The current project has - {{ _sphinx.env.all_docs | length }} - document(s). + .. data.render:: + :on: parsing + :extra: doc env -``parsed`` (:py:data:`sphinxnotes.render.Phase.Parsed`) - Render after the current document has been parsed. + {% set doc = load_extra('doc') %} + {% set env = load_extra('env') %} - Choose this when the template needs the complete doctree of the current - document. + - The current document has + {{ doc.sections | length }} + section(s). + - The current project has + {{ env.all_docs | length }} + document(s). - .. example:: - :style: grid + ``parsed`` + Corresponding to :py:data:`sphinxnotes.render.Phase.Parsed`. + Render after the current document has been parsed. - .. data.render:: - :on: parsed + Choose this when the template needs the complete doctree of the current + document. - - The current document has - {{ _doc.sections | length }} - section(s). - - The current project has - {{ _sphinx.env.all_docs | length }} - document(s). + .. example:: + :style: grid -``resolving`` (:py:data:`sphinxnotes.render.Phase.Resolving`) - Render late in the build, after references and other transforms are being - resolved. + .. data.render:: + :on: parsed + :extra: doc env - Choose this when the template depends on project-wide state or on document - structure that is only stable near the end of the pipeline. + {% set doc = load_extra('doc') %} + {% set env = load_extra('env') %} - .. example:: - :style: grid + - The current document has + {{ doc.sections | length }} + section(s). + - The current project has + {{ env.all_docs | length }} + document(s). - .. data.render:: - :on: resolving + ``resolving`` + Corresponding to :py:data:`sphinxnotes.render.Phase.Resolving`. + Render late in the build, after references and other transforms are being + resolved. + + Choose this when the template depends on pr + structure that is only stable near the end of the pipeline. + + .. example:: + :style: grid - - The current document has - {{ _doc.sections | length }} - section(s). - - The current project has - {{ _sphinx.env.all_docs | length }} - document(s). + .. data.render:: + :on: resolving + :extra: doc env + + {% set doc = load_extra('doc') %} + {% set env = load_extra('env') %} + + - The current document has + {{ doc.sections | length }} + section(s). + - The current project has + {{ env.all_docs | length }} + document(s). Debugging --------- @@ -317,3 +353,9 @@ This pattern is often the most convenient way to build small, declarative directives. For more control, subclass :py:class:`~sphinxnotes.render.BaseDataDefineDirective` directly and implement ``current_schema()`` and ``current_template()`` yourself. + .. data.render:: + :extra: doc + + + Title of current document is + "{{ load_extra('doc').title }}". diff --git a/src/sphinxnotes/render/__init__.py b/src/sphinxnotes/render/__init__.py index 41c0530..5d7d42e 100644 --- a/src/sphinxnotes/render/__init__.py +++ b/src/sphinxnotes/render/__init__.py @@ -30,11 +30,11 @@ from .ctx import PendingContext, ResolvedContext from .ctxnodes import pending_node from .extractx import ( - ExtraContextRegistry, - ExtraContextGenerator, - GlobalExtraContxt, - ParsePhaseExtraContext, - ResolvePhaseExtraContext, + ParsingPhaseExtraContext, + ParsedPhaseExtraContext, + ResolvingPhaseExtraContext, + GlobalExtraContext, + extra_context, ) from .pipeline import BaseContextRole, BaseContextDirective from .sources import ( @@ -63,9 +63,11 @@ 'Host', 'PendingContext', 'ResolvedContext', - 'GlobalExtraContxt', - 'ParsePhaseExtraContext', - 'ResolvePhaseExtraContext', + 'ParsingPhaseExtraContext', + 'ParsedPhaseExtraContext', + 'ResolvingPhaseExtraContext', + 'GlobalExtraContext', + 'extra_context', 'pending_node', 'BaseContextRole', 'BaseContextDirective', @@ -83,13 +85,10 @@ class Registry: def data(self) -> DataRegistry: return DATA_REGISTRY - @property - def extra_context(cls) -> ExtraContextRegistry: - return ExtraContextGenerator.registry - REGISTRY = Registry() + def setup(app: Sphinx): meta.pre_setup(app) diff --git a/src/sphinxnotes/render/extractx.py b/src/sphinxnotes/render/extractx.py index 2647425..5bf57c3 100644 --- a/src/sphinxnotes/render/extractx.py +++ b/src/sphinxnotes/render/extractx.py @@ -1,159 +1,158 @@ from __future__ import annotations -from typing import TYPE_CHECKING, override +from typing import TYPE_CHECKING, ClassVar, override from abc import ABC, abstractmethod -from sphinx.util.docutils import SphinxDirective -from docutils.parsers.rst.directives import _directives -from docutils.parsers.rst.roles import _roles +from sphinx.util.docutils import SphinxDirective, SphinxRole +from sphinx.transforms import SphinxTransform -from .render import HostWrapper +from .render import HostWrapper, Phase from .ctxnodes import pending_node from .utils import find_current_section, Report, Reporter -from .utils.ctxproxy import proxy if TYPE_CHECKING: - from typing import Any, Callable, ClassVar + from typing import Any, Callable from sphinx.application import Sphinx - from .render import ParseHost, ResolveHost + from sphinx.environment import BuildEnvironment +# ============================ +# ExtraContext ABC definitions +# ============================ -class GlobalExtraContxt(ABC): - """Extra context available in any phase.""" + +class _ExtraContext(ABC): + """Base class of extra context.""" + + phase: ClassVar[Phase | None] = None @abstractmethod - def generate(self) -> Any: ... + def generate(self, *args, **kwargs) -> Any: ... -class ParsePhaseExtraContext(ABC): - """Extra context generated during the :py:class:`~Phase.Parsing` phase.""" +class ParsingPhaseExtraContext(_ExtraContext): + """Extra context generated during the :py:data:`~Phase.Parsing` phase. + The ``generate`` method receives the current directive or role being executed. + """ + + phase = Phase.Parsing @abstractmethod - def generate(self, host: ParseHost) -> Any: ... + def generate(self, directive: SphinxDirective | SphinxRole) -> Any: ... + +class ParsedPhaseExtraContext(_ExtraContext): + """Extra context generated during the :py:data:`~Phase.Parsed` phase. + The ``generate`` method receives the current Sphinx transform. + """ -class ResolvePhaseExtraContext(ABC): - """Extra context generated during the :py:class:`~Phase.Resolving` phase.""" + phase = Phase.Parsed @abstractmethod - def generate(self, host: ResolveHost) -> Any: ... + def generate(self, transform: SphinxTransform) -> Any: ... + + +class ResolvingPhaseExtraContext(_ExtraContext): + """Extra context generated during the :py:data:`~Phase.Resolving` phase. + The ``generate`` method receives the current Sphinx transform. + """ + + phase = Phase.Resolving + @abstractmethod + def generate(self, transform: SphinxTransform) -> Any: ... + + +class GlobalExtraContext(_ExtraContext): + """Extra context available in all phases. + The ``generate`` method receives the Sphinx build environment. + """ + + phase = None + + @abstractmethod + def generate(self, env: BuildEnvironment) -> Any: ... -# ======================= + +# ========================== # Extra context registration -# ======================= +# ========================== -class ExtraContextRegistry: - names: set[str] - parsing: dict[str, ParsePhaseExtraContext] - parsed: dict[str, ResolvePhaseExtraContext] - post_transform: dict[str, ResolvePhaseExtraContext] - global_: dict[str, GlobalExtraContxt] +class _ExtraContextRegistry: + ctxs: dict[str, _ExtraContext] def __init__(self) -> None: - self.names = set() - self.parsing = {} - self.parsed = {} - self.post_transform = {} - self.global_ = {} - - self.add_global_context('sphinx', _SphinxExtraContext()) - self.add_global_context('docutils', _DocutilsExtraContext()) - self.add_parsing_phase_context('markup', _MarkupExtraContext()) - self.add_parsing_phase_context('section', _SectionExtraContext()) - self.add_parsing_phase_context('doc', _DocExtraContext()) - - def _name_dedup(self, name: str) -> None: - # TODO: allow dup - if name in self.names: - raise ValueError(f'Context generator {name} already exists') - self.names.add(name) - - def add_parsing_phase_context( - self, name: str, ctxgen: ParsePhaseExtraContext - ) -> None: - """Register an extra context for the :py:class:`~Phase.Parsing` phase.""" - self._name_dedup(name) - self.parsing['_' + name] = ctxgen - - def add_parsed_phase_context( - self, name: str, ctxgen: ResolvePhaseExtraContext - ) -> None: - """Register an extra context for the :py:class:`~Phase.Parsed` phase.""" - self._name_dedup(name) - self.parsed['_' + name] = ctxgen - - def add_post_transform_phase_context( - self, name: str, ctxgen: ResolvePhaseExtraContext - ) -> None: - """Register an extra context for the :py:class:`~Phase.Resolving` phase.""" - self._name_dedup(name) - self.post_transform['_' + name] = ctxgen - - def add_global_context(self, name: str, ctxgen: GlobalExtraContxt): - """Register a global extra context available across phases.""" - self._name_dedup(name) - self.global_['_' + name] = ctxgen - - -# =================================== -# Builtin extra context implementations -# =================================== + self.ctxs = {} + def register(self, name: str, ctx: _ExtraContext) -> None: + if name in self.ctxs: + raise ValueError(f'Extra context "{name}" already registered') + self.ctxs[name] = ctx -class _MarkupExtraContext(ParsePhaseExtraContext): - @override - def generate(self, host: ParseHost) -> Any: - isdir = isinstance(host, SphinxDirective) + def get(self, name: str) -> _ExtraContext | None: + if name not in self.ctxs: + return None + return self.ctxs[name] + + def get_names(self) -> set[str]: + return set(self.ctxs.keys()) + + def get_names_at_phase(self, phase: Phase | None) -> set[str]: + return {name for name, ctx in self.ctxs.items() if ctx.phase == phase} + + def get_names_before_phase(self, phase: Phase | None) -> set[str]: return { - 'type': 'directive' if isdir else 'role', - 'name': host.name, - 'lineno': host.lineno, - 'rawtext': host.block_text if isdir else host.rawtext, + name + for name, ctx in self.ctxs.items() + if phase is None or ctx.phase is None or phase >= ctx.phase } -class _DocExtraContext(ParsePhaseExtraContext): - @override - def generate(self, host: ParseHost) -> Any: - return proxy(HostWrapper(host).doctree) +# Global registry instance. +_REGISTRY = _ExtraContextRegistry() -class _SectionExtraContext(ParsePhaseExtraContext): - @override - def generate(self, host: ParseHost) -> Any: - parent = HostWrapper(host).parent - return proxy(find_current_section(parent)) +def extra_context(name: str): + """Decorator to register an extra context. + The phase is determined by which ExtraContext class is used: -class _SphinxExtraContext(GlobalExtraContxt): - app: ClassVar[Sphinx] + :py:class:`GlobalExtraContext` + available in all phases + :py:class:`ParsingPhaseExtraContext` + available during Parsing phase + :py:class:`ParsedPhaseExtraContext` + available during Parsed phase + :py:class:`ResolvingPhaseExtraContext` + available during Resolving phase - @override - def generate(self) -> Any: - return proxy(self.app) + Example:: + @extra_context('doc') + class DocExtraContext(ParsingPhaseExtraContext): + def generate(self, ctx): + return proxy(HostWrapper(ctx).doctree) -class _DocutilsExtraContext(GlobalExtraContxt): - @override - def generate(self) -> Any: - # FIXME: use unexported api - return { - 'directives': _directives, - 'roles': _roles, - } + :param name: The context name, used in templates via ``load_extra('name')``. + """ + + def decorator(cls): + _REGISTRY.register(name, cls()) + return cls + + return decorator # ======================== -# Extra Context Management +# Extra Context Generation # ======================== class ExtraContextGenerator: node: pending_node + todo: set[str] report: Report - registry: ClassVar[ExtraContextRegistry] = ExtraContextRegistry() + env: ClassVar[BuildEnvironment] def __init__(self, node: pending_node) -> None: self.node = node @@ -165,30 +164,104 @@ def __init__(self, node: pending_node) -> None: ) Reporter(node).append(self.report) - def on_anytime(self) -> None: - for name, ctxgen in self.registry.global_.items(): - self._safegen(name, lambda: ctxgen.generate()) + # Initialize todo with requested extra contexts, validate they exist + total = _REGISTRY.get_names() + avail = _REGISTRY.get_names_before_phase(node.template.phase) + requested = set(node.template.extra) + self.todo = requested & avail + + # Report errors for non-existent contexts + if nonexist := requested - total: + self.report.text(f'Extra contexts {nonexist} are non-exist.') + if nonavail := requested & total - avail: + self.report.text( + f'Extra contexts {nonavail} are not available ' + f'at pahse {node.template.phase}.' + ) + + def on_anytime(self, env: BuildEnvironment) -> None: + self._generate(GlobalExtraContext, lambda ctx: ctx.generate(env)) + + def on_parsing(self, directive: SphinxDirective | SphinxRole) -> None: + self._generate(ParsingPhaseExtraContext, lambda ctx: ctx.generate(directive)) + + def on_parsed(self, transform: SphinxTransform) -> None: + self._generate(ParsedPhaseExtraContext, lambda ctx: ctx.generate(transform)) + + def on_resolving(self, transform: SphinxTransform) -> None: + self._generate(ResolvingPhaseExtraContext, lambda ctx: ctx.generate(transform)) + + def _generate(self, cls: type[_ExtraContext], gen: Callable[..., Any]) -> None: + # Get all context names available for this phase + avail = _REGISTRY.get_names_at_phase(cls.phase) + # Find which ones are requested and not yet generated + todo = avail & self.todo + + for name in todo: + ctx = _REGISTRY.get(name) + if ctx is None: + continue + try: + self.node.extra[name] = gen(ctx) + self.todo.discard(name) + except Exception: + self.report.text(f'Failed to generate extra context "{name}":') + self.report.traceback() + + +# ===================================== +# Builtin extra context implementations +# ===================================== + + +@extra_context('markup') +class MarkupExtraContext(ParsingPhaseExtraContext): + @override + def generate(self, directive: SphinxDirective | SphinxRole) -> Any: + isdir = isinstance(directive, SphinxDirective) + return { + 'type': 'directive' if isdir else 'role', + 'name': directive.name, + 'lineno': directive.lineno, + 'rawtext': directive.block_text if isdir else directive.rawtext, + } + - def on_parsing(self, host: ParseHost) -> None: - for name, ctxgen in self.registry.parsing.items(): - self._safegen(name, lambda: ctxgen.generate(host)) +@extra_context('doc') +class DocExtraContext(ParsingPhaseExtraContext): + @override + def generate(self, directive: SphinxDirective | SphinxRole) -> Any: + from .utils.ctxproxy import proxy + + return proxy(HostWrapper(directive).doctree) + + +@extra_context('section') +class SectionExtraContext(ParsingPhaseExtraContext): + @override + def generate(self, directive: SphinxDirective | SphinxRole) -> Any: + from .utils.ctxproxy import proxy + + parent = HostWrapper(directive).parent + return proxy(find_current_section(parent)) + + +@extra_context('sphinx') +class SphinxAppExtraContext(GlobalExtraContext): + @override + def generate(self, env: BuildEnvironment) -> Any: + from .utils.ctxproxy import proxy + + return proxy(env.app) - def on_parsed(self, host: ResolveHost) -> None: - for name, ctxgen in self.registry.parsed.items(): - self._safegen(name, lambda: ctxgen.generate(host)) - def on_resolving(self, host: ResolveHost) -> None: - for name, ctxgen in self.registry.post_transform.items(): - self._safegen(name, lambda: ctxgen.generate(host)) +@extra_context('env') +class SphinxBuildEnvExtraContext(GlobalExtraContext): + @override + def generate(self, env: BuildEnvironment) -> Any: + from .utils.ctxproxy import proxy - def _safegen(self, name: str, gen: Callable[[], Any]): - try: - # ctxgen.generate can be user-defined code, exception of any kind are possible. - self.node.extra[name] = gen() - except Exception: - self.report.text(f'Failed to generate extra context "{name}":') - self.report.traceback() + return proxy(env) -def setup(app: Sphinx): - _SphinxExtraContext.app = app +def setup(app: Sphinx): ... diff --git a/src/sphinxnotes/render/pipeline.py b/src/sphinxnotes/render/pipeline.py index cc0779f..db5d131 100644 --- a/src/sphinxnotes/render/pipeline.py +++ b/src/sphinxnotes/render/pipeline.py @@ -121,10 +121,12 @@ def render_queue(self) -> list[pending_node]: ns.append(pending) continue + host = cast(Host, self) + # Generate global extra context for later use. - ExtraContextGenerator(pending).on_anytime() + ExtraContextGenerator(pending).on_anytime(host.env) - host = cast(Host, self) + # Perform render. pending.render(host) if pending.parent is None: diff --git a/src/sphinxnotes/render/render.py b/src/sphinxnotes/render/render.py index 8af664e..57d696c 100644 --- a/src/sphinxnotes/render/render.py +++ b/src/sphinxnotes/render/render.py @@ -1,5 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from docutils import nodes @@ -25,6 +25,10 @@ class Phase(Enum): def default(cls) -> Phase: return cls.Parsing + def __ge__(self, other: Phase) -> bool: + _ORDER = {Phase.Parsing: 1, Phase.Parsed: 2, Phase.Resolving: 3} + return _ORDER[self] >= _ORDER[other] + @dataclass class Template: @@ -34,6 +38,8 @@ class Template: phase: Phase = Phase.default() #: Enable debug output (shown as :py:class:`docutils.nodes.system_message` in document.) debug: bool = False + #: Names of extra context to be generated and available in the template. + extra: list[str] = field(default_factory=list) #: Possible render host of :meth:`pending_node.render`. diff --git a/src/sphinxnotes/render/template.py b/src/sphinxnotes/render/template.py index 8a0f94b..0b34169 100644 --- a/src/sphinxnotes/render/template.py +++ b/src/sphinxnotes/render/template.py @@ -41,7 +41,7 @@ def render( debug.text('Data:') debug.code(pformat(data), lang='python') - debug.text('Extra context (just key):') + debug.text('Available extra context (just keys):') debug.code(pformat(list(extra.keys())), lang='python') # Convert data to context dict. @@ -50,13 +50,16 @@ def render( elif isinstance(data, dict): ctx = data.copy() - # Merge extra context and main context. - conflicts = set() - for name, e in extra.items(): - if name not in ctx: - ctx[name] = e - else: - conflicts.add(name) + # Inject load_extra() function for accessing extra context. + def load_extra(name: str): + if name not in extra: + raise ValueError( + f'Extra context "{name}" is not available. ' + f'Available: {list(extra.keys())}' + ) + return extra[name] + + ctx['load_extra'] = load_extra text = self._render(ctx, debug=debug is not None) diff --git a/src/sphinxnotes/render/utils/freestyle.py b/src/sphinxnotes/render/utils/freestyle.py index 2ccd2f0..a71b190 100644 --- a/src/sphinxnotes/render/utils/freestyle.py +++ b/src/sphinxnotes/render/utils/freestyle.py @@ -32,7 +32,7 @@ def __contains__(self, _): class FreeStyleDirective(SphinxDirective): """ - TODO: docutils/parsers/rst/directives/misc.py::Meta + Standard impl of "FreeStyle" directive: docutils/parsers/rst/directives/misc.py::Meta """ final_argument_whitespace = True diff --git a/tests/roots/test-extra-context/conf.py b/tests/roots/test-extra-context/conf.py new file mode 100644 index 0000000..82b6e94 --- /dev/null +++ b/tests/roots/test-extra-context/conf.py @@ -0,0 +1,41 @@ +from sphinx.application import Sphinx +from sphinxnotes.render import ( + extra_context, + ParsingPhaseExtraContext, + GlobalExtraContext, + BaseContextDirective, + Template, +) + + +@extra_context('custom_parsing') +class CustomParsingExtraContext(ParsingPhaseExtraContext): + def generate(self, directive): + return {'custom_value': 'parsing_test'} + + +@extra_context('custom_global') +class CustomGlobalExtraContext(GlobalExtraContext): + def generate(self, env): + return {'custom_value': 'global_test'} + + +class CustomExtraContextDirective(BaseContextDirective): + def current_context(self): + return {} + + def current_template(self): + return Template( + """ +{% set _parsing = load_extra('custom_parsing') %} +{% set _global = load_extra('custom_global') %} +Parsing: {{ _parsing.custom_value }} +Global: {{ _global.custom_value }} +""", + extra=['custom_parsing', 'custom_global'], + ) + + +def setup(app: Sphinx): + app.setup_extension('sphinxnotes.render') + app.add_directive('custom-extra', CustomExtraContextDirective) diff --git a/tests/roots/test-extra-context/index.rst b/tests/roots/test-extra-context/index.rst new file mode 100644 index 0000000..e4a4241 --- /dev/null +++ b/tests/roots/test-extra-context/index.rst @@ -0,0 +1,4 @@ +Extra Context Test +================== + +.. custom-extra:: diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 5af277b..9fa23b2 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -26,3 +26,13 @@ def test_strict_data_define_directive_card(app, status, warning): # -- literalinclude:end:end-to-end-card -- + + +@pytest.mark.sphinx('html', testroot='extra-context') +def test_extra_context_custom_loader(app, status, warning): + app.build() + + html = (app.outdir / 'index.html').read_text(encoding='utf-8') + + assert 'Parsing: parsing_test' in html + assert 'Global: global_test' in html