Skip to content

Commit

Permalink
Merge pull request #3 from sphinx-notes/feat/support-std-xref
Browse files Browse the repository at this point in the history
Support `:ref:` and other roles from std domain
  • Loading branch information
SilverRainZ authored Feb 16, 2024
2 parents bdeeee2 + e89faf6 commit e6c5331
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 102 deletions.
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@
'parsed_emphasis': (['emphasis'], True),
'literal_issue': ['literal', 'issue'],
'literal_strike': ['literal', 'strike'],
'literal_ref': ['literal', 'ref'],
'literal_doc': ['literal', 'doc'],
}

extensions.append('sphinxnotes.strike')
Expand Down
89 changes: 89 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
========
Examples
========

Due to :ref:`limitation`, the extension can not work with all roles, so we list
all use cases we have tested.

.. _example-nested-parse:

Nested Parse
============

.. seealso:: :ref:`nested-parse`.

========================================== =====================================
``:parsed_literal:`https://example.com```` :parsed_literal:`https://example.com`
``:parsed_literal:`|release|```` :parsed_literal:`|release|`
``:parsed_literal:`RFC: :rfc:\`1459\```` :parsed_literal:`RFC: :rfc:\`1459\``
========================================== =====================================

.. note:: For nested roles, note that the backquote ````` needs to be escaped by ``\\``.

Cross references: ``:ref:``, ``:doc:`` and more…
=================================================

.. code:: python
comboroles_roles = {
'literal_ref': ['literal', 'ref'],
'literal_doc': ['literal', 'doc'],
}
================================== ==============================
``:ref:`composite-roles``` :ref:`composite-roles`
``:literal_ref:`composite-roles``` :literal_ref:`composite-roles`
``:doc:`changelog``` :doc:`changelog`
``:literal_doc:`changelog``` :literal_doc:`changelog`
================================== ==============================

Works with other Extensions
===========================

``sphinx.ext.extlink``
----------------------

:parsed_literal:`sphinx.ext.extlink_` is a Sphinx builtin extension to create
shorten external links.

Assume that we have the following configuration, extlink creates the ``issue`` role,
then comboroles creates a ``literal_issue`` role based on it:

.. code:: python
extlinks = {
'issue': ('https://github.com/sphinx-notes/comboroles/issues/%s', '💬%s'),
}
comboroles_roles = {
'literal_issue': ['literal', 'issue'],
}
========================== ====================
``:issue:`new``` :issue:`new`
``:literal_issue:`new``` :literal_issue:`new`
========================== ====================

.. seealso:: https://github.com/sphinx-doc/sphinx/issues/11745

.. _sphinx.ext.extlinks: https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html

``sphinxnotes.strike``
----------------------

:parsed_literal:`sphinxnotes.strike_` is an extension that adds
:del:`strikethrough text` support to Sphinx.

.. code:: python
comboroles_roles = {
'literal_strike': ['literal', 'strike'],
}
=========================== ======================
``:strike:`text``` :strike:`text`
``:literal_strike:`text``` :literal_strike:`text`
=========================== ======================

.. _sphinxnotes-strike: https://sphinx.silverrainz.me/strike/

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Contents
:caption: Contents

usage
examples
conf
changelog

Expand Down
72 changes: 10 additions & 62 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,77 +79,25 @@ The above configuration creates a composite role `parsed_literal` with
``nested_parse`` enabled, so the text "\*\*bold code\**" can be parsed.

Further, hyperlinks, substitutions, and even roles inside interpreted text can
be parsed too:

========================================== =====================================
``:parsed_literal:`https://example.com```` :parsed_literal:`https://example.com`
``:parsed_literal:`|release|```` :parsed_literal:`|release|`
``:parsed_literal:`RFC: :rfc:\`1459\```` :parsed_literal:`RFC: :rfc:\`1459\``
========================================== =====================================

