diff --git a/pygmt/src/paragraph.py b/pygmt/src/paragraph.py index 94416b7ac7c..e00e6cb6597 100644 --- a/pygmt/src/paragraph.py +++ b/pygmt/src/paragraph.py @@ -3,6 +3,7 @@ """ import io +import re from collections.abc import Sequence from typing import Literal @@ -35,6 +36,8 @@ def paragraph( # noqa: PLR0913 fill: str | None = None, pen: str | None = None, alignment: Literal["left", "center", "right", "justified"] = "left", + tab_width: int = 4, + blankline_between_paragraphs: bool = False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, @@ -74,11 +77,17 @@ def paragraph( # noqa: PLR0913 fill Set color for filling the paragraph box [Default is no fill]. pen - Set the pen used to draw a rectangle around the paragraph [Default is - ``"0.25p,black,solid"``]. + Set the pen for the paragraph box [Default is ``"0.25p,black,solid"``]. alignment Set the alignment of the text. Valid values are ``"left"``, ``"center"``, ``"right"``, and ``"justified"``. + tab_width + Number of spaces used to expand tab characters in ``text`` when typesetting. + Must be a non-negative integer. Use ``0`` to remove tab characters instead of + replacing them with spaces. + blankline_between_paragraphs + If ``True``, use a blank line between paragraphs. [Default is ``False``, i.e., + no blank line between paragraphs.] $verbose $panel $transparency @@ -108,6 +117,12 @@ def paragraph( # noqa: PLR0913 description="value for parameter 'alignment'", choices=_valid_alignments, ) + if tab_width < 0: + raise GMTValueError( + tab_width, + description="value for parameter 'tab_width'", + reason="Must be a non-negative integer.", + ) aliasdict = AliasSystem( F=[ @@ -124,11 +139,22 @@ def paragraph( # noqa: PLR0913 ) aliasdict.merge({"M": True}) - confdict = {} # Prepare the text string that will be passed to an io.StringIO object. - # Multiple paragraphs are separated by a blank line "\n\n". - _textstr: str = "\n\n".join(text) if is_nonstr_iter(text) else str(text) - + # Separator for multiple paragraphs. + # "\n\n": the default separator, which results in no blank line between paragraphs. + # " \n\n": add a blank line between paragraphs. + sep = " \n\n" if blankline_between_paragraphs else "\n\n" + # Convert a single string into a list of paragraphs for consistent handling. + # Split the single string on black lines, allowing for whitespaces in between. + if not is_nonstr_iter(text): + text = re.split(r"\n\s*\n", text) # type: ignore[arg-type] + # Join multiple paragraphs with a blank line. Remove trailing whitespaces and + # newlines in each paragraph, but keep leading whitespaces and tabs for now. + _textstr = sep.join(t.rstrip().replace("\n", "") for t in text) + # Replace two or more consecutive spaces with \040 (octal for space), and replace + # tabs with the appropriate number of \040. + _textstr = re.sub(r" {2,}", lambda m: r"\040" * len(m.group()), _textstr) + _textstr = _textstr.replace("\t", r"\040" * tab_width) if _textstr == "": raise GMTValueError( text, @@ -136,6 +162,7 @@ def paragraph( # noqa: PLR0913 reason="'text' must be a non-empty string or sequence of strings.", ) + confdict = {} # Check the encoding of the text string and convert it to octal if necessary. if (encoding := _check_encoding(_textstr)) != "ascii": _textstr = non_ascii_to_octal(_textstr, encoding=encoding) diff --git a/pygmt/tests/test_paragraph.py b/pygmt/tests/test_paragraph.py index ed25f0d2e58..08015b9a982 100644 --- a/pygmt/tests/test_paragraph.py +++ b/pygmt/tests/test_paragraph.py @@ -29,25 +29,31 @@ def test_paragraph_multiple_paragraphs(inputtype): """ Test typesetting multiple paragraphs. """ + text = [ + " Paragraph 1: Two leading whitespaces. Three inline whitespaces. Two trailing whitespaces. ", + " Paragraph 2: One leading tab results in one indentation (four whitespaces by default).", + " Paragraph 3: Two leading tabs results in two indentation (eight whitespaces by default).", + "Paragraph 4: Multiple inline tabs are converted to multiple spaces. Trailing tabs have not effects. ", + "Paragraph 5: Mixing tabs and spaces. 2T3STST( ).", + "\nParagraph 6: Leading newline is converted to a space. Trailing newlines are converted to spaces.\n\n", + "\n\nParagraph 7: Multiple leading newline are converted to multiple spaces. xxx yyy zzz.", + "Paragraph 8: Newlines insiden a paragraph\n\nare converted to spaces.", + "Paragraph 9: This is the last paragraph.", + ] + if inputtype == "list": - text = [ - "This is the first paragraph. " * 5, - "This is the second paragraph. " * 5, - ] + pass else: - text = ( - "This is the first paragraph. " * 5 - + "\n\n" # Separate the paragraphs with a blank line. - + "This is the second paragraph. " * 5 - ) - + text = "\n\n".join(text) fig = Figure() - fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.basemap(region=[0, 17, 0, 12], projection="x1c", frame=True) fig.paragraph( - x=4, - y=4, + x=1, + y=1, text=text, - parwidth="5c", + font="Courier", + justify="BL", + parwidth="15c", linespacing="12p", ) return fig