Skip to content

Commit 53b2511

Browse files
committed
DOCSP-5634: attach an id to headings
1 parent c9c74c5 commit 53b2511

File tree

6 files changed

+47
-8
lines changed

6 files changed

+47
-8
lines changed

snooty/gizaparser/nodes.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
import logging
33
import re
4+
import docutils.nodes
45
import networkx
56
from dataclasses import dataclass, field
67
from pathlib import Path, PurePath
@@ -9,6 +10,7 @@
910
Match
1011
from ..flutter import checked
1112
from ..types import Diagnostic, Page, EmbeddedRstParser, SerializableType, ProjectConfig
13+
from .. import util
1214

1315
_T = TypeVar('_T', str, object)
1416
PAT_SUBSTITUTION = re.compile(r'\{\{([\w-]+)\}\}')
@@ -262,8 +264,17 @@ def render_heading(self, parse_rst: EmbeddedRstParser) -> Sequence[SerializableT
262264
heading_text = 'Optional: ' + heading_text
263265

264266
result = parse_rst(heading_text, self.line, True)
267+
268+
# Generate an anchor ID for this heading. It would be useful for this
269+
# to be unique, but it's not possible to do so in a repeatable fashion
270+
# without seeing the whole page, so doing that has to fall to the
271+
# renderer.
272+
heading_id = docutils.nodes.make_id(
273+
''.join(util.ast_get_text(node) for node in result))
274+
265275
return ({
266276
'type': 'heading',
267277
'position': {'start': {'line': self.line}},
268-
'children': result
278+
'children': result,
279+
'id': heading_id
269280
},)

snooty/gizaparser/test_steps.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,34 +42,43 @@ def create_page() -> Tuple[Page, EmbeddedRstParser]:
4242
assert len(pages) == 1
4343
print(repr(ast_to_testing_string(pages[0].ast)))
4444
assert ast_to_testing_string(pages[0].ast) == ''.join((
45-
'<directive name="steps"><directive name="step"><section><heading><text>Import the public ',
46-
'key used by the package management system.</text></heading><paragraph><text>Issue the ',
45+
'<directive name="steps"><directive name="step">',
46+
'<section>',
47+
'<heading id="import-the-public-key-used-by-the-package-management-system">',
48+
'<text>Import the public key used by the package management system.</text>',
49+
'</heading><paragraph><text>Issue the ',
4750
'following command to import the\n</text><reference ',
4851
'refuri="https://www.mongodb.org/static/pgp/server-3.4.asc">',
4952
'<text>MongoDB public GPG Key</text>',
5053
'</reference><target ids="[\'mongodb-public-gpg-key\']" ',
5154
'refuri="https://www.mongodb.org/static/pgp/server-3.4.asc">',
5255
'</target></paragraph></section></directive>',
5356

54-
'<directive name="step"><section><heading><text>Create a </text><literal><text>',
57+
'<directive name="step">',
58+
'<section>',
59+
'<heading id="create-a-etc-apt-sources-list-d-mongodb-org-3-4-list-file-for-mongodb">',
60+
'<text>Create a </text><literal><text>',
5561
'/etc/apt/sources.list.d/mongodb-org-3.4.list</text></literal><text> file for </text>',
5662
'<role name="guilabel" label="',
5763
'{\'type\': \'text\', \'value\': \'MongoDB\', \'position\': {\'start\': {\'line\': 1}}}',
5864
'"></role>',
5965
'<text>.</text></heading>',
60-
'<section><heading><text>Optional: action heading</text></heading>'
66+
'<section><heading id="optional-action-heading">',
67+
'<text>Optional: action heading</text></heading>'
6168
'<paragraph><text>Create the list file using the command appropriate for ',
6269
'your version\nof Debian.</text></paragraph>',
6370
'<paragraph><text>action-content</text></paragraph>',
6471
'<paragraph><text>action-post</text></paragraph>',
6572
'</section></section></directive>',
6673

67-
'<directive name="step"><section><heading><text>Reload local package database.</text>',
74+
'<directive name="step"><section>',
75+
'<heading id="reload-local-package-database"><text>Reload local package database.</text>',
6876
'</heading><paragraph><text>Issue the following command to reload the local package ',
6977
'database:</text></paragraph><code lang="sh" copyable="True">sudo apt-get update\n</code>',
7078
'</section></directive>',
7179

72-
'<directive name="step"><section><heading><text>Install the MongoDB packages.</text>',
80+
'<directive name="step"><section><heading id="install-the-mongodb-packages">',
81+
'<text>Install the MongoDB packages.</text>',
7382
'</heading><paragraph><text>hi</text></paragraph>',
7483
'<paragraph><text>You can install either the latest stable version of MongoDB ',
7584
'or a\nspecific version of MongoDB.</text></paragraph>',

snooty/parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ def dispatch_visit(self, node: docutils.nodes.Node) -> None:
230230
doc['type'] = 'listItem'
231231
elif node_name == 'title':
232232
doc['type'] = 'heading'
233+
# Attach an anchor ID to this section
234+
assert node.parent
235+
doc['id'] = node.parent['ids'][0]
233236
elif node_name == 'reference':
234237
for attr_name in ('refuri', 'refname'):
235238
if attr_name in node:

snooty/test_legacy_guides.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_legacy_guides() -> None:
2121

2222
correct = ''.join((
2323
'<root guide="">',
24-
'<section><heading><text>Sample App</text></heading>',
24+
'<section><heading id="sample-app"><text>Sample App</text></heading>',
2525
'<directive name="author"><text>MongoDB</text></directive>',
2626
'<directive name="category"><text>Getting Started</text></directive>',
2727
'<directive name="languages"><list>',

snooty/util.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,16 @@ def __exit__(self, *args: object) -> None:
167167

168168
def __len__(self) -> int:
169169
return sum(len(w) for w in self.directories.values())
170+
171+
172+
def ast_get_text(ast: Any) -> str:
173+
"""Return pure textual content from a given AST node."""
174+
if ast.get('type') == 'text':
175+
return cast(str, ast['value'])
176+
177+
label = ast.get('label', None)
178+
if label:
179+
return ast_get_text(label)
180+
181+
children = ast.get('children', ())
182+
return ''.join(ast_get_text(child) for child in children)

stubs/docutils/nodes.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ from typing import Any, Optional, Sequence, Iterable, Union
44
from typing_extensions import Protocol
55

66

7+
def make_id(input_value: str) -> str: ...
8+
9+
710
class NodeVisitor(Protocol):
811
def dispatch_visit(self, node: docutils.nodes.Node) -> None: ...
912
def dispatch_departure(self, node: docutils.nodes.Node) -> None: ...

0 commit comments

Comments
 (0)