Skip to content

Commit 102b1a4

Browse files
authored
DOP 888: Add glossary support (#152)
1 parent a286d47 commit 102b1a4

File tree

5 files changed

+176
-41
lines changed

5 files changed

+176
-41
lines changed

snooty/diagnostics.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,21 @@ def __init__(
204204
self.name = name
205205

206206

207+
class MalformedGlossary(Diagnostic):
208+
severity = Diagnostic.Level.error
209+
210+
def __init__(
211+
self,
212+
start: Union[int, Tuple[int, int]],
213+
end: Union[None, int, Tuple[int, int]] = None,
214+
) -> None:
215+
super().__init__(
216+
f"Malformed glossary: glossary must contain only a definition list",
217+
start,
218+
end,
219+
)
220+
221+
207222
class FailedToInheritRef(Diagnostic):
208223
severity = Diagnostic.Level.error
209224

snooty/n.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,12 @@ class TargetIdentifier(InlineParent):
302302
ids: List[str]
303303

304304

305+
@dataclass
306+
class InlineTarget(InlineParent, Target):
307+
__slots__ = ()
308+
type = "inline_target"
309+
310+
305311
@dataclass
306312
class Reference(InlineParent):
307313
__slots__ = ("refuri", "refname")

snooty/parser.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from functools import partial
1414
from pathlib import Path, PurePath
1515
from typing import Any, Dict, MutableSequence, Tuple, Optional, Set, List, Iterable
16+
from docutils.nodes import make_id
1617
from typing_extensions import Protocol
1718
import docutils.utils
1819
import watchdog.events
@@ -48,6 +49,7 @@
4849
InvalidURL,
4950
InvalidLiteralInclude,
5051
InvalidTableStructure,
52+
MalformedGlossary,
5153
)
5254

5355
# XXX: Work around to get snooty working with Python 3.8 until we can fix
@@ -60,7 +62,7 @@
6062

6163
@dataclass
6264
class _DefinitionListTerm(n.InlineParent):
63-
"""A private node used for internal book-keeping that should not be exported to the AST."""
65+
"""A vate node used for internal book-keeping that should not be exported to the AST."""
6466

6567
__slots__ = ()
6668
type = "definition_list_term"
@@ -427,6 +429,37 @@ def dispatch_departure(self, node: docutils.nodes.Node) -> None:
427429
repr(popped),
428430
)
429431

432+
if (
433+
isinstance(popped, n.Directive)
434+
and f"{popped.domain}:{popped.name}" == ":glossary"
435+
):
436+
437+
definition_list = next(popped.get_child_of_type(n.DefinitionList), None)
438+
439+
if definition_list is None:
440+
return
441+
442+
if len(popped.children) != 1:
443+
self.diagnostics.append(MalformedGlossary(util.get_line(node)))
444+
return
445+
446+
if popped.options.get("sorted", False):
447+
definition_list.children = sorted(
448+
definition_list.children,
449+
key=lambda DefinitionListItem: "".join(
450+
term.get_text() for term in DefinitionListItem.term
451+
),
452+
)
453+
454+
for item in definition_list.get_child_of_type(n.DefinitionListItem):
455+
term_text = "".join(term.get_text() for term in item.term)
456+
term_identifier = make_id(term_text)
457+
identifier = n.TargetIdentifier(item.start, [], [term_identifier])
458+
identifier.children = item.term[:]
459+
target = n.InlineTarget(item.start, [], "std", "term", None)
460+
target.children = [identifier]
461+
item.term.append(target)
462+
430463
def handle_directive(
431464
self, node: docutils.nodes.Node, line: int
432465
) -> Optional[n.Node]:

snooty/test_parser.py

Lines changed: 120 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,16 @@
88
DocUtilsParseError,
99
CannotOpenFile,
1010
InvalidLiteralInclude,
11+
MalformedGlossary,
1112
)
12-
from .parser import parse_rst, JSONVisitor, InlineJSONVisitor
13+
from .parser import parse_rst, JSONVisitor
1314

1415
ROOT_PATH = Path("test_data")
1516

1617
# Some of the tests in this file may seem a little weird around refs: the raw parser output
1718
# does NOT include postprocessing artifacts such as nonlocal link titles and intersphinx lookups.
1819

1920

20-
def test_inline_parser_references() -> None:
21-
tabs_path = ROOT_PATH.joinpath(Path("test.rst"))
22-
project_config = ProjectConfig(ROOT_PATH, "")
23-
parser = rstparser.Parser(project_config, InlineJSONVisitor)
24-
25-
# Test a simple code-block
26-
page, diagnostics = parse_rst(
27-
parser,
28-
tabs_path,
29-
"""
30-
`package management system <https://en.wikipedia.org/wiki/Package_manager>`_
31-
""",
32-
)
33-
page.finish(diagnostics)
34-
assert diagnostics == []
35-
print(ast_to_testing_string(page.ast))
36-
check_ast_testing_string(
37-
page.ast,
38-
"""
39-
<root>
40-
<reference refuri="https://en.wikipedia.org/wiki/Package_manager">
41-
<text>package management system</text>
42-
</reference>
43-
</root>""",
44-
)
45-
46-
4721
def test_tabs() -> None:
4822
tabs_path = ROOT_PATH.joinpath(Path("test_tabs.rst"))
4923
project_config = ProjectConfig(ROOT_PATH, "")
@@ -639,6 +613,123 @@ def test_accidental_indentation() -> None:
639613
assert len(diagnostics) == 1
640614

