Skip to content

Commit

Permalink
👌 IMPROVE: Bug fix, latex support and some refactoring #33
Browse files Browse the repository at this point in the history
* proper enumeration

* bug fixes and latex support

* writing post-transforms and node output handling

* edits

* if not number for enumerable node

* handling of refs

* test file

* handled nodes

* test corrections

* creating different transforms

* readding test

* some refactoring

* created global variables, and moved code around
  • Loading branch information
AakashGfude authored Sep 23, 2021
1 parent 78bf69b commit ee0f065
Show file tree
Hide file tree
Showing 20 changed files with 417 additions and 345 deletions.
407 changes: 174 additions & 233 deletions sphinx_exercise/__init__.py

Large diffs are not rendered by default.

57 changes: 29 additions & 28 deletions sphinx_exercise/directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from sphinx.util.docutils import SphinxDirective
from docutils.parsers.rst import directives
from .local_nodes import enumerable_node, unenumerable_node, linked_node
from .nodes import exercise_node, exercise_unenumerable_node, solution_node
from docutils import nodes
from sphinx.util import logging

Expand All @@ -25,42 +25,42 @@ class CustomDirective(SphinxDirective):
name = ""

def run(self) -> List[Node]:
if self.name == "solution" and self.env.app.config.hide_solutions:
env = self.env
typ = self.name
if typ == "solution" and env.app.config.hide_solutions:
return []

serial_no = self.env.new_serialno()
serial_no = env.new_serialno()

if not hasattr(self.env, "exercise_list"):
self.env.exercise_list = {}
if not hasattr(env, "exercise_list"):
env.exercise_list = {}

classes, class_name = [self.name], self.options.get("class", "")
classes, class_name = [typ], self.options.get("class", "")
if class_name:
classes.extend(class_name)

title_text, title = "", ""
if self.name == "exercise":
if "nonumber" in self.options:
title_text = f"{self.name.title()} "
title_text = ""
if typ == "exercise":
title_text = f"{self.name.title()} "

if self.arguments != []:
title_text += f"({self.arguments[0]})"
title += self.arguments[0]
title_text = f"({self.arguments[0]})"
else:
title_text = f"{self.name.title()} to "
target_label = self.arguments[0]

textnodes, messages = self.state.inline_text(title_text, self.lineno)

section = nodes.section(ids=[f"{self.name}-content"])
section = nodes.section(ids=[f"{typ}-content"])
self.state.nested_parse(self.content, self.content_offset, section)

if self.name == "exercise":
if typ == "exercise":
if "nonumber" in self.options:
node = unenumerable_node()
node = exercise_unenumerable_node()
else:
node = enumerable_node()
node = exercise_node()
else:
node = linked_node()
node = solution_node()

node += nodes.title(title_text, "", *textnodes)
node += section
Expand All @@ -70,13 +70,13 @@ def run(self) -> List[Node]:
self.options["noindex"] = False
else:
self.options["noindex"] = True
label = f"{self.env.docname}-{self.name}-{serial_no}"
label = f"{env.docname}-{typ}-{serial_no}"

# Duplicate label warning
if not label == "" and label in self.env.exercise_list.keys():
docpath = self.env.doc2path(self.env.docname)
if not label == "" and label in env.exercise_list.keys():
docpath = env.doc2path(env.docname)
path = docpath[: docpath.rfind(".")]
other_path = self.env.doc2path(self.env.exercise_list[label]["docname"])
other_path = env.doc2path(env.exercise_list[label]["docname"])
msg = f"duplicate label: {label}; other instance in {other_path}"
logger.warning(msg, location=path, color="red")
return []
Expand All @@ -87,20 +87,21 @@ def run(self) -> List[Node]:
node["classes"].extend(classes)
node["ids"].append(label)
node["label"] = label
node["docname"] = self.env.docname
node["docname"] = env.docname
node["title"] = title_text
node["type"] = typ
node["hidden"] = True if "hidden" in self.options else False
node.document = self.state.document

if self.name == "solution":
if typ == "solution":
node["target_label"] = target_label

self.add_name(node)

