diff --git a/CHANGELOGS.rst b/CHANGELOGS.rst index baa938f..1df5536 100644 --- a/CHANGELOGS.rst +++ b/CHANGELOGS.rst @@ -4,6 +4,7 @@ Change Logs 0.2.0 +++++ +* :pr:`23`: add sphinx extension quote * :pr:`21`: add sphinx extension faqref, runpython uses black * :pr:`17`: add RST writer to output the documentation into pure RST format * :pr:`16`: add command line nb2py to convert notebook to python files diff --git a/_doc/api/index.rst b/_doc/api/index.rst index f368f75..c386747 100644 --- a/_doc/api/index.rst +++ b/_doc/api/index.rst @@ -15,6 +15,7 @@ Extensions docassert epkg gdot + quote runpython tools rst_builder diff --git a/_doc/api/quote.rst b/_doc/api/quote.rst new file mode 100644 index 0000000..ec7cd4d --- /dev/null +++ b/_doc/api/quote.rst @@ -0,0 +1,49 @@ +===== +quote +===== + +A bloc to insert a quote from a book, a film... + +Usage +===== + +In *conf.py*: + +:: + + extensions = [ ... + 'sphinx_runpython.quote', + ] + +One example: + +:: + + .. quote:: + :author: Esther Duflo + :book: Expérience, science et lutte contre la pauvreté + :year: 2013 + :index: economy + :pages: 26 + + [Roosevelt] It is common sense to take a method and try it: + if it fails, admit frankly and try another. But above all, + try something. + +Which gives: + +.. quote:: + :author: Esther Duflo + :book: Expérience, science et lutte contre la pauvreté + :year: 2013 + :index: pauvreté + :pages: 26 + + [Roosevelt] It is common sense to take a method and try it: + if it fails, admit frankly and try another. But above all, + try something. + +Directive +========= + +.. autoclass:: sphinx_runpython.quote.sphinx_quote_extension.QuoteNode diff --git a/_doc/conf.py b/_doc/conf.py index b8b451e..1e2037d 100644 --- a/_doc/conf.py +++ b/_doc/conf.py @@ -26,6 +26,7 @@ "sphinx_runpython.docassert", "sphinx_runpython.gdot", "sphinx_runpython.epkg", + "sphinx_runpython.quote", "sphinx_runpython.runpython", "sphinx_runpython.sphinx_rst_builder", ] diff --git a/_unittests/ut_epkg/test_epkg_extension.py b/_unittests/ut_epkg/test_epkg_extension.py index 3a2ccf9..d9e9185 100644 --- a/_unittests/ut_epkg/test_epkg_extension.py +++ b/_unittests/ut_epkg/test_epkg_extension.py @@ -24,11 +24,7 @@ def test_epkg_module(self): " ", "" ) content = content.replace('u"', '"') - - html = rst2html( - content, - writer_name="rst", - ) + html = rst2html(content, writer_name="rst") t1 = "abeforea" if t1 not in html: diff --git a/_unittests/ut_quote/test_quote_extension.py b/_unittests/ut_quote/test_quote_extension.py new file mode 100644 index 0000000..b49db17 --- /dev/null +++ b/_unittests/ut_quote/test_quote_extension.py @@ -0,0 +1,454 @@ +import unittest +from sphinx_runpython.process_rst import rst2html +from sphinx_runpython.ext_test_case import ExtTestCase + + +class TestQuoteExtension(ExtTestCase): + def test_quote(self): + content = """ + .. quote:: + :author: auteur + :book: livre titre + :lid: label1 + :pages: 234 + :year: 2018 + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "livre titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "livre titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_manga(self): + content = """ + .. quote:: + :author: auteur + :manga: manga titre + :lid: label1 + :pages: 234 + :year: 2018 + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "manga titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "manga titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_film(self): + content = """ + .. quote:: + :author: auteur + :film: film titre + :lid: label1 + :pages: 234 + :year: 2018 + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "film titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "film titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_show(self): + content = """ + .. quote:: + :author: auteur + :show: show titre + :lid: label1 + :pages: 234 + :year: 2018 + :title1: true + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "show titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "show titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_comic(self): + content = """ + .. quote:: + :author: auteur + :comic: comic titre + :lid: label1 + :pages: 234 + :year: 2018 + :title1: true + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "comic titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "comic titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_disc(self): + content = """ + .. quote:: + :author: auteur + :disc: disc titre + :lid: label1 + :pages: 234 + :year: 2018 + :title1: true + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "disc titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "disc titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_ado(self): + content = """ + .. quote:: + :author: auteur + :ado: ado titre + :lid: label1 + :pages: 234 + :year: 2018 + :title1: true + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "ado titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "ado titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + def test_quote_child(self): + content = """ + .. quote:: + :author: auteur + :child: child titre + :lid: label1 + :pages: 234 + :year: 2018 + :title1: true + + this code should appear___ + + next + """.replace( + " ", "" + ) + content = content.replace('u"', '"') + + html = rst2html( + content, + writer_name="html", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in html: + raise AssertionError(html) + if "auteur" not in html: + raise AssertionError(html) + if "child titre" not in html: + raise AssertionError(html) + if "234" not in html: + raise AssertionError(html) + + rst = rst2html( + content, + writer_name="rst", + keep_warnings=True, + extlinks={"issue": ("http://%s", "_issue_%s")}, + ) + + t1 = "this code should appear" + if t1 not in rst: + raise AssertionError(rst) + if "auteur" not in rst: + raise AssertionError(rst) + if "child titre" not in rst: + raise AssertionError(rst) + if "234" not in rst: + raise AssertionError(rst) + if ".. quote::" not in rst: + raise AssertionError(rst) + if ":author: auteur" not in rst: + raise AssertionError(rst) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyproject.toml b/pyproject.toml index f2b58ae..9fb2861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ ignore_directives = [ "image-sg", "mathdef", "mathdeflist", + "quote", "runpython", ] ignore_roles = ["epkg"] diff --git a/sphinx_runpython/process_rst.py b/sphinx_runpython/process_rst.py index da9e176..fef858d 100644 --- a/sphinx_runpython/process_rst.py +++ b/sphinx_runpython/process_rst.py @@ -26,6 +26,7 @@ "sphinx_runpython.docassert", "sphinx_runpython.epkg", "sphinx_runpython.runpython", + "sphinx_runpython.quote", "sphinx_runpython.sphinx_rst_builder", ] diff --git a/sphinx_runpython/quote/__init__.py b/sphinx_runpython/quote/__init__.py new file mode 100644 index 0000000..483609b --- /dev/null +++ b/sphinx_runpython/quote/__init__.py @@ -0,0 +1,3 @@ +from .sphinx_quote_extension import setup + +__all__ = ["setup"] diff --git a/sphinx_runpython/quote/sphinx_quote_extension.py b/sphinx_runpython/quote/sphinx_quote_extension.py new file mode 100644 index 0000000..caee27f --- /dev/null +++ b/sphinx_runpython/quote/sphinx_quote_extension.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +from docutils import nodes +from docutils.parsers.rst import directives + +import sphinx +from sphinx.locale import _ +from docutils.parsers.rst.directives.admonitions import BaseAdmonition +from docutils.statemachine import StringList +from sphinx.util.nodes import nested_parse_with_titles +from ..language import TITLES + + +class quote_node(nodes.admonition): + """ + Defines ``quote`` node. + """ + + pass + + +class QuoteNode(BaseAdmonition): + """ + A ``quotedef`` entry, displayed in the form of an admonition. + It takes the following options: + + * *author* + * *book* or *manga* or *film* or *show* or *disc* or + *comic* or *child* or *ado* + * *year* + * *pages* + * *tag* + * *source* + * *lid* or *label* + * *index*, additional index words beside the title and the author + * *date*, if the text was written or declared at specific date + * *title1*, by default, the author comes first, if True, the title is + + Example:: + + .. quote:: + :author: author + :book: book + :year: year + :pages: pages (optional) + :tag: something + :lid: id (used for further reference) + :source: optional + :index: word + + A monkey could... + """ + + node_class = quote_node + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + "author": directives.unchanged, + "book": directives.unchanged, + "manga": directives.unchanged, + "disc": directives.unchanged, + "ado": directives.unchanged, + "child": directives.unchanged, + "comic": directives.unchanged, + "show": directives.unchanged, + "film": directives.unchanged, + "year": directives.unchanged, + "pages": directives.unchanged, + "tag": directives.unchanged, + "lid": directives.unchanged, + "label": directives.unchanged, + "source": directives.unchanged, + "class": directives.class_option, + "index": directives.unchanged, + "date": directives.unchanged, + "title1": directives.unchanged, + } + + def run(self): + """ + Builds the mathdef text. + """ + 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] + language_code = ( + self.state.document.settings.language_code + if hasattr(self.state.document.settings, "language_code") + else "en" + ) + + if not self.options.get("class"): + self.options["class"] = ["admonition-quote"] + + # body + (quote,) = super(QuoteNode, self).run() + if isinstance(quote, nodes.system_message): + return [quote] # pragma: no cover + + # mid + tag = self.options.get("tag", "quotetag").strip() + if len(tag) == 0: + raise ValueError("tag is empty") # pragma: no cover + + def __(text): + if text: + return _(text) + return "" + + # book + author = __(self.options.get("author", "").strip()) + book = __(self.options.get("book", "").strip()) + manga = __(self.options.get("manga", "").strip()) + comic = __(self.options.get("comic", "").strip()) + ado = __(self.options.get("ado", "").strip()) + child = __(self.options.get("child", "").strip()) + disc = __(self.options.get("disc", "").strip()) + film = __(self.options.get("film", "").strip()) + show = __(self.options.get("show", "").strip()) + pages = __(self.options.get("pages", "").strip()) + year = __(self.options.get("year", "").strip()) + source = __(self.options.get("source", "").strip()) + index = __(self.options.get("index", "").strip()) + date = __(self.options.get("date", "").strip()) + title1 = __(self.options.get("title1", "").strip()) in ( + "1", + 1, + "True", + True, + "true", + ) + + indexes = [] + if index: + indexes.append(index) # pragma: no cover + + # add a label + lid = self.options.get("lid", self.options.get("label", None)) + if lid: + tnl = ["", f".. _{lid}:", ""] + else: + tnl = [] # pragma: no cover + + if title1: + if ado: + tnl.append(f"**{ado}**") + if child: + tnl.append(f"**{child}**") + if comic: + tnl.append(f"**{comic}**") + if disc: + tnl.append(f"**{disc}**") + if book: + tnl.append(f"**{book}**") + if manga: + tnl.append(f"**{manga}**") + if show: + tnl.append(f"**{show}**") + if film: + tnl.append(f"**{film}**") + if author: + tnl.append(f"*{author}*, ") + else: + if author: + tnl.append(f"**{author}**, ") + if ado: + tnl.append(f"*{ado}*") + if child: + tnl.append(f"*{child}*") + if comic: + tnl.append(f"*{comic}*") + if disc: + tnl.append(f"*{disc}*") + if book: + tnl.append(f"*{book}*") + if manga: + tnl.append(f"*{manga}*") + if show: + tnl.append(f"*{show}*") + if film: + tnl.append(f"*{film}*") + + if author: + indexes.append(author) + indexes.append(TITLES[language_code]["author"] + "; " + author) + if ado: + indexes.append(ado) + indexes.append(TITLES[language_code]["ado"] + "; " + ado) + if child: + indexes.append(child) + indexes.append(TITLES[language_code]["child"] + "; " + child) + if comic: + indexes.append(comic) + indexes.append(TITLES[language_code]["comic"] + "; " + comic) + if disc: + indexes.append(disc) + indexes.append(TITLES[language_code]["disc"] + "; " + disc) + if book: + indexes.append(book) + indexes.append(TITLES[language_code]["book"] + "; " + book) + if manga: + indexes.append(manga) + indexes.append(TITLES[language_code]["manga"] + "; " + manga) + if show: + indexes.append(show) + indexes.append(TITLES[language_code]["show"] + "; " + show) + if film: + indexes.append(film) + indexes.append(TITLES[language_code]["film"] + "; " + film) + + if pages: + tnl.append(f", {pages}") + if date: + tnl.append(f" ({date})") + if year: + tnl.append(f" ({year})") + if source: + if source.startswith("http"): + tnl.append(f", `source <{source}>`_") + else: + tnl.append(f", {source}") + tnl.append("") + tnl.append(".. index:: " + ", ".join(indexes)) + tnl.append("") + + content = StringList(tnl) + content = content + self.content + node = quote_node() + + try: + nested_parse_with_titles(self.state, content, node) + except Exception as e: # pragma: no cover + from sphinx.util import logging + + logger = logging.getLogger("blogpost") + logger.warning( + "[blogpost] unable to parse %r - %r - %r", author, book or manga, e + ) + raise e + + node["tag"] = tag + node["author"] = author + node["pages"] = pages + node["year"] = year + node["label"] = lid + node["source"] = source + node["book"] = book + node["manga"] = manga + node["disc"] = disc + node["comic"] = comic + node["ado"] = ado + node["child"] = child + node["film"] = film + node["show"] = show + node["index"] = index + node["content"] = "\n".join(self.content) + node["classes"] += ["quote"] + + return [node] + + +def visit_quote_node(self, node): + """ + visit_quote_node + """ + self.visit_admonition(node) + + +def depart_quote_node(self, node): + """ + depart_quote_node, + see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py + """ + self.depart_admonition(node) + + +def visit_quote_node_rst(self, node): + """ + visit_quote_node + """ + self.new_state(0) + self.add_text(".. quote::") + for k, v in sorted(node.attributes.items()): + if k in ("content", "classes"): + continue + if v: + self.new_state(4) + self.add_text(f":{k}: {v}") + self.end_state(wrap=False, end=None) + self.add_text(self.nl) + self.new_state(4) + self.add_text(node["content"]) + self.end_state() + self.end_state() + raise nodes.SkipNode + + +def depart_quote_node_rst(self, node): + """ + depart_quote_node, + see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py + """ + pass + + +def setup(app): + """ + setup for ``mathdef`` (sphinx) + """ + if hasattr(app, "add_mapping"): + app.add_mapping("quote", quote_node) + + app.add_node( + quote_node, + html=(visit_quote_node, depart_quote_node), + epub=(visit_quote_node, depart_quote_node), + elatex=(visit_quote_node, depart_quote_node), + latex=(visit_quote_node, depart_quote_node), + text=(visit_quote_node, depart_quote_node), + md=(visit_quote_node, depart_quote_node), + rst=(visit_quote_node_rst, depart_quote_node_rst), + ) + + app.add_directive("quote", QuoteNode) + return {"version": sphinx.__display_version__, "parallel_read_safe": True}