diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index d1f65f98..d790672c 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -7,6 +7,7 @@ import os import re import textwrap +import unicodedata from collections import defaultdict from pathlib import Path @@ -181,6 +182,22 @@ def find_fragments( return content, fragment_files +def get_underline_size(text: str) -> int: + """ + Given `text` determine the underline size needed for the reStructuredText output. + + Particularly helps determine if an extra underline is needed for wide characters like emojis. + """ + underline_size: int = 0 + for char in text: + if unicodedata.east_asian_width(char) in ("W", "F"): + underline_size += 2 + else: + underline_size += 1 + + return underline_size + + def indent(text: str, prefix: str) -> str: """ Adds `prefix` to the beginning of non-empty lines in `text`. @@ -224,6 +241,11 @@ def split_fragments( # it's recorded. content = "" + # Calculate the underline size for the name of the category. + definitions[category]["underline_size"] = get_underline_size( + definitions[category]["name"] + ) + texts = section.setdefault(category, {}) issues = texts.setdefault(content, []) diff --git a/src/towncrier/newsfragments/626.bugfix.rst b/src/towncrier/newsfragments/626.bugfix.rst new file mode 100644 index 00000000..e47790db --- /dev/null +++ b/src/towncrier/newsfragments/626.bugfix.rst @@ -0,0 +1 @@ +Fixed section names not having enough underline characters for emojis. diff --git a/src/towncrier/templates/default.rst b/src/towncrier/templates/default.rst index bee15720..1c19b333 100644 --- a/src/towncrier/templates/default.rst +++ b/src/towncrier/templates/default.rst @@ -16,7 +16,7 @@ {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section]%} {{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} +{{ underline * definitions[category]['underline_size'] }} {% for text, values in sections[section][category].items() %} - {% if text %}{{ text }}{% if values %} ({{ values|join(', ') }}){% endif %}{% else %}{{ values|join(', ') }}{% endif %} diff --git a/src/towncrier/templates/hr-between-versions.rst b/src/towncrier/templates/hr-between-versions.rst index 455e571d..dd484c63 100644 --- a/src/towncrier/templates/hr-between-versions.rst +++ b/src/towncrier/templates/hr-between-versions.rst @@ -16,7 +16,7 @@ {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section]%} {{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} +{{ underline * definitions[category]['underline_size'] }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category].items() %} diff --git a/src/towncrier/templates/single-file-no-bullets.rst b/src/towncrier/templates/single-file-no-bullets.rst index 5b83c515..93a39a2b 100644 --- a/src/towncrier/templates/single-file-no-bullets.rst +++ b/src/towncrier/templates/single-file-no-bullets.rst @@ -16,7 +16,7 @@ {% for category, val in definitions.items() if category in sections[section] %} {{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} +{{ underline * definitions[category]['underline_size'] }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category].items() %} diff --git a/src/towncrier/test/test_builder.py b/src/towncrier/test/test_builder.py index 31108fdf..9aaded60 100644 --- a/src/towncrier/test/test_builder.py +++ b/src/towncrier/test/test_builder.py @@ -5,7 +5,7 @@ from twisted.trial.unittest import TestCase -from .._builder import parse_newfragment_basename, render_fragments +from .._builder import get_underline_size, parse_newfragment_basename, render_fragments class TestParseNewsfragmentBasename(TestCase): @@ -208,3 +208,13 @@ def test_ordering(self): - Added Fish """ ) + + +class TestUtilityFunctions(TestCase): + def test_get_underline_size_ascii(self): + """Determine underline size for normal ASCII strings.""" + assert get_underline_size("bugfixes") == 8 + + def test_get_underline_size_wide_character(self): + """Determine underline size for strings with wide characters.""" + assert get_underline_size("🐛 Bugfixes") == 11