import os
from docutils import nodes
from docutils.parsers.rst import directives
from docutils.frontend import Values
import sphinx
from sphinx.locale import _
try:
from sphinx.errors import NoUri
except ImportError: # pragma: no cover
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 sphinx.util.nodes import set_source_info, process_index_entry
from sphinx import addnodes
from ..language import TITLES
[docs]
class mathdef_node(nodes.admonition):
"""
Defines ``mathdef`` node.
"""
pass
class mathdeflist(nodes.General, nodes.Element):
"""
Defines ``mathdeflist`` node.
"""
pass
[docs]
class MathDef(BaseAdmonition):
"""
A ``mathdef`` entry, displayed in the form of an admonition.
It takes the following options:
* *title*: a title for the math
* *tag*: a tag to have several categories of math
* *lid* or *label*: a label to refer to
* *index*: to add an entry to the index (comma separated)
Example::
.. mathdef::
:title: title
:tag: definition or theorem or ...
:lid: id (used for further reference)
Description of the math
"""
node_class = mathdef_node
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,
}
[docs]
def run(self):
"""
Builds the mathdef text.
"""
# sett = self.state.document.settings
# language_code = sett.language_code
lineno = self.lineno
env = (
self.state.document.settings.env
if hasattr(self.state.document.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 hasattr(env, "settings") and hasattr(env.settings, "mathdef_link_number"):
number_format = env.settings.mathdef_link_number
elif hasattr(self.state.document.settings, "mathdef_link_number"):
number_format = self.state.document.settings.mathdef_link_number
elif hasattr(env, "config") and hasattr(env.config, "mathdef_link_number"):
number_format = env.config.mathdef_link_number
else:
raise ValueError( # pragma: no cover
"mathdef_link_number is not defined in the configuration"
)
if not self.options.get("class"):
self.options["class"] = ["admonition-mathdef"]
# body
(mathdef,) = super(MathDef, self).run()
if isinstance(mathdef, nodes.system_message):
return [mathdef]
# 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
mathtag = self.options.get("tag", "").strip()
if len(mathtag) == 0:
raise ValueError("tag is empty") # pragma: no cover
if env is not None:
mid = int(env.new_serialno(f"indexmathe-u-{mathtag}")) + 1
else:
mid = -1
# id of the section
first_letter = mathtag[0].upper()
number = mid
try:
label_number = number_format.format(
number=number, first_letter=first_letter
)
except ValueError as e: # pragma: no cover
raise RuntimeError(f"Unable to interpret format '{number_format}'.") from e
# title
title = self.options.get("title", "").strip()
if len(title) > 0:
title = f"{mathtag} {label_number} : {title}"
else:
raise ValueError("title is empty") # pragma: no cover
# main node
ttitle = title
title = nodes.title(text=_(title))
if container is not None:
mathdef.insert(0, title)
mathdef.insert(0, container)
else:
mathdef.insert(0, title)
mathdef["mathtag"] = mathtag
mathdef["mathmid"] = mid
mathdef["mathtitle"] = ttitle
set_source_info(self, mathdef)
if env is not None:
targetid = "indexmathe-%s%s" % (
mathtag,
env.new_serialno("indexmathe%s" % mathtag),
)
targetnode = nodes.target(legend, "", ids=[targetid])
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}\ntid={2}\ntnode={3}".format(
None if env is None else env.docname,
lineno,
targetid,
targetnode,
)
) from e
# index node
index = self.options.get("index", None)
imposed = ",".join(a for a in [mathtag, ttitle] if a)
if index is None or len(index.strip()) == 0:
index = imposed
else:
index += "," + imposed
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
return [a for a in [indexnode, targetnode, mathdef] if a is not None]
def process_mathdefs(app, doctree):
"""
Collects all mathdefs 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
if not hasattr(env, "mathdef_all_mathsext"):
env.mathdef_all_mathsext = []
for node in doctree.traverse(mathdef_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()
mathtag = newnode["mathtag"]
mathtitle = newnode["mathtitle"]
mathmid = newnode["mathmid"]
del newnode["ids"]
del newnode["mathtag"]
env.mathdef_all_mathsext.append(
{
"docname": env.docname,
"source": node.source or env.doc2path(env.docname),
"lineno": node.line,
"mathdef": newnode,
"target": targetnode,
"mathtag": mathtag,
"mathtitle": mathtitle,
"mathmid": mathmid,
}
)
class MathDefList(Directive):
"""
A list of all mathdef entries, for a specific tag.
* tag: a tag to have several categories of mathdef
* contents: add a bullet list with links to added blocs
Example::
.. mathdeflist::
:tag: issue
:contents:
"""
has_content = False
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
"tag": directives.unchanged,
"contents": directives.unchanged,
}
def run(self):
"""
Simply insert an empty mathdeflist node which will be replaced later
when process_mathdef_nodes is called
"""
env = (
self.state.document.settings.env
if hasattr(self.state.document.settings, "env")
else None
)
tag = self.options.get("tag", "").strip()
contents = self.options.get("contents", False) in (
True,
"True",
"true",
1,
"1",
"",
None,
"None",
)
if env is not None:
targetid = f"indexmathelist-{env.new_serialno('indexmathelist')}"
targetnode = nodes.target("", "", ids=[targetid])
n = mathdeflist("")
n["mathtag"] = tag
n["mathcontents"] = contents
n["docname"] = env.docname if env else "none"
return [targetnode, n]
n = mathdeflist("")
n["mathtag"] = tag
n["mathcontents"] = contents
n["docname"] = env.docname if env else "none"
return [n]
def process_mathdef_nodes(app, doctree, fromdocname):
"""
process_mathdef_nodes
"""
if not app.config["mathdef_include_mathsext"]:
for node in doctree.traverse(mathdef_node):
node.parent.remove(node)
# Replace all mathdeflist nodes with a list of the collected mathsext.
# Augment each mathdef with a backlink to the original location.
env = app.builder.env
if hasattr(env, "settings") and hasattr(env.settings, "language_code"):
lang = env.settings.language_code
else:
lang = "en"
orig_entry = TITLES[lang]["original entry"]
mathmes = TITLES[lang]["mathmes"]
if not hasattr(env, "mathdef_all_mathsext"):
env.mathdef_all_mathsext = []
for ilist, node in enumerate(doctree.traverse(mathdeflist)):
if "ids" in node:
node["ids"] = []
if not app.config["mathdef_include_mathsext"]:
node.replace_self([])
continue
nbmath = 0
content = []
mathtag = node["mathtag"]
add_contents = node["mathcontents"]
mathdocname = node["docname"]
if add_contents:
bullets = nodes.enumerated_list()
content.append(bullets)
double_list = [
(info.get("mathtitle", ""), info) for info in env.mathdef_all_mathsext
]
double_list.sort(key=lambda x: x[:1])
for n, mathdef_info_ in enumerate(double_list):
mathdef_info = mathdef_info_[1]
if mathdef_info["mathtag"] != mathtag:
continue
nbmath += 1
para = nodes.paragraph(classes=["mathdef-source"])
if app.config["mathdef_link_only"]:
description = _(f"<<{orig_entry}>>")
else:
description = _(mathmes) % (
orig_entry,
os.path.split(mathdef_info["source"])[-1],
mathdef_info["lineno"],
)
desc1 = description[: description.find("<<")]
desc2 = description[description.find(">>") + 2 :]
para += nodes.Text(desc1, desc1)
# Create a reference
newnode = nodes.reference("", "", internal=True)
innernode = nodes.emphasis("", _(orig_entry))
try:
newnode["refuri"] = app.builder.get_relative_uri(
fromdocname, mathdef_info["docname"]
)
try:
newnode["refuri"] += "#" + mathdef_info["target"]["refid"]
except Exception as e: # pragma: no cover
raise KeyError(
"refid in not present in '{0}'".format(mathdef_info["target"])
) from e
except NoUri: # pragma: no cover
# ignore if no URI can be determined, e.g. for LaTeX output
pass
newnode.append(innernode)
para += newnode
para += nodes.Text(desc2, desc2)
# (Recursively) resolve references in the mathdef content
mathdef_entry = mathdef_info["mathdef"]
idss = ["index-mathdef-%d-%d" % (ilist, n)]
# Insert into the mathreflist
if add_contents:
title = mathdef_info["mathtitle"]
item = nodes.list_item()
p = nodes.paragraph()
item += p
newnode = nodes.reference("", "", internal=True)
innernode = nodes.paragraph(text=title)
try:
newnode["refuri"] = app.builder.get_relative_uri(
fromdocname, mathdocname
)
newnode["refuri"] += "#" + idss[0]
except NoUri: # pragma: no cover
# ignore if no URI can be determined, e.g. for LaTeX output
pass
newnode.append(innernode)
p += newnode
bullets += item
mathdef_entry["ids"] = idss
if not hasattr(mathdef_entry, "settings"):
mathdef_entry.settings = Values()
mathdef_entry.settings.env = env
# If an exception happens here, see blog 2017-05-21 from the
# documentation.
env.resolve_references(mathdef_entry, mathdef_info["docname"], app.builder)
# Insert into the mathdeflist
content.append(mathdef_entry)
content.append(para)
node.replace_self(content)
def purge_mathsext(app, env, docname):
"""
purge_mathsext
"""
if not hasattr(env, "mathdef_all_mathsext"):
return
env.mathdef_all_mathsext = [
mathdef for mathdef in env.mathdef_all_mathsext if mathdef["docname"] != docname
]
def merge_mathdef(app, env, docnames, other):
"""
merge_mathdef
"""
if not hasattr(other, "mathdef_all_mathsext"):
return
if not hasattr(env, "mathdef_all_mathsext"):
env.mathdef_all_mathsext = []
env.mathdef_all_mathsext.extend(other.mathdef_all_mathsext)
def visit_mathdef_node(self, node):
"""
visit_mathdef_node
"""
self.visit_admonition(node)
def depart_mathdef_node(self, node):
"""
depart_mathdef_node,
see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
"""
self.depart_admonition(node)
def visit_mathdeflist_node(self, node):
"""
visit_mathdeflist_node
see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
"""
self.visit_admonition(node)
def depart_mathdeflist_node(self, node):
"""
depart_mathdef_node
"""
self.depart_admonition(node)
def setup(app):
"""
setup for ``mathdef`` (sphinx)
"""
if hasattr(app, "add_mapping"):
app.add_mapping("mathdef", mathdef_node)
app.add_mapping("mathdeflist", mathdeflist)
app.add_config_value("mathdef_include_mathsext", True, "html")
app.add_config_value("mathdef_link_only", True, "html")
app.add_config_value("mathdef_link_number", "{first_letter}{number}", "html")
app.add_node(
mathdeflist,
html=(visit_mathdeflist_node, depart_mathdeflist_node),
epub=(visit_mathdeflist_node, depart_mathdeflist_node),
elatex=(visit_mathdeflist_node, depart_mathdeflist_node),
latex=(visit_mathdeflist_node, depart_mathdeflist_node),
text=(visit_mathdeflist_node, depart_mathdeflist_node),
md=(visit_mathdeflist_node, depart_mathdeflist_node),
rst=(visit_mathdeflist_node, depart_mathdeflist_node),
)
app.add_node(
mathdef_node,
html=(visit_mathdef_node, depart_mathdef_node),
epub=(visit_mathdef_node, depart_mathdef_node),
elatex=(visit_mathdef_node, depart_mathdef_node),
latex=(visit_mathdef_node, depart_mathdef_node),
text=(visit_mathdef_node, depart_mathdef_node),
md=(visit_mathdef_node, depart_mathdef_node),
rst=(visit_mathdef_node, depart_mathdef_node),
)
app.add_directive("mathdef", MathDef)
app.add_directive("mathdeflist", MathDefList)
app.connect("doctree-read", process_mathdefs)
app.connect("doctree-resolved", process_mathdef_nodes)
app.connect("env-purge-doc", purge_mathsext)
app.connect("env-merge-info", merge_mathdef)
return {"version": sphinx.__display_version__, "parallel_read_safe": True}