Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add utility function to calculate underline size for category names #645

Open
wants to merge 13 commits into
base: trunk
Choose a base branch
from
22 changes: 22 additions & 0 deletions src/towncrier/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import re
import textwrap
import unicodedata

from collections import defaultdict
from fnmatch import fnmatch
Expand Down Expand Up @@ -203,6 +204,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"):
jacobgulan marked this conversation as resolved.
Show resolved Hide resolved
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`.
Expand Down Expand Up @@ -246,6 +263,10 @@ def split_fragments(
# it's recorded.
content = ""

definitions[category]["underline_size"] = get_underline_size(
jacobgulan marked this conversation as resolved.
Show resolved Hide resolved
definitions[category]["name"]
)

texts = section.setdefault(category, {})

issues = texts.setdefault(content, [])
Expand Down Expand Up @@ -411,6 +432,7 @@ def get_indent(text: str) -> str:
top_underline=top_underline,
get_indent=get_indent, # simplify indentation in the jinja template.
issues_by_category=issues_by_category,
versiondata_name_underline_size=get_underline_size(versiondata.get("name", "")),
)

for line in res.split("\n"):
Expand Down
9 changes: 7 additions & 2 deletions src/towncrier/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@

from towncrier import _git

from ._builder import find_fragments, render_fragments, split_fragments
from ._builder import (
find_fragments,
get_underline_size,
render_fragments,
split_fragments,
)
from ._project import get_project_name, get_version
from ._settings import ConfigError, config_option_help, load_config_from_options
from ._writer import append_to_newsfile
Expand Down Expand Up @@ -234,7 +239,7 @@ def __main(
if is_markdown:
parts = [top_line]
else:
parts = [top_line, config.underlines[0] * len(top_line)]
parts = [top_line, config.underlines[0] * get_underline_size(top_line)]
parts.append(rendered)
content = "\n".join(parts)
else:
Expand Down
2 changes: 2 additions & 0 deletions src/towncrier/newsfragments/626.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
For reStructuredText format, you can now use emojis in category names.
In previous versions, the generated underline was not matching the title width.
4 changes: 2 additions & 2 deletions src/towncrier/templates/default.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% if render_title %}
{% if versiondata.name %}
{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}
{{ top_underline * ((versiondata.version + versiondata.date)|length + 4 + versiondata_name_underline_size)}}
{% else %}
{{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
Expand All @@ -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 %}
Expand Down
4 changes: 2 additions & 2 deletions src/towncrier/templates/hr-between-versions.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% if render_title %}
{% if versiondata.name %}
{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}
{{ top_underline * ((versiondata.version + versiondata.date)|length + 4 + versiondata_name_underline_size)}}
{% else %}
{{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
Expand All @@ -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() %}
Expand Down
4 changes: 2 additions & 2 deletions src/towncrier/templates/single-file-no-bullets.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% if render_title %}
{% if versiondata.name %}
{{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}}
{{ top_underline * ((versiondata.version + versiondata.date)|length + 4 + versiondata_name_underline_size)}}
{% else %}
{{ versiondata.version }} ({{ versiondata.date }})
{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}}
Expand All @@ -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() %}
Expand Down
6 changes: 3 additions & 3 deletions src/towncrier/test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def write(path: str | Path, contents: str, dedent: bool = False) -> None:
p.parent.mkdir(parents=True, exist_ok=True)
if dedent:
contents = textwrap.dedent(contents)
p.write_text(contents)
p.write_text(contents, encoding="utf-8")


def read_pkg_resource(path: str) -> str:
Expand Down Expand Up @@ -65,9 +65,9 @@ def setup_simple_project(
config = "[tool.towncrier]\n" 'package = "foo"\n' + extra_config
else:
config = textwrap.dedent(config)
Path(pyproject_path).write_text(config)
Path(pyproject_path).write_text(config, encoding="utf-8")
Path("foo").mkdir()
Path("foo/__init__.py").write_text('__version__ = "1.2.3"\n')
Path("foo/__init__.py").write_text('__version__ = "1.2.3"\n', encoding="utf-8")

if mkdir_newsfragments:
Path("foo/newsfragments").mkdir()
Expand Down
56 changes: 56 additions & 0 deletions src/towncrier/test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,62 @@ def test_default_start_string_markdown(self, runner):

self.assertEqual(expected_output, output)

@with_project(
config="""
[tool.towncrier]
name = "🍫 FooBar"

[[tool.towncrier.type]]
directory = "bugfix"
name = "🐛 Bugfixes"
showcontent = true
"""
)
def test_underline_size_unicode(self, runner):
"""
The news file can be generated for project names,
and categories that contains emoji in their names.

This is a test for generating the RST section
underline to meet the docutils requirements.
"""
os.mkdir("newsfragments")
with open("newsfragments/321.bugfix", "w") as f:
f.write("Squashed a bug")

result = runner.invoke(
_main,
[
"--version=7.8.9",
"--date=01-01-2001",
"--draft",
],
)

expected_output = dedent(
"""\
Loading template...
Finding news fragments...
Rendering news fragments...
Draft only -- nothing has been written.
What is seen below is what would be written.

🍫 FooBar 7.8.9 (01-01-2001)
============================

🐛 Bugfixes
-----------

- Squashed a bug (#321)



"""
)

self.assertEqual(0, result.exit_code, result.output)
self.assertEqual(expected_output, result.output)

@with_project(
config="""
[tool.towncrier]
Expand Down
10 changes: 9 additions & 1 deletion src/towncrier/test/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -41,6 +41,14 @@ def test_counter_with_extension(self):
("123", "feature", 1),
)

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

def test_ignores_extension(self):
"""File extensions are ignored."""
self.assertEqual(
Expand Down
Loading