Skip to content

Commit e66589f

Browse files
BREAKING: Support typesetting apostrophe (') and backtick (`) (#3105)
Co-authored-by: Yvonne Fröhlich <[email protected]>
1 parent 8e1200f commit e66589f

File tree

3 files changed

+66
-20
lines changed

3 files changed

+66
-20
lines changed

pygmt/helpers/utils.py

+56-14
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,41 @@ def _is_printable_ascii(argstr: str) -> bool:
174174
return all(32 <= ord(c) <= 126 for c in argstr)
175175

176176

177+
def _contains_apostrophe_or_backtick(argstr: str) -> bool:
178+
"""
179+
Check if a string contains apostrophe (') or backtick (`).
180+
181+
For typographical reasons, apostrophe (') and backtick (`) are mapped to left and
182+
right single quotation marks (‘ and ’) in Adobe ISOLatin1+ encoding. To ensure that
183+
what you type is what you get (issue #3476), they need special handling in the
184+
``_check_encoding`` and ``non_ascii_to_octal`` functions. More specifically, a
185+
string containing printable ASCII characters with apostrophe (') and backtick (`)
186+
will not be considered as "ascii" encoding.
187+
188+
Parameters
189+
----------
190+
argstr
191+
The string to be checked.
192+
193+
Returns
194+
-------
195+
``True`` if the string contains apostrophe (') or backtick (`). Otherwise, return
196+
``False``.
197+
198+
Examples
199+
--------
200+
>>> _contains_apostrophe_or_backtick("12AB±β①②")
201+
False
202+
>>> _contains_apostrophe_or_backtick("12AB`")
203+
True
204+
>>> _contains_apostrophe_or_backtick("12AB'")
205+
True
206+
>>> _contains_apostrophe_or_backtick("12AB'`")
207+
True
208+
""" # noqa: RUF002
209+
return "'" in argstr or "`" in argstr
210+
211+
177212
def _check_encoding(argstr: str) -> Encoding:
178213
"""
179214
Check the charset encoding of a string.
@@ -206,8 +241,9 @@ def _check_encoding(argstr: str) -> Encoding:
206241
>>> _check_encoding("123AB中文") # Characters not in any charset encoding
207242
'ISOLatin1+'
208243
"""
209-
# Return "ascii" if the string only contains printable ASCII characters.
210-
if _is_printable_ascii(argstr):
244+
# Return "ascii" if the string only contains printable ASCII characters, excluding
245+
# apostrophe (') and backtick (`).
246+
if _is_printable_ascii(argstr) and not _contains_apostrophe_or_backtick(argstr):
211247
return "ascii"
212248
# Loop through all supported encodings and check if all characters in the string
213249
# are in the charset of the encoding. If all characters are in the charset, return
@@ -402,9 +438,14 @@ def non_ascii_to_octal(argstr: str, encoding: Encoding = "ISOLatin1+") -> str:
402438
'ABC \\261120\\260 DEF @~\\141@~ @%34%\\252@%%'
403439
>>> non_ascii_to_octal("12ABāáâãäåβ①②", encoding="ISO-8859-4")
404440
'12AB\\340\\341\\342\\343\\344\\345@~\\142@~@%34%\\254@%%@%34%\\255@%%'
441+
>>> non_ascii_to_octal("'‘’\"“”")
442+
'\\234\\140\\047"\\216\\217'
405443
""" # noqa: RUF002
406-
# Return the input string if it only contains printable ASCII characters.
407-
if encoding == "ascii" or _is_printable_ascii(argstr):
444+
# Return the input string if it only contains printable ASCII characters, excluding
445+
# apostrophe (') and backtick (`).
446+
if encoding == "ascii" or (
447+
_is_printable_ascii(argstr) and not _contains_apostrophe_or_backtick(argstr)
448+
):
408449
return argstr
409450

410451
# Dictionary mapping non-ASCII characters to octal codes
@@ -420,6 +461,11 @@ def non_ascii_to_octal(argstr: str, encoding: Encoding = "ISOLatin1+") -> str:
420461

421462
# Remove any printable characters.
422463
mapping = {k: v for k, v in mapping.items() if k not in string.printable}
464+
465+
if encoding == "ISOLatin1+":
466+
# Map apostrophe (') and backtick (`) to correct octal codes.
467+
# See _contains_apostrophe_or_backtick() for explanations.
468+
mapping.update({"'": "\\234", "`": "\\221"})
423469
return argstr.translate(str.maketrans(mapping))
424470

425471

@@ -465,16 +511,12 @@ def build_arg_list( # noqa: PLR0912
465511
['-A', '-D0', '-E200', '-F', '-G1/2/3/4']
466512
>>> build_arg_list(dict(A="1/2/3/4", B=["xaf", "yaf", "WSen"], C=("1p", "2p")))
467513
['-A1/2/3/4', '-BWSen', '-Bxaf', '-Byaf', '-C1p', '-C2p']
468-
>>> print(
469-
... build_arg_list(
470-
... dict(
471-
... B=["af", "WSne+tBlank Space"],
472-
... F='+t"Empty Spaces"',
473-
... l="'Void Space'",
474-
... )
475-
... )
476-
... )
477-
['-BWSne+tBlank Space', '-Baf', '-F+t"Empty Spaces"', "-l'Void Space'"]
514+
>>> build_arg_list(dict(B=["af", "WSne+tBlank Space"]))
515+
['-BWSne+tBlank Space', '-Baf']
516+
>>> build_arg_list(dict(F='+t"Empty Spaces"'))
517+
['-F+t"Empty Spaces"']
518+
>>> build_arg_list(dict(l="'Void Space'"))
519+
['-l\\234Void Space\\234', '--PS_CHAR_ENCODING=ISOLatin1+']
478520
>>> print(
479521
... build_arg_list(
480522
... dict(A="0", B=True, C="rainbow"),
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
outs:
2-
- md5: 90d08c5a11c606abed51b84eafcdea04
3-
size: 1662
2+
- md5: f3ddc9b50f3da1facdbcd32261db3bd6
3+
size: 2965
44
hash: md5
55
path: test_text_quotation_marks.png

pygmt/tests/test_text.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -466,13 +466,17 @@ def test_text_nonascii(encoding):
466466
@pytest.mark.mpl_image_compare
467467
def test_text_quotation_marks():
468468
"""
469-
Test typesetting quotation marks.
469+
Test typesetting backtick, apostrophe, and single and double quotation marks.
470470
471-
See https://github.com/GenericMappingTools/pygmt/issues/3104.
471+
See https://github.com/GenericMappingTools/pygmt/issues/3104 and
472+
https://github.com/GenericMappingTools/pygmt/issues/3476.
472473
"""
474+
quotations = "` ' ‘ ’ \" “ ”" # noqa: RUF001
473475
fig = Figure()
474-
fig.basemap(projection="X4c/2c", region=[0, 4, 0, 2], frame=0)
475-
fig.text(x=2, y=1, text='\\234 ‘ ’ " “ ”', font="20p") # noqa: RUF001
476+
fig.basemap(
477+
projection="X4c/2c", region=[0, 4, 0, 2], frame=["S", f"x+l{quotations}"]
478+
)
479+
fig.text(x=2, y=1, text=quotations, font="20p")
476480
return fig
477481

478482

0 commit comments

Comments
 (0)