self.env.exercise_list[label] = {
"type": self.name,
"docname": self.env.docname,
env.exercise_list[label] = {
"type": typ,
"docname": env.docname,
"node": node,
"title": title,
"title": title_text,
"hidden": node.get("hidden", bool),
}

Expand Down
69 changes: 0 additions & 69 deletions sphinx_exercise/local_nodes.py

This file was deleted.

145 changes: 145 additions & 0 deletions sphinx_exercise/nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
sphinx_exercise.nodes
~~~~~~~~~~~~~~~~~~~~~
Enumerable and unenumerable nodes
:copyright: Copyright 2020 by the QuantEcon team, see AUTHORS
:licences: see LICENSE for details
"""
from docutils.nodes import Node
from sphinx.util import logging
from docutils import nodes as docutil_nodes
from sphinx.writers.latex import LaTeXTranslator
from .utils import get_node_number, find_parent

logger = logging.getLogger(__name__)

CR = "\n"
latex_admonition_start = CR + "\\begin{sphinxadmonition}{note}"
latex_admonition_end = "\\end{sphinxadmonition}" + CR


class exercise_node(docutil_nodes.Admonition, docutil_nodes.Element):
pass


class solution_node(docutil_nodes.Admonition, docutil_nodes.Element):
pass


class exercise_unenumerable_node(docutil_nodes.Admonition, docutil_nodes.Element):
pass


def visit_enumerable_node(self, node: Node) -> None:
if isinstance(self, LaTeXTranslator):
docname = find_parent(self.builder.env, node, "section")
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
self.body.append(latex_admonition_start)
else:
for title in node.traverse(docutil_nodes.title):
if "Exercise" in title.astext():
title[0] = docutil_nodes.Text("")
self.body.append(self.starttag(node, "div", CLASS="admonition"))


def depart_enumerable_node(self, node: Node) -> None:
typ = node.attributes.get("type", "")
if isinstance(self, LaTeXTranslator):
number = get_node_number(self, node, typ)
idx = list_rindex(self.body, latex_admonition_start) + 2
self.body.insert(idx, f"{typ.title()} {number} ")
self.body.append(latex_admonition_end)
else:
number = get_node_number(self, node, typ)
if number:
idx = self.body.index(f"{typ.title()} {number} ")
self.body[idx] = f"{typ.title()} {number} "
self.body.append("</div>")


def visit_exercise_unenumerable_node(self, node: Node) -> None:
if isinstance(self, LaTeXTranslator):
docname = find_parent(self.builder.env, node, "section")
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
self.body.append(latex_admonition_start)
else:
for title in node.traverse(docutil_nodes.title):
if "Exercise" in title.astext():
title[0] = docutil_nodes.Text("")
self.body.append(self.starttag(node, "div", CLASS="admonition"))


def depart_exercise_unenumerable_node(self, node: Node) -> None:
typ = node.attributes.get("type", "")
if isinstance(self, LaTeXTranslator):
idx = list_rindex(self.body, latex_admonition_start) + 2
self.body.insert(idx, f"{typ.title()} ")
self.body.append(latex_admonition_end)
else:
idx = list_rindex(self.body, '<p class="admonition-title">') + 1
element = f"<span>{typ.title()} </span>"
self.body.insert(idx, element)
self.body.append("</div>")


def visit_solution_node(self, node: Node) -> None:
if isinstance(self, LaTeXTranslator):
docname = find_parent(self.builder.env, node, "section")
self.body.append("\\label{" + f"{docname}:{node.attributes['label']}" + "}")
self.body.append(latex_admonition_start)
else:
self.body.append(self.starttag(node, "div", CLASS="admonition"))


def depart_solution_node(self, node: Node) -> None:
typ = node.attributes.get("type", "")
if isinstance(self, LaTeXTranslator):
idx = list_rindex(self.body, latex_admonition_start) + 2
self.body.pop(idx)
self.body.insert(idx, f"{typ.title()} ")
self.body.append(latex_admonition_end)
else:
number = get_node_number(self, node, typ)
idx = self.body.index(f"{typ.title()} {number} ")
self.body.pop(idx)
self.body.append("</div>")


def is_exercise_node(node):
return isinstance(node, exercise_node)


def is_unenumerable_node(node):
return isinstance(node, exercise_unenumerable_node)


def is_solution_node(node):
return isinstance(node, solution_node)


def is_extension_node(node):
return (
is_exercise_node(node) or is_unenumerable_node(node) or is_solution_node(node)
)


def rreplace(s, old, new, occurrence):
# taken from https://stackoverflow.com/a/2556252
li = s.rsplit(old, occurrence)
return new.join(li)


def list_rindex(li, x) -> int:
"""Getting the last occurence of an item in a list."""
for i in reversed(range(len(li))):
if li[i] == x:
return i
raise ValueError("{} is not in list".format(x))


NODE_TYPES = {
"exercise": {"node": exercise_node, "type": "exercise"},
"solution": {"node": solution_node, "type": "solution"},
}
55 changes: 55 additions & 0 deletions sphinx_exercise/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from sphinx.writers.latex import LaTeXTranslator
from docutils import nodes as docutil_nodes


def find_parent(env, node, parent_tag):
"""Find the nearest parent node with the given tagname."""
while True:
node = node.parent
if node is None:
return None
# parent should be a document in toc
if (
"docname" in node.attributes
and env.titles[node.attributes["docname"]].astext().lower()
in node.attributes["names"]
):
return node.attributes["docname"]

if node.tagname == parent_tag:
return node.attributes["docname"]

return None


def get_node_number(self, node, typ) -> str:
"""Get the number for the directive node for HTML."""
ids = node.attributes.get("ids", [])[0]
if isinstance(self, LaTeXTranslator):
docname = find_parent(self.builder.env, node, "section")
else:
docname = node.attributes.get("docname", "")
# Latex does not have builder.fignumbers
fignumbers = self.builder.env.toc_fignumbers.get(docname, {})
number = fignumbers.get(typ, {}).get(ids, ())
return ".".join(map(str, number))


def has_math_child(node):
""" Check if a parent node as a math child node. """
for item in node:
if isinstance(item, docutil_nodes.math):
return True
return False


def get_refuri(node):
""" Check both refuri and refid, to see which one is available. """
id_ = ""
if node.get("refuri", ""):
id_ = node.get("refuri", "")

if node.get("refid", ""):
id_ = node.get("refid", "")

return id_.split("#")[-1]
1 change: 0 additions & 1 deletion tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def test_warnings(app, warnings):
"_enum_numref_notitle.rst:6: WARNING: invalid numfig_format: some text"
in warnings(app)
)
assert "WARNING: invalid numfig_format: some text {name}" in warnings(app)
assert (
"_enum_numref_title.rst:6: WARNING: invalid numfig_format: some text"
in warnings(app)
Expand Down
Loading

0 comments on commit ee0f065

Please sign in to comment.