diff --git a/docs/_images/rst.png b/docs/_images/rst.png new file mode 100644 index 0000000..58a5e7e Binary files /dev/null and b/docs/_images/rst.png differ diff --git a/docs/conf.py b/docs/conf.py index 8d918e1..4a14953 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -112,4 +112,9 @@ # DOG FOOD CONFIGURATION START +comboroles_roles = { + 'literal_emphasis_strong': ['literal', 'emphasis', 'strong'], + 'parsed_literal': (['literal'], True), +} + # DOG FOOD CONFIGURATION END diff --git a/docs/index.rst b/docs/index.rst index 4533004..63ff889 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 @@ -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 @@ -47,6 +92,7 @@ Contents .. toctree:: :caption: Contents + usage changelog The Sphinx Notes Project diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..ed6fb96 --- /dev/null +++ b/docs/usage.rst @@ -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 +======================= diff --git a/src/sphinxnotes/comboroles/__init__.py b/src/sphinxnotes/comboroles/__init__.py new file mode 100644 index 0000000..ccfb2ee --- /dev/null +++ b/src/sphinxnotes/comboroles/__init__.py @@ -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, + }