1"""
2    test_domain_std
3    ~~~~~~~~~~~~~~~
4
5    Tests the std domain
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11from unittest import mock
12
13import pytest
14from docutils import nodes
15from docutils.nodes import definition, definition_list, definition_list_item, term
16from html5lib import HTMLParser
17
18from sphinx import addnodes
19from sphinx.addnodes import (desc, desc_addname, desc_content, desc_name, desc_signature,
20                             glossary, index, pending_xref)
21from sphinx.domains.std import StandardDomain
22from sphinx.testing import restructuredtext
23from sphinx.testing.util import assert_node
24from sphinx.util import docutils
25
26
27def test_process_doc_handle_figure_caption():
28    env = mock.Mock(domaindata={})
29    env.app.registry.enumerable_nodes = {}
30    figure_node = nodes.figure(
31        '',
32        nodes.caption('caption text', 'caption text'),
33    )
34    document = mock.Mock(
35        nametypes={'testname': True},
36        nameids={'testname': 'testid'},
37        ids={'testid': figure_node},
38        citation_refs={},
39    )
40    document.traverse.return_value = []
41
42    domain = StandardDomain(env)
43    if 'testname' in domain.data['labels']:
44        del domain.data['labels']['testname']
45    domain.process_doc(env, 'testdoc', document)
46    assert 'testname' in domain.data['labels']
47    assert domain.data['labels']['testname'] == (
48        'testdoc', 'testid', 'caption text')
49
50
51def test_process_doc_handle_table_title():
52    env = mock.Mock(domaindata={})
53    env.app.registry.enumerable_nodes = {}
54    table_node = nodes.table(
55        '',
56        nodes.title('title text', 'title text'),
57    )
58    document = mock.Mock(
59        nametypes={'testname': True},
60        nameids={'testname': 'testid'},
61        ids={'testid': table_node},
62        citation_refs={},
63    )
64    document.traverse.return_value = []
65
66    domain = StandardDomain(env)
67    if 'testname' in domain.data['labels']:
68        del domain.data['labels']['testname']
69    domain.process_doc(env, 'testdoc', document)
70    assert 'testname' in domain.data['labels']
71    assert domain.data['labels']['testname'] == (
72        'testdoc', 'testid', 'title text')
73
74
75def test_get_full_qualified_name():
76    env = mock.Mock(domaindata={})
77    env.app.registry.enumerable_nodes = {}
78    domain = StandardDomain(env)
79
80    # normal references
81    node = nodes.reference()
82    assert domain.get_full_qualified_name(node) is None
83
84    # simple reference to options
85    node = nodes.reference(reftype='option', reftarget='-l')
86    assert domain.get_full_qualified_name(node) is None
87
88    # options with std:program context
89    kwargs = {'std:program': 'ls'}
90    node = nodes.reference(reftype='option', reftarget='-l', **kwargs)
91    assert domain.get_full_qualified_name(node) == 'ls.-l'
92
93
94def test_cmd_option_with_optional_value(app):
95    text = ".. option:: -j[=N]"
96    doctree = restructuredtext.parse(app, text)
97    assert_node(doctree, (index,
98                          [desc, ([desc_signature, ([desc_name, '-j'],
99                                                    [desc_addname, '[=N]'])],
100                                  [desc_content, ()])]))
101    objects = list(app.env.get_domain("std").get_objects())
102    assert ('-j', '-j', 'cmdoption', 'index', 'cmdoption-j', 1) in objects
103
104
105def test_cmd_option_starting_with_bracket(app):
106    text = ".. option:: [enable=]PATTERN"
107    doctree = restructuredtext.parse(app, text)
108    assert_node(doctree, (index,
109                          [desc, ([desc_signature, ([desc_name, '[enable'],
110                                                    [desc_addname, '=]PATTERN'])],
111                                  [desc_content, ()])]))
112    objects = list(app.env.get_domain("std").get_objects())
113    assert ('[enable', '[enable', 'cmdoption', 'index', 'cmdoption-arg-enable', 1) in objects
114
115
116def test_glossary(app):
117    text = (".. glossary::\n"
118            "\n"
119            "   term1\n"
120            "   TERM2\n"
121            "       description\n"
122            "\n"
123            "   term3 : classifier\n"
124            "       description\n"
125            "       description\n"
126            "\n"
127            "   term4 : class1 : class2\n"
128            "       description\n")
129
130    # doctree
131    doctree = restructuredtext.parse(app, text)
132    assert_node(doctree, (
133        [glossary, definition_list, ([definition_list_item, ([term, ("term1",
134                                                                     index)],
135                                                             [term, ("TERM2",
136                                                                     index)],
137                                                             definition)],
138                                     [definition_list_item, ([term, ("term3",
139                                                                     index)],
140                                                             definition)],
141                                     [definition_list_item, ([term, ("term4",
142                                                                     index)],
143                                                             definition)])],
144    ))
145    assert_node(doctree[0][0][0][0][1],
146                entries=[("single", "term1", "term-term1", "main", None)])
147    assert_node(doctree[0][0][0][1][1],
148                entries=[("single", "TERM2", "term-TERM2", "main", None)])
149    assert_node(doctree[0][0][0][2],
150                [definition, nodes.paragraph, "description"])
151    assert_node(doctree[0][0][1][0][1],
152                entries=[("single", "term3", "term-term3", "main", "classifier")])
153    assert_node(doctree[0][0][1][1],
154                [definition, nodes.paragraph, ("description\n"
155                                               "description")])
156    assert_node(doctree[0][0][2][0][1],
157                entries=[("single", "term4", "term-term4", "main", "class1")])
158    assert_node(doctree[0][0][2][1],
159                [nodes.definition, nodes.paragraph, "description"])
160
161    # index
162    domain = app.env.get_domain("std")
163    objects = list(domain.get_objects())
164    assert ("term1", "term1", "term", "index", "term-term1", -1) in objects
165    assert ("TERM2", "TERM2", "term", "index", "term-TERM2", -1) in objects
166    assert ("term3", "term3", "term", "index", "term-term3", -1) in objects
167    assert ("term4", "term4", "term", "index", "term-term4", -1) in objects
168
169    # term reference (case sensitive)
170    refnode = domain.resolve_xref(app.env, 'index', app.builder, 'term', 'term1',
171                                  pending_xref(), nodes.paragraph())
172    assert_node(refnode, nodes.reference, refid="term-term1")
173
174    # term reference (case insensitive)
175    refnode = domain.resolve_xref(app.env, 'index', app.builder, 'term', 'term2',
176                                  pending_xref(), nodes.paragraph())
177    assert_node(refnode, nodes.reference, refid="term-TERM2")
178
179
180def test_glossary_warning(app, status, warning):
181    # empty line between terms
182    text = (".. glossary::\n"
183            "\n"
184            "   term1\n"
185            "\n"
186            "   term2\n")
187    restructuredtext.parse(app, text, "case1")
188    assert ("case1.rst:4: WARNING: glossary terms must not be separated by empty lines"
189            in warning.getvalue())
190
191    # glossary starts with indented item
192    text = (".. glossary::\n"
193            "\n"
194            "       description\n"
195            "   term\n")
196    restructuredtext.parse(app, text, "case2")
197    assert ("case2.rst:3: WARNING: glossary term must be preceded by empty line"
198            in warning.getvalue())
199
200    # empty line between terms
201    text = (".. glossary::\n"
202            "\n"
203            "   term1\n"
204            "       description\n"
205            "   term2\n")
206    restructuredtext.parse(app, text, "case3")
207    assert ("case3.rst:4: WARNING: glossary term must be preceded by empty line"
208            in warning.getvalue())
209
210    # duplicated terms
211    text = (".. glossary::\n"
212            "\n"
213            "   term-case4\n"
214            "   term-case4\n")
215    restructuredtext.parse(app, text, "case4")
216    assert ("case4.rst:3: WARNING: duplicate term description of term-case4, "
217            "other instance in case4" in warning.getvalue())
218
219
220def test_glossary_comment(app):
221    text = (".. glossary::\n"
222            "\n"
223            "   term1\n"
224            "       description\n"
225            "   .. term2\n"
226            "       description\n"
227            "       description\n")
228    doctree = restructuredtext.parse(app, text)
229    assert_node(doctree, (
230        [glossary, definition_list, definition_list_item, ([term, ("term1",
231                                                                   index)],
232                                                           definition)],
233    ))
234    assert_node(doctree[0][0][0][1],
235                [nodes.definition, nodes.paragraph, "description"])
236
237
238def test_glossary_comment2(app):
239    text = (".. glossary::\n"
240            "\n"
241            "   term1\n"
242            "       description\n"
243            "\n"
244            "   .. term2\n"
245            "   term3\n"
246            "       description\n"
247            "       description\n")
248    doctree = restructuredtext.parse(app, text)
249    assert_node(doctree, (
250        [glossary, definition_list, ([definition_list_item, ([term, ("term1",
251                                                                     index)],
252                                                             definition)],
253                                     [definition_list_item, ([term, ("term3",
254                                                                     index)],
255                                                             definition)])],
256    ))
257    assert_node(doctree[0][0][0][1],
258                [nodes.definition, nodes.paragraph, "description"])
259    assert_node(doctree[0][0][1][1],
260                [nodes.definition, nodes.paragraph, ("description\n"
261                                                     "description")])
262
263
264def test_glossary_sorted(app):
265    text = (".. glossary::\n"
266            "   :sorted:\n"
267            "\n"
268            "   term3\n"
269            "       description\n"
270            "\n"
271            "   term2\n"
272            "   term1\n"
273            "       description\n")
274    doctree = restructuredtext.parse(app, text)
275    assert_node(doctree, (
276        [glossary, definition_list, ([definition_list_item, ([term, ("term2",
277                                                                     index)],
278                                                             [term, ("term1",
279                                                                     index)],
280                                                             definition)],
281                                     [definition_list_item, ([term, ("term3",
282                                                                     index)],
283                                                             definition)])],
284    ))
285    assert_node(doctree[0][0][0][2],
286                [nodes.definition, nodes.paragraph, "description"])
287    assert_node(doctree[0][0][1][1],
288                [nodes.definition, nodes.paragraph, "description"])
289
290
291def test_glossary_alphanumeric(app):
292    text = (".. glossary::\n"
293            "\n"
294            "   1\n"
295            "   /\n")
296    restructuredtext.parse(app, text)
297    objects = list(app.env.get_domain("std").get_objects())
298    assert ("1", "1", "term", "index", "term-1", -1) in objects
299    assert ("/", "/", "term", "index", "term-0", -1) in objects
300
301
302def test_glossary_conflicted_labels(app):
303    text = (".. _term-foo:\n"
304            ".. glossary::\n"
305            "\n"
306            "   foo\n")
307    restructuredtext.parse(app, text)
308    objects = list(app.env.get_domain("std").get_objects())
309    assert ("foo", "foo", "term", "index", "term-0", -1) in objects
310
311
312def test_cmdoption(app):
313    text = (".. program:: ls\n"
314            "\n"
315            ".. option:: -l\n")
316    domain = app.env.get_domain('std')
317    doctree = restructuredtext.parse(app, text)
318    assert_node(doctree, (addnodes.index,
319                          [desc, ([desc_signature, ([desc_name, "-l"],
320                                                    [desc_addname, ()])],
321                                  [desc_content, ()])]))
322    assert_node(doctree[0], addnodes.index,
323                entries=[('pair', 'ls command line option; -l', 'cmdoption-ls-l', '', None)])
324    assert ('ls', '-l') in domain.progoptions
325    assert domain.progoptions[('ls', '-l')] == ('index', 'cmdoption-ls-l')
326
327
328def test_multiple_cmdoptions(app):
329    text = (".. program:: cmd\n"
330            "\n"
331            ".. option:: -o directory, --output directory\n")
332    domain = app.env.get_domain('std')
333    doctree = restructuredtext.parse(app, text)
334    assert_node(doctree, (addnodes.index,
335                          [desc, ([desc_signature, ([desc_name, "-o"],
336                                                    [desc_addname, " directory"],
337                                                    [desc_addname, ", "],
338                                                    [desc_name, "--output"],
339                                                    [desc_addname, " directory"])],
340                                  [desc_content, ()])]))
341    assert_node(doctree[0], addnodes.index,
342                entries=[('pair', 'cmd command line option; -o directory',
343                          'cmdoption-cmd-o', '', None),
344                         ('pair', 'cmd command line option; --output directory',
345                          'cmdoption-cmd-o', '', None)])
346    assert ('cmd', '-o') in domain.progoptions
347    assert ('cmd', '--output') in domain.progoptions
348    assert domain.progoptions[('cmd', '-o')] == ('index', 'cmdoption-cmd-o')
349    assert domain.progoptions[('cmd', '--output')] == ('index', 'cmdoption-cmd-o')
350
351
352@pytest.mark.skipif(docutils.__version_info__ < (0, 13),
353                    reason='docutils-0.13 or above is required')
354@pytest.mark.sphinx(testroot='productionlist')
355def test_productionlist(app, status, warning):
356    app.builder.build_all()
357
358    warnings = warning.getvalue().split("\n")
359    assert len(warnings) == 2
360    assert warnings[-1] == ''
361    assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0]
362
363    with (app.outdir / 'index.html').open('rb') as f:
364        etree = HTMLParser(namespaceHTMLElements=False).parse(f)
365    ul = list(etree.iter('ul'))[1]
366    cases = []
367    for li in list(ul):
368        assert len(list(li)) == 1
369        p = list(li)[0]
370        assert p.tag == 'p'
371        text = str(p.text).strip(' :')
372        assert len(list(p)) == 1
373        a = list(p)[0]
374        assert a.tag == 'a'
375        link = a.get('href')
376        assert len(list(a)) == 1
377        code = list(a)[0]
378        assert code.tag == 'code'
379        assert len(list(code)) == 1
380        span = list(code)[0]
381        assert span.tag == 'span'
382        linkText = span.text.strip()
383        cases.append((text, link, linkText))
384    assert cases == [
385        ('A', 'Bare.html#grammar-token-A', 'A'),
386        ('B', 'Bare.html#grammar-token-B', 'B'),
387        ('P1:A', 'P1.html#grammar-token-P1-A', 'P1:A'),
388        ('P1:B', 'P1.html#grammar-token-P1-B', 'P1:B'),
389        ('P2:A', 'P1.html#grammar-token-P1-A', 'P1:A'),
390        ('P2:B', 'P2.html#grammar-token-P2-B', 'P2:B'),
391        ('Explicit title A, plain', 'Bare.html#grammar-token-A', 'MyTitle'),
392        ('Explicit title A, colon', 'Bare.html#grammar-token-A', 'My:Title'),
393        ('Explicit title P1:A, plain', 'P1.html#grammar-token-P1-A', 'MyTitle'),
394        ('Explicit title P1:A, colon', 'P1.html#grammar-token-P1-A', 'My:Title'),
395        ('Tilde A', 'Bare.html#grammar-token-A', 'A'),
396        ('Tilde P1:A', 'P1.html#grammar-token-P1-A', 'A'),
397        ('Tilde explicit title P1:A', 'P1.html#grammar-token-P1-A', '~MyTitle'),
398        ('Tilde, explicit title P1:A', 'P1.html#grammar-token-P1-A', 'MyTitle'),
399        ('Dup', 'Dup2.html#grammar-token-Dup', 'Dup'),
400        ('FirstLine', 'firstLineRule.html#grammar-token-FirstLine', 'FirstLine'),
401        ('SecondLine', 'firstLineRule.html#grammar-token-SecondLine', 'SecondLine'),
402    ]
403
404    text = (app.outdir / 'LineContinuation.html').read_text()
405    assert "A</strong> ::=  B C D    E F G" in text
406
407
408def test_productionlist2(app):
409    text = (".. productionlist:: P2\n"
410            "   A: `:A` `A`\n"
411            "   B: `P1:B` `~P1:B`\n")
412    doctree = restructuredtext.parse(app, text)
413    refnodes = list(doctree.traverse(pending_xref))
414    assert_node(refnodes[0], pending_xref, reftarget="A")
415    assert_node(refnodes[1], pending_xref, reftarget="P2:A")
416    assert_node(refnodes[2], pending_xref, reftarget="P1:B")
417    assert_node(refnodes[3], pending_xref, reftarget="P1:B")
418    assert_node(refnodes[0], [pending_xref, nodes.literal, "A"])
419    assert_node(refnodes[1], [pending_xref, nodes.literal, "A"])
420    assert_node(refnodes[2], [pending_xref, nodes.literal, "P1:B"])
421    assert_node(refnodes[3], [pending_xref, nodes.literal, "B"])
422
423
424def test_disabled_docref(app):
425    text = (":doc:`index`\n"
426            ":doc:`!index`\n")
427    doctree = restructuredtext.parse(app, text)
428    assert_node(doctree, ([nodes.paragraph, ([pending_xref, nodes.inline, "index"],
429                                             "\n",
430                                             [nodes.inline, "index"])],))
431
432
433def test_labeled_rubric(app):
434    text = (".. _label:\n"
435            ".. rubric:: blah *blah* blah\n")
436    restructuredtext.parse(app, text)
437
438    domain = app.env.get_domain("std")
439    assert 'label' in domain.labels
440    assert domain.labels['label'] == ('index', 'label', 'blah blah blah')
441