641615

616+
def test_glossary_node() -> None:
617+
path = ROOT_PATH.joinpath(Path("test.rst"))
618+
project_config = ProjectConfig(ROOT_PATH, "", source="./")
619+
parser = rstparser.Parser(project_config, JSONVisitor)
620+
621+
page, diagnostics = parse_rst(
622+
parser,
623+
path,
624+
"""
625+
.. glossary::
626+
:sorted:
627+
628+
_id
629+
foofoobarbar
630+
631+
index
632+
foofoofoofoobarbarbarbar
633+
634+
$cmd
635+
foobar
636+
637+
aggregate
638+
foofoofoobarbarbar
639+
640+
641+
""",
642+
)
643+
page.finish(diagnostics)
644+
645+
check_ast_testing_string(
646+
page.ast,
647+
"""
648+
<root>
649+
<directive name="glossary" sorted="True">
650+
<definitionList>
651+
<definitionListItem>
652+
<term>
653+
<text>$cmd</text>
654+
<inline_target domain="std" name="term">
655+
<target_identifier ids="['cmd']">
656+
<text>$cmd</text></target_identifier>
657+
</inline_target>
658+
</term>
659+
<paragraph><text>foobar</text></paragraph>
660+
</definitionListItem>
661+
662+
<definitionListItem>
663+
<term>
664+
<text>_id</text>
665+
<inline_target domain="std" name="term">
666+
<target_identifier ids="['id']">
667+
<text>_id</text>
668+
</target_identifier>
669+
</inline_target>
670+
</term>
671+
<paragraph><text>foofoobarbar</text></paragraph>
672+
</definitionListItem>
673+
674+
<definitionListItem>
675+
<term>
676+
<text>aggregate</text>
677+
<inline_target domain="std" name="term">
678+
<target_identifier ids="['aggregate']">
679+
<text>aggregate</text></target_identifier>
680+
</inline_target>
681+
</term>
682+
<paragraph><text>foofoofoobarbarbar</text></paragraph>
683+
</definitionListItem>
684+
685+
<definitionListItem>
686+
<term>
687+
<text>index</text>
688+
<inline_target domain="std" name="term">
689+
<target_identifier ids="['index']">
690+
<text>index</text></target_identifier>
691+
</inline_target>
692+
</term>
693+
<paragraph><text>foofoofoofoobarbarbarbar</text></paragraph>
694+
</definitionListItem>
695+
</definitionList>
696+
</directive>
697+
</root>
698+
""",
699+
)
700+
701+
page, diagnostics = parse_rst(
702+
parser,
703+
path,
704+
"""
705+
.. glossary::
706+
:sorted:
707+
708+
""",
709+
)
710+
711+
page.finish(diagnostics)
712+
713+
assert len(diagnostics) == 0
714+
715+
page, diagnostics = parse_rst(
716+
parser,
717+
path,
718+
"""
719+
.. glossary::
720+
721+
_id
722+
foo
723+
724+
This is a paragraph, not a definition list item.
725+
726+
""",
727+
)
728+
page.finish(diagnostics)
729+
assert len(diagnostics) == 1
730+
assert isinstance(diagnostics[0], MalformedGlossary)
731+
732+
642733
def test_cond() -> None:
643734
path = ROOT_PATH.joinpath(Path("test.rst"))
644735
project_config = ProjectConfig(ROOT_PATH, "", source="./")
@@ -1110,7 +1201,7 @@ def test_callable_target() -> None:
11101201
)
11111202

11121203

1113-
def test_no_hyperlink_references() -> None:
1204+
def test_no_weird_targets() -> None:
11141205
path = ROOT_PATH.joinpath(Path("test.rst"))
11151206
project_config = ProjectConfig(ROOT_PATH, "", source="./")
11161207
parser = rstparser.Parser(project_config, JSONVisitor)
@@ -1132,16 +1223,6 @@ def test_no_hyperlink_references() -> None:
11321223
diagnostics[1], InvalidURL
11331224
)
11341225

1135-
check_ast_testing_string(
1136-
page.ast,
1137-
"""
1138-
<root>
1139-
<paragraph><reference refname="ios-universal-links"><text>universal link</text></reference></paragraph>
1140-
<paragraph><reference refname="ios-universal-links"><text>universal link</text></reference></paragraph>
1141-
</root>
1142-
""",
1143-
)
1144-
11451226

11461227
def test_dates() -> None:
11471228
path = ROOT_PATH.joinpath(Path("test.rst"))

snooty/util_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,4 @@ def check_ast_testing_string(ast: Any, testing_string: str) -> None:
107107
"""Ensure that an AST node matches the given testing XML string, using ast_to_testing_string()."""
108108
correct_tree = ET.fromstring(testing_string)
109109
evaluating_tree = ET.fromstring(ast_to_testing_string(ast))
110-
assert_etree_equals(correct_tree, evaluating_tree)
110+
assert_etree_equals(evaluating_tree, correct_tree)

0 commit comments

Comments
 (0)