Skip to content

Commit 443379f

Browse files
committed
Simplify print style for links with URL text
Currently the site print style marks up links with their URL, for example, given a link with text "Example" that points to example.com, the link will render like Example (example.com) This is overly verbose in cases where the link text is actually the same as the URL, in which case you get duplication, like example.com/foo (https://example.com/foo) This commit attempts to modify this markup logic to avoid this duplication, while keeping existing behavior for all other links. This is implemented by adding a new class on the backend to links with URL link text, and having our frontend CSS hide the URL markup if that class exists. It isn't possible to implement this logic in CSS alone. The backend logic attempts to be smart; for example, a link to https://www.consumerfinance.gov/foo that has link text of "consumerfinance.gov/foo" will still get the simplified styling. See internal DeCo#641 for context.
1 parent ff2540b commit 443379f

3 files changed

Lines changed: 109 additions & 8 deletions

File tree

cfgov/core/tests/test_utils.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
get_body_html,
1414
get_link_tags,
1515
make_safe,
16+
text_matches_href,
1617
)
1718

1819

@@ -29,8 +30,8 @@ def test_make_safe(self):
2930
self.assertEqual(term, make_safe(unsafe_term))
3031

3132
def test_make_safe_max_length(self):
32-
term = """We're the Consumer Financial Protection Bureau (CFPB),
33-
a U.S. government agency that makes sure banks,
33+
term = """We're the Consumer Financial Protection Bureau (CFPB),
34+
a U.S. government agency that makes sure banks,
3435
lenders, and other financial companies treat you fairly."""
3536
self.assertEqual(75, len(make_safe(term)))
3637

@@ -323,3 +324,65 @@ def test_non_cfpb_links(self):
323324
for url in non_cfpb_urls:
324325
with self.subTest(url=url):
325326
self.assertTrue(NON_CFPB_LINKS.match(url))
327+
328+
329+
class TextMatchesHrefTests(SimpleTestCase):
330+
def test_text_matchs_href(self):
331+
self.assertTrue(
332+
text_matches_href("http://example.com", "https://example.com")
333+
)
334+
self.assertTrue(
335+
text_matches_href("https://example.com/", "http://example.com")
336+
)
337+
338+
def test_trailing_slash(self):
339+
self.assertTrue(
340+
text_matches_href("https://example.com", "https://example.com/")
341+
)
342+
self.assertTrue(text_matches_href("/foo/bar/", "/foo/bar"))
343+
self.assertTrue(text_matches_href("/foo/bar", "/foo/bar/"))
344+
345+
def test_relative_urls(self):
346+
self.assertTrue(text_matches_href("/about", "/about/"))
347+
self.assertTrue(text_matches_href("/about/", "/about"))
348+
349+
def test_domains_and_paths(self):
350+
for text, href in [
351+
(
352+
"https://consumerfinance.gov/foo",
353+
"http://www.consumerfinance.gov/foo",
354+
),
355+
(
356+
"consumerfinance.gov/bar/baz",
357+
"https://www.consumerfinance.gov/bar/baz/",
358+
),
359+
(
360+
"consumerfinance.gov/bar/baz",
361+
"/bar/baz/",
362+
),
363+
("https://example.com/about", "https://example.com/about"),
364+
]:
365+
with self.subTest(text=text, href=href):
366+
self.assertTrue(text_matches_href(text, href))
367+
368+
for text, href in [
369+
(
370+
"https://www.consumerfinance.gov/foo",
371+
"https://example.com/foo",
372+
),
373+
("https://example.com/about", "https://example.com/contact"),
374+
]:
375+
with self.subTest(text=text, href=href):
376+
self.assertFalse(text_matches_href(text, href))
377+
378+
def test_whitespace(self):
379+
self.assertTrue(
380+
text_matches_href(
381+
" https://example.com ", "https://example.com/"
382+
)
383+
)
384+
385+
def test_case_sensitivity(self):
386+
self.assertFalse(
387+
text_matches_href("https://Example.com", "https://example.com")
388+
)

cfgov/core/utils.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from urllib.parse import urlparse
23

34
from django.template.defaultfilters import slugify
45

@@ -130,6 +131,37 @@ def get_link_tags(html):
130131
return A_TAG_RE.findall(html)
131132

132133

134+
def text_matches_href(text, href):
135+
"""Compare link text with link URL.
136+
137+
Returns true if link text is functionally equivalent to the link href.
138+
"""
139+
internal_domains = [
140+
"consumerfinance.gov",
141+
"www.consumerfinance.gov",
142+
]
143+
144+
def parse_potential_url(url):
145+
if "://" not in url and "." in url.split("/")[0]:
146+
url = "https://" + url
147+
elif "://" not in url:
148+
url = "https://RELATIVE/" + url.lstrip("/")
149+
150+
return urlparse(url)
151+
152+
def normalize(url):
153+
parsed = parse_potential_url(url)
154+
netloc = parsed.netloc or "RELATIVE"
155+
path = parsed.path.rstrip("/")
156+
157+
if netloc in internal_domains or netloc == "RELATIVE":
158+
netloc = "INTERNAL"
159+
160+
return f"{netloc}{path}"
161+
162+
return normalize((text or "").strip()) == normalize((href or "").strip())
163+
164+
133165
def add_link_markup(tag, request_path):
134166
"""Add necessary markup to the given link and return if modified.
135167
@@ -149,9 +181,13 @@ def add_link_markup(tag, request_path):
149181
if tag is None:
150182
return None
151183