.. note:: For nested roles, note that the backquote ````` needs to be escaped.
be parsed too, see :ref:`example-nested-parse` for more details.

Works with other Extensions
===========================

The extensions can also work with roles provided by the some other extensions
(not all, see :ref:`limitation`).

``sphinx.ext.extlink``
----------------------

:parsed_literal:`sphinx.ext.extlink_` is a Sphinx builtin extension to create
shorten external links.

Assume that we have the following configuration, extlink creates the ``issue`` role,
then comboroles creates a ``literal_issue`` role based on it:

.. code:: python
extlinks = {
'issue': ('https://github.com/sphinx-notes/comboroles/issues/%s', '💬%s'),
}
comboroles_roles = {
'literal_issue': ['literal', 'issue'],
}
========================== ====================
``:issue:`new``` :issue:`new`
``:literal_issue:`new```` :literal_issue:`new`
========================== ====================

.. seealso:: https://github.com/sphinx-doc/sphinx/issues/11745

.. _sphinx.ext.extlinks: https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html

``sphinxnotes.strike``
----------------------

:parsed_literal:`sphinxnotes.strike_` is an extension that adds
:del:`strikethrough text` support to Sphinx.

.. code:: python
comboroles_roles = {
'literal_strike': ['literal', 'strike'],
}
=========================== ======================
``:strike:`text``` :strike:`text`
``:literal_strike:`text```` :literal_strike:`text`
=========================== ======================
.. For compatibility:
.. _sphinx.ext.extlink:
.. _sphinxnotes.strike:

.. _sphinxnotes-strike: https://sphinx.silverrainz.me/strike/
Moved to :doc:`examples`.

.. _limitation:

Limitation
==========

.. warning::
Due to internal implementation, the extension can only used to composite
simple roles and may CRASH Sphinx when compositing complex roles.
**DO NOT report to Sphinx first if it crashes**, please report to here :issue:`new`
instead.

Due to internal implementation, the extension can only used to composite
simple roles and may CRASH Sphinx when compositing complex roles.
DO NOT report to Sphinx first if it crashes, please report to here :issue:`new`
instead.
n
115 changes: 75 additions & 40 deletions src/sphinxnotes/comboroles/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from dataclasses import dataclass

from sphinx.util.docutils import SphinxRole

Expand All @@ -10,9 +11,18 @@
if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.util.typing import RoleFunction

__version__ = '0.1.0'


@dataclass
class RoleMetaInfo(object):
"""Metainfo used to assist role composition."""
name: str
fn: RoleFunction


class CompositeRole(SphinxRole):
#: Rolenames to be composited
rolenames: list[str]
Expand All @@ -26,54 +36,55 @@ def __init__(self, rolenames: list[str], nested_parse: bool):


def run(self) -> tuple[list[Node], list[system_message]]:
nodes: list[TextElement] = []
reporter = self.inliner.reporter # type: ignore[attr-defined]

# Lookup RoleFunction by name.
# NOTE: We can not do this during __init__, some roles created by
# NOTE: We should NOT lookup roles during __init__, some roles created by
# 3rd-party extension do not exist yet at that time.
components = []
for r in self.rolenames:
if r in roles._roles: # type: ignore[attr-defined]
components.append(roles._roles[r]) # type: ignore[attr-defined]
elif r in roles._role_registry: # type: ignore[attr-defined]
components.append(roles._role_registry[r]) # type: ignore[attr-defined]
else:
msg = reporter.error(f'no such role: {r}', line=self.lineno)
roles = []
for name in self.rolenames:
role = self.lookup_role(name)
if role is None:
msg = reporter.error(f'no such role: {name}', line=self.lineno)
return [], [msg]
roles.append(role)

# Run all RoleFunction, collect the produced nodes.
for comp in components:
ns, msgs = comp(self.name, self.rawtext, self.text, self.lineno, self.inliner, self.options, self.content)
# Run all RoleFunction, collect the produced nodes:
#
# - the innermost element `contnode` can be an Inline or TextElement,
# - allother elements `wrapnodes` MUST be TextElement.
wrapnodes: list[TextElement] = []
contnode: TextElement|Inline|None = None
for i, role in enumerate(roles):

# The returned nodes should be exactly one TextElement and contains
# exactly one Text node as child, like this::
#
# <TextElement>
# <Text>
#
# So that it can be used as part of composite roles.
ns, msgs = role.fn(role.name, self.rawtext, self.text, self.lineno, self.inliner, self.options, self.content)
if len(msgs) != 0:
return [], msgs # once system_message is thrown, return
if len(ns) != 1:
msg = reporter.error(f'role should returns exactly 1 node, but {len(ns)} found: {ns}', line=self.lineno)
return [], [msg]
if not isinstance(ns[0], (Inline, TextElement)):
msg = reporter.error(f'node {ns[0]} is not ({Inline}, {TextElement})', line=self.lineno)
return [], [msg]
n = cast(TextElement, ns[0])
if len(n) != 1:
msg = reporter.error(f'node {n} should has exactly 1 child, but {len(n)} found', line=self.lineno)
return [], [msg]
if not isinstance(n[0], Text):
msg = reporter.error(f'child of node {n} should have Text, but {type(n[0])} found', line=self.lineno)
n = ns[0]

# So that it can be used as part of composite roles.
innermost = i == len(roles) - 1
classes = (Inline, TextElement) if innermost else TextElement
if not isinstance(n, classes):
msg = reporter.error(f'node {n} is not {classes}', line=self.lineno)
return [], [msg]
nodes.append(n)

if len(nodes) == 0:
if innermost:
contnode = n
else:
wrapnodes.append(cast(TextElement, n))

if contnode is None:
return [], [] # no node produced, return

if self.nested_parse:
if not isinstance(contnode, TextElement):
msg = reporter.error(f'can not do nested parse because node {contnode} is not {TextElement}', line=self.lineno)
return [], [msg]
contnode = cast(TextElement, contnode)

# See also:
#
# - :ref:`nested-parse`
Expand All @@ -83,29 +94,53 @@ def run(self) -> tuple[list[Node], list[system_message]]:
document=inliner.document, # type: ignore[attr-defined]
reporter=inliner.reporter, # type: ignore[attr-defined]
language=inliner.language) # type: ignore[attr-defined]
n, msgs = inliner.parse(self.text, self.lineno, memo, nodes[-1]) # type: ignore[attr-defined]
n, msgs = inliner.parse(self.text, self.lineno, memo, wrapnodes) # type: ignore[attr-defined]
if len(msgs) != 0:
return [], msgs
nodes[-1].replace(nodes[-1][0], n) # replace the Text node
contnode.replace(contnode[0], n) # replace the Text node



# Composite all nodes together, for examle:
#
# before::
#
# <strong>
# <strong> # wrapnodes[0]
# <text>
# <literal>
# <literal> # wrapnodes[1]
# <text>
# <pending_xref> # contnode
# <text>
#
# after::
#
# <strong>
# <literal>
# <text>
for i in range(0, len(nodes) -1):
nodes[i].replace(nodes[i][0], nodes[i+1]) # replace the Text node with the inner(i+1) TextElement
# <pending_xref>
# <text>
allnodes = wrapnodes + [contnode] # must not empty
for i in range(0, len(allnodes)-1):
# replace the Text node with the inner(i+1) TextElement
allnodes[i].replace(allnodes[i][0], allnodes[i+1])

return [allnodes[0]], []


def lookup_role(self, name:str) -> RoleMetaInfo|None:
"""Lookup RoleFunction by name."""

# Lookup in docutils' regsitry.
if name in roles._roles: # type: ignore[attr-defined]
return RoleMetaInfo(name=name, fn=roles._roles[name]) # type: ignore[attr-defined]
if name in roles._role_registry: # type: ignore[attr-defined]
return RoleMetaInfo(name=name, fn=roles._role_registry[name]) # type: ignore[attr-defined]

# Lookup in Sphinx's std domain.
std = self.env.get_domain('std')
if name in std.roles:
return RoleMetaInfo(name='std:'+name, fn=std.roles[name]) # type: ignore[attr-defined]

return [nodes[0]], []
return None


def _config_inited(app:Sphinx, config:Config) -> None:
Expand Down

0 comments on commit e6c5331

Please sign in to comment.