diff --git a/CHANGES b/CHANGES index 89ebe8a7e..f516b63d6 100644 --- a/CHANGES +++ b/CHANGES @@ -85,6 +85,12 @@ Bugs fixed the page content. * #1884, #1885: plug-in html themes cannot inherit another plug-in theme. Thanks to Suzumizaki. +* #1818: `sphinx.ext.todo` directive generates broken html class attribute as + 'admonition-' when :confval:`language` is specified with non-ASCII linguistic area like + 'ru' or 'ja'. To fix this, now ``todo`` directive can use ```:class:`` option. +* #2140: Fix footnotes in table has broken in LaTeX +* #2127: MecabBinder for html searching feature doesn't work with Python 3. + Thanks to Tomoko Uchida. Documentation ------------- diff --git a/doc/ext/todo.rst b/doc/ext/todo.rst index c0d94ba1d..8ab6eb37a 100644 --- a/doc/ext/todo.rst +++ b/doc/ext/todo.rst @@ -16,6 +16,10 @@ There are two additional directives when using this extension: It will only show up in the output if :confval:`todo_include_todos` is ``True``. + .. versionadded:: 1.3.2 + This directive supports an ``class`` option that determines the class attribute + for HTML output. If not given, the class defaults to ``admonition-todo``. + .. rst:directive:: todolist diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 64359c9b0..faa086fc8 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -13,6 +13,7 @@ """ from docutils import nodes +from docutils.parsers.rst import directives import sphinx from sphinx.locale import _ @@ -38,13 +39,18 @@ class Todo(Directive): required_arguments = 0 optional_arguments = 0 final_argument_whitespace = False - option_spec = {} + option_spec = { + 'class': directives.class_option, + } def run(self): env = self.state.document.settings.env targetid = 'index-%s' % env.new_serialno('index') targetnode = nodes.target('', '', ids=[targetid]) + if not self.options.get('class'): + self.options['class'] = ['admonition-todo'] + ad = make_admonition(todo_node, self.name, [_('Todo')], self.options, self.content, self.lineno, self.content_offset, self.block_text, self.state, self.state_machine) @@ -165,6 +171,7 @@ def merge_info(app, env, docnames, other): def visit_todo_node(self, node): self.visit_admonition(node) + # self.visit_admonition(node, 'todo') def depart_todo_node(self, node): diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py index 3c9ee88fd..23de74d37 100644 --- a/sphinx/search/ja.py +++ b/sphinx/search/ja.py @@ -21,7 +21,7 @@ import os import re import sys -from six import iteritems +from six import iteritems, PY3 try: import MeCab @@ -43,13 +43,16 @@ class MecabBinder(object): self.dict_encode = options.get('dic_enc', 'utf-8') def split(self, input): - input2 = input.encode(self.dict_encode) + input2 = input if PY3 else input.encode(self.dict_encode) if native_module: result = self.native.parse(input2) else: result = self.ctypes_libmecab.mecab_sparse_tostr( self.ctypes_mecab, input.encode(self.dict_encode)) - return result.decode(self.dict_encode).split(' ') + if PY3: + return result.split(' ') + else: + return result.decode(self.dict_encode).split(' ') def init_native(self, options): param = '-Owakati' diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index c98f10758..03d34a0c8 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -137,6 +137,7 @@ class Table(object): self.has_verbatim = False self.caption = None self.longtable = False + self.footnotes = [] class LaTeXTranslator(nodes.NodeVisitor): @@ -275,6 +276,7 @@ class LaTeXTranslator(nodes.NodeVisitor): # by .. highlight:: directive in the master file self.hlsettingstack = 2 * [[builder.config.highlight_language, sys.maxsize]] + self.bodystack = [] self.footnotestack = [] self.curfilestack = [] self.handled_abbrs = set() @@ -303,6 +305,15 @@ class LaTeXTranslator(nodes.NodeVisitor): self.remember_multirow = {} self.remember_multirowcol = {} + def pushbody(self, newbody): + self.bodystack.append(self.body) + self.body = newbody + + def popbody(self): + body = self.body + self.body = self.bodystack.pop() + return body + def format_docclass(self, docclass): """ prepends prefix to sphinx document classes """ @@ -495,7 +506,8 @@ class LaTeXTranslator(nodes.NodeVisitor): fnotes = {} for fn in footnotes_under(node): num = fn.children[0].astext().strip() - fnotes[num] = [collected_footnote(*fn.children), False] + newnode = collected_footnote(*fn.children, number=num) + fnotes[num] = [newnode, False] return fnotes def depart_start_of_file(self, node): @@ -602,8 +614,8 @@ class LaTeXTranslator(nodes.NodeVisitor): self.body.append('{') self.context.append('}\n') elif isinstance(parent, nodes.table): - self.table.caption = self.encode(node.astext()) - raise nodes.SkipNode + # Redirect body output until title is finished. + self.pushbody([]) else: self.builder.warn( 'encountered title node not in section, topic, table, ' @@ -615,7 +627,10 @@ class LaTeXTranslator(nodes.NodeVisitor): def depart_title(self, node): self.in_title = 0 - self.body.append(self.context.pop()) + if isinstance(node.parent, nodes.table): + self.table.caption = self.popbody() + else: + self.body.append(self.context.pop()) def visit_subtitle(self, node): if isinstance(node.parent, nodes.sidebar): @@ -743,7 +758,10 @@ class LaTeXTranslator(nodes.NodeVisitor): def visit_collected_footnote(self, node): self.in_footnote += 1 - self.body.append('\\footnote{') + if 'in_table' in node: + self.body.append('\\footnotetext[%s]{' % node['number']) + else: + self.body.append('\\footnote[%s]{' % node['number']) def depart_collected_footnote(self, node): self.body.append('}') @@ -770,16 +788,18 @@ class LaTeXTranslator(nodes.NodeVisitor): self.tablebody = [] self.tableheaders = [] # Redirect body output until table is finished. - self._body = self.body - self.body = self.tablebody + self.pushbody(self.tablebody) def depart_table(self, node): if self.table.rowcount > 30: self.table.longtable = True - self.body = self._body + self.popbody() if not self.table.longtable and self.table.caption is not None: - self.body.append(u'\n\n\\begin{threeparttable}\n' - u'\\capstart\\caption{%s}\n' % self.table.caption) + self.body.append('\n\n\\begin{threeparttable}\n' + '\\capstart\\caption{') + for caption in self.table.caption: + self.body.append(caption) + self.body.append('}') for id in self.next_table_ids: self.body.append(self.hypertarget(id, anchor=False)) if node['ids']: @@ -839,6 +859,10 @@ class LaTeXTranslator(nodes.NodeVisitor): self.body.append(endmacro) if not self.table.longtable and self.table.caption is not None: self.body.append('\\end{threeparttable}\n\n') + if self.table.footnotes: + for footnode in self.table.footnotes: + footnode['in_table'] = True + footnode.walkabout(self) self.table = None self.tablebody = None @@ -1531,7 +1555,14 @@ class LaTeXTranslator(nodes.NodeVisitor): # if a footnote has been inserted once, it shouldn't be repeated # by the next reference if used: - self.body.append('\\footnotemark[%s]' % num) + if self.table: + self.body.append('\\protect\\footnotemark[%s]' % num) + else: + self.body.append('\\footnotemark[%s]' % num) + elif self.table: + self.footnotestack[-1][num][1] = True + self.body.append('\\protect\\footnotemark[%s]' % num) + self.table.footnotes.append(footnode) else: if self.in_caption: raise UnsupportedError('%s:%s: footnotes in float captions ' diff --git a/tests/root/footnote.txt b/tests/root/footnote.txt index b600697fd..36ad3fadc 100644 --- a/tests/root/footnote.txt +++ b/tests/root/footnote.txt @@ -24,6 +24,17 @@ citation [bar]_ +footnotes in table +-------------------- + +.. list-table:: Table caption [#]_ + :header-rows: 1 + + * - name [#]_ + - desription + * - VIDIOC_CROPCAP + - Information about VIDIOC_CROPCAP + footenotes -------------------- @@ -39,6 +50,10 @@ footenotes .. [bar] cite +.. [#] footnotes in table caption + +.. [#] footnotes in table + missing target -------------------- diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 9679b2629..e51644df5 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -280,14 +280,18 @@ HTML_XPATH = { (".//dt/a", "double"), ], 'footnote.html': [ - (".//a[@class='footnote-reference'][@href='#id5'][@id='id1']", r"\[1\]"), - (".//a[@class='footnote-reference'][@href='#id6'][@id='id2']", r"\[2\]"), + (".//a[@class='footnote-reference'][@href='#id7'][@id='id1']", r"\[1\]"), + (".//a[@class='footnote-reference'][@href='#id8'][@id='id2']", r"\[2\]"), (".//a[@class='footnote-reference'][@href='#foo'][@id='id3']", r"\[3\]"), (".//a[@class='reference internal'][@href='#bar'][@id='id4']", r"\[bar\]"), + (".//a[@class='footnote-reference'][@href='#id9'][@id='id5']", r"\[4\]"), + (".//a[@class='footnote-reference'][@href='#id10'][@id='id6']", r"\[5\]"), (".//a[@class='fn-backref'][@href='#id1']", r"\[1\]"), (".//a[@class='fn-backref'][@href='#id2']", r"\[2\]"), (".//a[@class='fn-backref'][@href='#id3']", r"\[3\]"), (".//a[@class='fn-backref'][@href='#id4']", r"\[bar\]"), + (".//a[@class='fn-backref'][@href='#id5']", r"\[4\]"), + (".//a[@class='fn-backref'][@href='#id6']", r"\[5\]"), ], 'otherext.html': [ (".//h1", "Generated section"), diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 639204ca1..e2350c6a3 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -297,3 +297,24 @@ def test_contentsname_with_language_ja(app, status, warning): print(status.getvalue()) print(warning.getvalue()) assert '\\renewcommand{\\contentsname}{Table of content}' in result + + +@with_app(buildername='latex') +def test_footnote(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'SphinxTests.tex').text(encoding='utf8') + print(result) + print(status.getvalue()) + print(warning.getvalue()) + assert '\\footnote[1]{\nnumbered\n}' in result + assert '\\footnote[2]{\nauto numbered\n}' in result + assert '\\footnote[3]{\nnamed\n}' in result + assert '{\\hyperref[footnote:bar]{\\emph{{[}bar{]}}}}' in result + assert '\\bibitem[bar]{bar}{\\phantomsection\\label{footnote:bar} ' in result + assert '\\bibitem[bar]{bar}{\\phantomsection\\label{footnote:bar} \ncite' in result + assert '\\bibitem[bar]{bar}{\\phantomsection\\label{footnote:bar} \ncite\n}' in result + assert '\\capstart\\caption{Table caption \\protect\\footnotemark[4]}' in result + assert 'name \\protect\\footnotemark[5]' in result + assert ('\\end{threeparttable}\n\n' + '\\footnotetext[4]{\nfootnotes in table caption\n}' + '\\footnotetext[5]{\nfootnotes in table\n}' in result) diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index 33b8f8913..c90139c88 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -137,8 +137,7 @@ def test_extract_messages_without_rawsource(): Check node.rawsource is fall-backed by using node.astext() value. `extract_message` which is used from Sphinx i18n feature drop ``not node.rawsource`` - nodes. - So, all nodes which want to translate must have ``rawsource`` value. + nodes. So, all nodes which want to translate must have ``rawsource`` value. However, sometimes node.rawsource is not set. For example: recommonmark-0.2.0 doesn't set rawsource to `paragraph` node.