drammock
drammock

Reputation: 2543

how to make a custom sphinx role that is also an external link?

I'm trying to extend Sphinx to have a new inline role called newcontrib. What it should do is take its content and (1) add some extra text to it, (2) make the text bold in the rendered output, and (3) make the text a link, whose target is the original content of the role. Example:

:newcontrib:`Jane Doe`

Should end up being parsed as if it were

`**new contributor Jane Doe** <Jane Doe_>`_

The target is defined in another file (names.inc) and looks like:

.. _Jane Doe: https://jane-does-website.com

My attempt so far (based loosely on this example) is this:

from docutils.nodes import reference, strong, target
from sphinx.addnodes import pending_xref


def newcontrib_role(name, rawtext, text, lineno, inliner, options={},
                    content=[]):
    """Create a role to highlight new contributors in changelog entries."""
    newcontrib = f'new contributor {text}'
    targ = f' <{text}_>'
    rawtext = f'`{newcontrib}{targ}`_'
    options.update(name=newcontrib, refname=text.lower())
    strong_node = strong(rawtext, newcontrib)
    node = reference('', '', strong_node, **options)
    return [node], []


def setup(app):
    app.add_role('newcontrib', newcontrib_role)
    return

I've also tried using pending_xref() instead of reference(), and tried explicitly making a target node and adding it as a sibling. Results vary depending on exactly what I try; sometimes I get an error during build (missing attribute refname, refdomain, reftype...), other times I get a successful build but the link goes to url-of-current-page#jane-doe. I also briefly tried subclassing XRefRole instead of defining the role function... open to any approach.

Upvotes: 2

Views: 440

Answers (1)

drammock
drammock

Reputation: 2543

What ended up working was this:

def newcontrib_role(name, rawtext, text, lineno, inliner, options={},
                    content=[]):
    """Create a role to highlight new contributors in changelog entries."""
    newcontrib = f'new contributor {text}'
    alias_text = f' <{text}_>'
    rawtext = f'`{newcontrib}{alias_text}`_'
    refname = text.lower()
    strong_node = strong(rawtext, newcontrib)
    target_node = target(alias_text, refname=refname, names=[newcontrib])
    target_node.indirect_reference_name = text
    options.update(name=newcontrib, refname=refname)
    ref_node = reference('', '', strong_node, **options)
    ref_node[0].rawsource = rawtext
    inliner.document.note_indirect_target(target_node)
    inliner.document.note_refname(ref_node)
    return [ref_node, target_node], []

(plus the setup(app) function as in the question)

in particular, the key steps I hadn't tried before were inliner.document.note_indirect_target and inliner.document.note_refname. I got the idea from looking at how docutils parses links.

Upvotes: 2

Related Questions