Skip to content

Commit

Permalink
Impl roles composing
Browse files Browse the repository at this point in the history
  • Loading branch information
SilverRainZ committed Dec 21, 2023
1 parent 4bde76c commit 4d322f1
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 0 deletions.
Binary file added docs/_images/rst.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,9 @@

# DOG FOOD CONFIGURATION START

comboroles_roles = {
'literal_emphasis_strong': ['literal', 'emphasis', 'strong'],
'parsed_literal': (['literal'], True),
}

# DOG FOOD CONFIGURATION END
46 changes: 46 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@ Introduction

.. ADDITIONAL CONTENT START
The extension allows users to create roles composited by multiple roles.

As we know, |rst| does not yet support `nested inline markups/roles`__,
so text like ````***bold italic code***```` doesn't render as expected.
With the extension, we can compose roles ``literal`` (code), ``emphasis``
(italic), and ``strong`` (bold) to composite roles ``literal_emphasis_strong``,
to achieve the same effect as nested inline roles:

.. list-table::

* - ````***bold italic code***````
- ``***bold italic code***``
- ❌
* - ``:literal_emphasis_strong:`bold italic code```
- :literal_emphasis_strong:`bold italic code`
- ✔️

.. warning::

Due to :ref:`internal-impl`, the extension can only composite simple roles
(such as `docutils' Standard Roles`__),
and may crash Sphinx when compositing complex roles,
so DO NOT report to Sphinx first if it crashes, report to here
:issue:`new` instead.

.. |rst| image:: /_images/rst.png
:target: https://docutils.sourceforge.io/rst.html
:alt: reStructuredText
:height: 1em
:align: bottom

__ https://docutils.sourceforge.io/FAQ.html#is-nested-inline-markup-possible
__ https://docutils.sourceforge.io/docs/ref/rst/roles.html#standard-roles

.. ADDITIONAL CONTENT END
Getting Started
Expand Down Expand Up @@ -39,6 +73,17 @@ Then, add the extension name to ``extensions`` configuration item in your conf.p

.. ADDITIONAL CONTENT START
TODO: cfg

.. list-table::

* - ``:literal_emphasis_strong:`Sphinx```
- :literal_emphasis_strong:`Sphinx`
* - ``:parsed_literal:`https://silverrainz.me```
- :parsed_literal:`https://silverrainz.me`

See :doc:`usage` for more details.

.. ADDITIONAL CONTENT END
Contents
Expand All @@ -47,6 +92,7 @@ Contents
.. toctree::
:caption: Contents

usage
changelog

The Sphinx Notes Project
Expand Down
25 changes: 25 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
=====
Usage
=====

TODO:

https://docutils.sourceforge.io/docs/ref/rst/roles.html

======================= ===============
Usage Role
======================= ===============
*emphasis* ``emphasis``
**strong emphasis** ``strong``
``inline literals`` ``literal``
:sub:`sub` script ``sub``
:sup:`super` script ``sup``
======================= ===============

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

.. _internal-impl:

Internal Implementation
=======================
104 changes: 104 additions & 0 deletions src/sphinxnotes/comboroles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast

from sphinx.util.docutils import SphinxRole

# Memo:
#
# https://docutils.sourceforge.io/FAQ.html#is-nested-inline-markup-possible
# https://stackoverflow.com/questions/44829580/composing-roles-in-restructuredtext

from docutils.nodes import Node, Inline, TextElement, Text, system_message
from docutils.parsers.rst import roles
from docutils.parsers.rst import states

if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.util.typing import RoleFunction

__version__ = '0.1.0'

class CompositeRole(SphinxRole):
#: Roles to be composited
components: list[RoleFunction]
nested_parse: bool

def __init__(self, rolenames: list[str], nested_parse: bool):
self.components = []
for r in rolenames:
if r in roles._roles: # type: ignore[attr-defined]
self.components.append(roles._roles[r]) # type: ignore[attr-defined]
elif r in roles._role_registry: # type: ignore[attr-defined]
self.components.append(roles._role_registry[r]) # type: ignore[attr-defined]
else:
raise KeyError(f'no such role: {r}')
self.nested_parse = nested_parse


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

# Run all RoleFunction, collect the produced nodes.
for comp in reversed(self.components):
ns, sysmsgs = comp(self.name, self.rawtext, self.text, self.lineno, self.inliner, self.options, self.content)
if len(sysmsgs) != 0:
return [], sysmsgs # once system_message is thrown, return
if len(ns) != 1:
msg = reporter.error(f'role should returns exactly 1 nodes, 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.children) != 1:
msg = reporter.error(f'node {n} should has exactly 1 child, but {len(n.children)} 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)
return [], [msg]
nodes.append(n)


# ref: https://stackoverflow.com/questions/44829580/composing-roles-in-restructuredtext
if self.nested_parse:
memo = states.Struct(
document=self.inliner.document, # type: ignore[attr-defined]
reporter=reporter,
language=self.inliner.language) # type: ignore[attr-defined]

n, sysmsgs = self.inliner.parse(self.text, self.lineno, memo, nodes[-1]) # type: ignore[attr-defined]
if len(sysmsgs) != 0:
return [], sysmsgs
nodes[-1].replace(nodes[-1][0], n)

# Composite all nodes together.
for i in range(0, len(nodes) -1):
nodes[i].replace(nodes[i][0], nodes[i+1])

return [nodes[0]], []

def _config_inited(app:Sphinx, config:Config) -> None:
for name, cfg in config.comboroles_roles.items():
if isinstance(cfg, list):
rolenames = cfg
nested_parse = False
else:
rolenames = cfg[0]
nested_parse = cfg[1]
app.add_role(name, CompositeRole(rolenames, nested_parse))


def setup(app:Sphinx):
"""Sphinx extension entrypoint."""

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

app.add_config_value('comboroles_roles', {}, 'env', types=dict[str, list[str] | tuple[list[str],bool]])

return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}

0 comments on commit 4d322f1

Please sign in to comment.