Make section titles have stable anchor links

Currently some sections produce anchors like "#id1", which will change
over time as new releases are made. Instead, put in explicit references
so that each version gets an anchor based on the version number, and so
that each heading inside of the version has one based on the section
title and the release version.

Each anchor needs to be prefixed either with the title, if given, or
with another string as some people include reno inside of other
sphinx documentation and these will produce global refererences for the
sphinx build. Also, since it's one global set of anchors, each version
needs to be prefixed by title (or 'relnotes') and the section title
because otherwise in cases like reno's own docs where some versions are
included in the output twice, sphinx will produce conflicts.

Change-Id: Ia6bdaffa6d0ae286542fbb7ae12613be56bdb326
This commit is contained in:
Monty Taylor
2018-04-13 11:15:42 -05:00
parent 9e17ac9c0b
commit 847f13a14a
3 changed files with 82 additions and 5 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Added explicitly calculated anchors to ensure section links are both
unique and stable over time.

View File

@ -25,6 +25,21 @@ def _indent_for_list(text, prefix=' '):
]) + '\n'
def _anchor(version_title, title):
title = title or 'relnotes'
return '.. _{title}_{version_title}:'.format(
title=title,
version_title=version_title)
def _section_anchor(section_title, version_title, title):
# Get the title and remove the trailing :
title = _anchor(version_title, title)[:-1]
return "{title}_{section_title}:".format(
title=title,
section_title=section_title)
def format_report(loader, config, versions_to_include, title=None,
show_source=True):
report = []
@ -44,11 +59,13 @@ def format_report(loader, config, versions_to_include, title=None,
for version in versions_to_include:
if '-' in version:
# This looks like an "unreleased version".
title = config.unreleased_version_title or version
version_title = config.unreleased_version_title or version
else:
title = version
report.append(title)
report.append('=' * len(title))
version_title = version
report.append(_anchor(version_title, title))
report.append('')
report.append(version_title)
report.append('=' * len(version_title))
report.append('')
# Add the preludes.
@ -57,7 +74,11 @@ def format_report(loader, config, versions_to_include, title=None,
notefiles_with_prelude = [(n, sha) for n, sha in notefiles
if prelude_name in file_contents[n]]
if notefiles_with_prelude:
report.append(prelude_name.replace('_', ' ').title())
prelude_title = prelude_name.replace('_', ' ').title()
report.append(_section_anchor(
prelude_title, version_title, title))
report.append('')
report.append(prelude_title)
report.append('-' * len(prelude_name))
report.append('')
@ -76,6 +97,9 @@ def format_report(loader, config, versions_to_include, title=None,
for n in file_contents[fn].get(section_name, [])
]
if notes:
report.append(_section_anchor(
section_title, version_title, title))
report.append('')
report.append(section_title)
report.append('-' * len(section_title))
report.append('')

View File

@ -156,6 +156,7 @@ class TestFormatterCustomSections(TestFormatterBase):
expected = [prelude_pos, api_pos, features_pos]
actual = list(sorted([prelude_pos, features_pos, api_pos]))
self.assertEqual(expected, actual)
self.assertIn('.. _relnotes_1.0.0_API Changes:', result)
class TestFormatterCustomUnreleaseTitle(TestFormatterBase):
@ -182,6 +183,7 @@ class TestFormatterCustomUnreleaseTitle(TestFormatterBase):
)
self.assertIn('Not Released', result)
self.assertNotIn('0.1.0-1', result)
self.assertIn('.. _This is the title_Not Released:', result)
def test_without_title(self):
result = formatter.format_report(
@ -191,3 +193,49 @@ class TestFormatterCustomUnreleaseTitle(TestFormatterBase):
title='This is the title',
)
self.assertIn('0.1.0-1', result)
self.assertIn('.. _This is the title_0.1.0-1:', result)
class TestFormatterAnchors(TestFormatterBase):
note_bodies = {
'note1': {
'prelude': 'This is the prelude.',
},
'note2': {
'issues': [
'This is the first issue.',
'This is the second issue.',
],
},
'note3': {
'features': [
'We added a feature!',
],
'upgrade': None,
},
}
def test_with_title(self):
self.c.override(unreleased_version_title='Not Released')
result = formatter.format_report(
loader=self.ldr,
config=self.c,
versions_to_include=self.versions,
title='This is the title',
)
self.assertIn('.. _This is the title_0.0.0:', result)
self.assertIn('.. _This is the title_0.0.0_Prelude:', result)
self.assertIn('.. _This is the title_1.0.0:', result)
self.assertIn('.. _This is the title_1.0.0_Known Issues:', result)
def test_without_title(self):
result = formatter.format_report(
loader=self.ldr,
config=self.c,
versions_to_include=self.versions,
)
self.assertIn('.. _relnotes_0.0.0:', result)
self.assertIn('.. _relnotes_0.0.0_Prelude:', result)
self.assertIn('.. _relnotes_1.0.0:', result)
self.assertIn('.. _relnotes_1.0.0_Known Issues:', result)