184+
original_tag = str(tag)
152185
href = tag["href"]
153186
class_attrs = tag.attrs.setdefault("class", [])
154187

188+
if text_matches_href(tag.get_text(), href):
189+
tag["class"].append("u-link-text-url")
190+
155191
if request_path is not None:
156192
# Strips the path of the current page from hrefs that are internal page
157193
# anchor links.
@@ -193,8 +229,10 @@ def add_link_markup(tag, request_path):
193229
# be an external link and modified accordingly above.
194230
return str(tag)
195231

232+
# If we're not adding an icon, there's nothing more to do.
196233
if not icon:
197-
return
234+
str_tag = str(tag)
235+
return str_tag if str_tag != original_tag else None
198236

199237
icon_classes = {"class": LINK_ICON_TEXT_CLASSES}
200238
spans = tag.findAll("span", icon_classes)

cfgov/unprocessed/css/print.scss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@
1313
white-space: normal;
1414
}
1515

16-
a[href*="://"]:after
16+
a[href*="://"]:not(.u-link-text-url)::after
1717
{
1818
// Output href in parentheses for links with a protocol already there.
1919
content: ' (' attr(href) ')';
2020
}
2121

22-
a[href^='/']::after {
22+
a[href^='/']:not(.u-link-text-url)::after {
2323
// Output root-relative hrefs in parentheses with our domain prepended.
2424
content: ' (cfpb.gov' attr(href) ')';
2525
}
2626

27-
a[href^='/ask-cfpb/']::after,
28-
a[href*='consumerfinance.gov/ask-cfpb']::after {
27+
a[href^='/ask-cfpb/']:not(.u-link-text-url)::after,
28+
a[href*='consumerfinance.gov/ask-cfpb']:not(.u-link-text-url)::after {
2929
// Use short URL when printing Ask CFPB links.
3030
content: ' (' attr(data-pretty-href) ')';
3131
}
@@ -34,7 +34,7 @@
3434
a.m-info-unit__heading-link::after,
3535
.m-info-unit__content h4 a::after {
3636
// Do not append hrefs to the logo or info unit headings.
37-
content: none;
37+
content: none !important;
3838
}
3939

4040
a[href^='#'] {

0 commit comments

Comments
 (0)