import os
import logging
from docutils import nodes
from docutils.parsers.rst import directives
import sphinx
from sphinx.locale import _
try:
from sphinx.errors import NoUri
except ImportError:
from sphinx.environment import NoUri
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from docutils.statemachine import StringList
from docutils.frontend import Values
from sphinx.util.nodes import set_source_info, process_index_entry
from sphinx import addnodes
from ..language import TITLES
from ..ext_helper import info_blocref
[docs]
class blocref_node(nodes.admonition):
"""
Defines ``blocref`` node.
"""
pass
class blocreflist(nodes.General, nodes.Element):
"""
Defines ``blocreflist`` node.
"""
pass
[docs]
class BlocRef(BaseAdmonition):
"""
A ``blocref`` entry, displayed in the form of an admonition.
It takes the following options:
* *title*: a title for the bloc
* *tag*: a tag to have several categories of blocs
* *lid* or *label*: a label to refer to
* *index*: to add an entry to the index (comma separated)
Example::
.. blocref::
:title: example of a blocref
:tag: example
:lid: id-you-can-choose
An example of code::
print("mignon")
Which renders as:
.. blocref::
:title: example of a blocref
:tag: dummy_example
:lid: id-you-can-choose
An example of code::
print("mignon")
All blocs can be displayed in another page by using ``blocreflist``::
.. blocreflist::
:tag: dummy_example
:sort: title
Only examples tagged as ``dummy_example`` will be inserted here.
The option ``sort`` sorts items by *title*, *number*, *file*.
You also link to it by typing ``:ref:'anchor <id-you-can-choose>'`` which gives
something like :ref:`link_to_blocref <id-you-can-choose>`.
The link must receive a name.
.. blocreflist::
:tag: dummy_example
:sort: title
This directive is used to highlight a bloc about
anything :class:`sphinx_runpython.blocdefs.sphinx_blocref_extension.BlocRef`,
a question :class:`sphinx_runpython.blocdefs.sphinx_faqref_extension.FaqRef`,
an example :class:`sphinx_runpython.blocdefs.sphinx_exref_extension.ExRef`.
It supports option *index*
in most of the extensions so that the documentation
can refer to it.
"""
node_class = blocref_node
name_sphinx = "blocref"
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
"class": directives.class_option,
"title": directives.unchanged,
"tag": directives.unchanged,
"lid": directives.unchanged,
"label": directives.unchanged,
"index": directives.unchanged,
}
def _update_title(self, title, tag, lid):
"""
Updates the title for the bloc itself.
"""
return title
[docs]
def run(self):
"""
Builds a node @see cl blocref_node.
"""
return self.private_run()
[docs]
def private_run(self, add_container=False):
"""
Builds a node @see cl blocref_node.
:param add_container: add a container node and return as a second result
:return: list of nodes or list of nodes, container
"""
name_desc = self.__class__.name_sphinx
lineno = self.lineno
settings = self.state.document.settings
env = settings.env if hasattr(settings, "env") else None
docname = None if env is None else env.docname
if docname is not None:
docname = docname.replace("\\", "/").split("/")[-1]
legend = f"{docname}:{lineno}"
else:
legend = ""
if not self.options.get("class"):
self.options["class"] = [f"admonition-{name_desc}"]
# body
(blocref,) = super(BlocRef, self).run()
if isinstance(blocref, nodes.system_message):
return [blocref]
# add a label
lid = self.options.get("lid", self.options.get("label", None))
if lid:
container = nodes.container()
tnl = [f".. _{lid}:", ""]
content = StringList(tnl)
self.state.nested_parse(content, self.content_offset, container)
else:
container = None
# mid
breftag = self.options.get("tag", "").strip()
if len(breftag) == 0:
raise ValueError("tag is empty") # pragma: no cover
if env is not None:
mid = int(env.new_serialno(f"index{name_desc}-{breftag}")) + 1
else:
mid = -1
# title
titleo = self.options.get("title", "").strip()
if len(titleo) == 0:
raise ValueError("title is empty") # pragma: no cover
title = self._update_title(titleo, breftag, mid)
# main node
ttitle = title
title = nodes.title(text=_(title))
if container is not None:
blocref.insert(0, title)
blocref.insert(0, container)
else:
blocref.insert(0, title)
if add_container:
ret_container = nodes.container()
blocref += ret_container
blocref["breftag"] = breftag
blocref["brefmid"] = mid
blocref["breftitle"] = ttitle
blocref["breftitleo"] = titleo
blocref["brefline"] = lineno
blocref["breffile"] = docname
set_source_info(self, blocref)
if env is not None:
targetid = "index%s-%s%s" % (
name_desc,
breftag,
env.new_serialno("index%s%s" % (name_desc, breftag)),
)
blocref["breftargetid"] = targetid
ids = [targetid]
targetnode = nodes.target(legend, "", ids=ids)
set_source_info(self, targetnode)
try:
self.state.add_target(targetid, "", targetnode, lineno)
except Exception as e: # pragma: no cover
raise RuntimeError(
"Issue in \n File '{0}', line "
"{1}\ntitle={2}\ntag={3}\ntargetid={4}".format(
docname, lineno, title, breftag, targetid
)
) from e
# index node
index = self.options.get("index", None)
if index is not None:
indexnode = addnodes.index()
indexnode["entries"] = ne = []
indexnode["inline"] = False
set_source_info(self, indexnode)
for entry in index.split(","):
ne.extend(process_index_entry(entry, targetid))
else:
indexnode = None
else:
targetnode = None
indexnode = None
res = [a for a in [indexnode, targetnode, blocref] if a is not None]
if add_container:
return res, ret_container
return res
def process_blocrefs(app, doctree):
"""
Collects all blocrefs in the environment
this is not done in the directive itself because it some transformations
must have already been run, e.g. substitutions.
"""
process_blocrefs_generic(app, doctree, bloc_name="blocref", class_node=blocref_node)
def process_blocrefs_generic(app, doctree, bloc_name, class_node):
"""
Collects all blocrefs in the environment
this is not done in the directive itself because it some transformations
must have already been run, e.g. substitutions.
"""
env = app.builder.env
attr = f"{bloc_name}_all_{bloc_name}s"
if not hasattr(env, attr):
setattr(env, attr, [])
attr_list = getattr(env, attr)
for node in doctree.traverse(class_node):
try:
targetnode = node.parent[node.parent.index(node) - 1]
if not isinstance(targetnode, nodes.target):
raise IndexError # pragma: no cover
except IndexError: # pragma: no cover
targetnode = None
newnode = node.deepcopy()
breftag = newnode["breftag"]
breftitle = newnode["breftitle"]
brefmid = newnode["brefmid"]
brefline = newnode["brefline"]
breffile = newnode["breffile"]
del newnode["ids"]
del newnode["breftag"]
attr_list.append(
{
"docname": env.docname,
"source": node.source or env.doc2path(env.docname),
"lineno": node.line,
"blocref": newnode,
"target": targetnode,
"breftag": breftag,
"breftitle": breftitle,
"brefmid": brefmid,
"brefline": brefline,
"breffile": breffile,
}
)
class BlocRefList(Directive):
"""
A list of all blocref entries, for a specific tag.
* tag: a tag to filter bloc having this tag
* sort: a way to sort the blocs based on the title, file, number, default: *title*
* contents: add a bullet list with links to added blocs
Example::
.. blocreflist::
:tag: issue
:contents:
"""
name_sphinx = "blocreflist"
node_class = blocreflist
has_content = False
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
"tag": directives.unchanged,
"sort": directives.unchanged,
"contents": directives.unchanged,
}
def run(self):
"""
Simply insert an empty blocreflist node which will be replaced later
when process_blocref_nodes is called.
"""
name_desc = self.__class__.name_sphinx
settings = self.state.document.settings
env = settings.env if hasattr(settings, "env") else None
docname = None if env is None else env.docname
tag = self.options.get("tag", "").strip()
n = self.__class__.node_class("")
n["breftag"] = tag
n["brefsort"] = self.options.get("sort", "title").strip()
n["brefsection"] = self.options.get("section", True) in (
True,
"True",
"true",
1,
"1",
)
n["brefcontents"] = self.options.get("contents", False) in (
True,
"True",
"true",
1,
"1",
"",
None,
"None",
)
n["docname"] = docname
if env is not None:
targetid = "index%slist-%s" % (
name_desc,
env.new_serialno("index%slist" % name_desc),
)
targetnode = nodes.target("", "", ids=[targetid])
return [targetnode, n]
else:
return [n]
def process_blocref_nodes(app, doctree, fromdocname):
"""
process_blocref_nodes
"""
process_blocref_nodes_generic(
app,
doctree,
fromdocname,
class_name="blocref",
entry_name="brefmes",
class_node=blocref_node,
class_node_list=blocreflist,
)
def process_blocref_nodes_generic(
app, doctree, fromdocname, class_name, entry_name, class_node, class_node_list
):
"""
process_blocref_nodes and other kinds of nodes,
If the configuration file specifies a variable
``blocref_include_blocrefs`` equals to False,
all nodes are removed.
"""
# logging
cont = info_blocref(
app, doctree, fromdocname, class_name, entry_name, class_node, class_node_list
)
if not cont:
return
# check this is something to process
env = app.builder.env
attr_name = f"{class_name}_all_{class_name}s"
if not hasattr(env, attr_name):
setattr(env, attr_name, [])
bloc_list_env = getattr(env, attr_name)
if len(bloc_list_env) == 0:
return
# content
incconf = f"{class_name}_include_{class_name}s"
if app.config[incconf] and not app.config[incconf]:
for node in doctree.traverse(class_node):
node.parent.remove(node)
# Replace all blocreflist nodes with a list of the collected blocrefs.
# Augment each blocref with a backlink to the original location.
if hasattr(env, "settings"):
settings = env.settings
if hasattr(settings, "language_code"):
lang = env.settings.language_code
else:
lang = "en"
else:
settings = None
lang = "en"
orig_entry = TITLES[lang]["original entry"]
brefmes = TITLES[lang][entry_name]
for ilist, node in enumerate(doctree.traverse(class_node_list)):
if "ids" in node:
node["ids"] = []
if not app.config[incconf]:
node.replace_self([])
continue
nbbref = 0
content = []
breftag = node["breftag"]
brefsort = node["brefsort"]
add_contents = node["brefcontents"]
brefdocname = node["docname"]
if add_contents:
bullets = nodes.enumerated_list()
content.append(bullets)
# sorting
if brefsort == "title":
double_list = [
(info.get("breftitle", ""), info)
for info in bloc_list_env
if info["breftag"] == breftag
]
double_list.sort(key=lambda x: x[:1])
elif brefsort == "file":
double_list = [
((info.get("breffile", ""), info.get("brefline", "")), info)
for info in bloc_list_env
if info["breftag"] == breftag
]
double_list.sort(key=lambda x: x[:1])
elif brefsort == "number":
double_list = [
(info.get("brefmid", ""), info)
for info in bloc_list_env
if info["breftag"] == breftag
]
double_list.sort(key=lambda x: x[:1])
else:
raise ValueError("sort option should be file, number, title")
# printing
for n, blocref_info_ in enumerate(double_list):
blocref_info = blocref_info_[1]
nbbref += 1
para = nodes.paragraph(classes=[f"{class_name}-source"])
# Create a target?
try:
targ = blocref_info["target"]
except KeyError as e:
logger = logging.getLogger("blocref")
logger.warning(
"Unable to find key 'target' in %r (e=%r)", blocref_info, e
)
continue
try:
targ_refid = blocref_info["target"]["refid"]
except KeyError as e:
logger = logging.getLogger("blocref")
logger.warning("Unable to find key 'refid' in %r (e=%r)", targ, e)
continue
int_ids = [f"index{targ_refid}-{env.new_serialno(targ_refid)}"]
int_targetnode = nodes.target(blocref_info["breftitle"], "", ids=int_ids)
para += int_targetnode
# rest of the content
if app.config[f"{class_name}_link_only"]:
description = _(f"<<{orig_entry}>>")
else:
description = _(brefmes) % (
orig_entry,
os.path.split(blocref_info["source"])[-1],
blocref_info["lineno"],
)
desc1 = description[: description.find("<<")]
desc2 = description[description.find(">>") + 2 :]
para += nodes.Text(desc1, desc1)
# Create a reference
newnode = nodes.reference("", "", internal=True)
newnode["name"] = _(orig_entry)
try:
newnode["refuri"] = app.builder.get_relative_uri(
fromdocname, blocref_info["docname"]
)
if blocref_info["target"] is None:
raise NoUri # pragma: no cover
try:
newnode["refuri"] += "#" + blocref_info["target"]["refid"]
except Exception as e: # pragma: no cover
raise KeyError(
"refid in not present in '{0}'".format(blocref_info["target"])
) from e
except NoUri: # pragma: no cover
# ignore if no URI can be determined, e.g. for LaTeX output
pass
newnode.append(nodes.Text(newnode["name"]))
# para is duplicate of the content of the bloc
para += newnode
para += nodes.Text(desc2, desc2)
blocref_entry = blocref_info["blocref"]
idss = ["index-%s-%d-%d" % (class_name, ilist, n)]
# Inserts into the blocreflist
# in the list of links at the beginning of the page.
if add_contents:
title = blocref_info["breftitle"]
item = nodes.list_item()
p = nodes.paragraph()
item += p
newnode = nodes.reference("", title, internal=True)
try:
newnode["refuri"] = app.builder.get_relative_uri(
fromdocname, brefdocname
)
newnode["refuri"] += "#" + idss[0]
except NoUri: # pragma: no cover
# ignore if no URI can be determined, e.g. for LaTeX output
pass
p += newnode
bullets += item
# Adds the content.
blocref_entry["ids"] = idss
if not hasattr(blocref_entry, "settings"):
blocref_entry.settings = Values()
blocref_entry.settings.env = env
# If an exception happens here, see blog 2017-05-21 from the
# documentation.
env.resolve_references(blocref_entry, blocref_info["docname"], app.builder)
content.append(blocref_entry)
content.append(para)
node.replace_self(content)
def purge_blocrefs(app, env, docname):
"""
purge_blocrefs
"""
if not hasattr(env, "blocref_all_blocrefs"):
return
env.blocref_all_blocrefs = [
blocref for blocref in env.blocref_all_blocrefs if blocref["docname"] != docname
]
def merge_blocref(app, env, docnames, other):
"""
merge_blocref
"""
if not hasattr(other, "blocref_all_blocrefs"):
return
if not hasattr(env, "blocref_all_blocrefs"):
env.blocref_all_blocrefs = []
env.blocref_all_blocrefs.extend(other.blocref_all_blocrefs)
def visit_blocref_node(self, node):
"""
visit_blocref_node
"""
self.visit_admonition(node)
def depart_blocref_node(self, node):
"""
depart_blocref_node,
see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
"""
self.depart_admonition(node)
def visit_blocreflist_node(self, node):
"""
visit_blocreflist_node
see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
"""
self.visit_admonition(node)
def depart_blocreflist_node(self, node):
"""
depart_blocref_node
"""
self.depart_admonition(node)
def setup(app):
"""
setup for ``blocref`` (sphinx)
"""
if hasattr(app, "add_mapping"):
app.add_mapping("blocref", blocref_node)
app.add_mapping("blocreflist", blocreflist)
app.add_config_value("blocref_include_blocrefs", True, "html")
app.add_config_value("blocref_link_only", False, "html")
app.add_node(
blocreflist,
html=(visit_blocreflist_node, depart_blocreflist_node),
epub=(visit_blocreflist_node, depart_blocreflist_node),
latex=(visit_blocreflist_node, depart_blocreflist_node),
elatex=(visit_blocreflist_node, depart_blocreflist_node),
text=(visit_blocreflist_node, depart_blocreflist_node),
md=(visit_blocreflist_node, depart_blocreflist_node),
rst=(visit_blocreflist_node, depart_blocreflist_node),
)
app.add_node(
blocref_node,
html=(visit_blocref_node, depart_blocref_node),
epub=(visit_blocref_node, depart_blocref_node),
elatex=(visit_blocref_node, depart_blocref_node),
latex=(visit_blocref_node, depart_blocref_node),
text=(visit_blocref_node, depart_blocref_node),
md=(visit_blocref_node, depart_blocref_node),
rst=(visit_blocref_node, depart_blocref_node),
)
app.add_directive("blocref", BlocRef)
app.add_directive("blocreflist", BlocRefList)
app.connect("doctree-read", process_blocrefs)
app.connect("doctree-resolved", process_blocref_nodes)
app.connect("env-purge-doc", purge_blocrefs)
app.connect("env-merge-info", merge_blocref)
return {"version": sphinx.__display_version__, "parallel_read_safe": True}