33"""
44
55import io
6+ import re
67from collections .abc import Sequence
78from typing import Literal
89
@@ -35,6 +36,8 @@ def paragraph( # noqa: PLR0913
3536 fill : str | None = None ,
3637 pen : str | None = None ,
3738 alignment : Literal ["left" , "center" , "right" , "justified" ] = "left" ,
39+ tab_width : int = 4 ,
40+ blankline_between_paragraphs : bool = False ,
3841 verbose : Literal ["quiet" , "error" , "warning" , "timing" , "info" , "compat" , "debug" ]
3942 | bool = False ,
4043 panel : int | Sequence [int ] | bool = False ,
@@ -79,6 +82,13 @@ def paragraph( # noqa: PLR0913
7982 alignment
8083 Set the alignment of the text. Valid values are ``"left"``, ``"center"``,
8184 ``"right"``, and ``"justified"``.
85+ tab_width
86+ Number of spaces used to expand tab characters in ``text`` when typesetting.
87+ Must be a non-negative integer. Use ``0`` to remove tab characters instead of
88+ replacing them with spaces.
89+ blankline_between_paragraphs
90+ If ``True``, use a blank line between paragraphs. [Default is ``False``, i.e.,
91+ no blank line between paragraphs.]
8292 $verbose
8393 $panel
8494 $transparency
@@ -108,6 +118,12 @@ def paragraph( # noqa: PLR0913
108118 description = "value for parameter 'alignment'" ,
109119 choices = _valid_alignments ,
110120 )
121+ if tab_width < 0 :
122+ raise GMTValueError (
123+ tab_width ,
124+ description = "value for parameter 'tab_width'" ,
125+ reason = "Must be a non-negative integer." ,
126+ )
111127
112128 aliasdict = AliasSystem (
113129 F = [
@@ -124,18 +140,40 @@ def paragraph( # noqa: PLR0913
124140 )
125141 aliasdict .merge ({"M" : True })
126142
127- confdict = {}
128143 # Prepare the text string that will be passed to an io.StringIO object.
129- # Multiple paragraphs are separated by a blank line "\n\n".
130- _textstr : str = "\n \n " .join (text ) if is_nonstr_iter (text ) else str (text )
131-
144+ #
145+ # The GMT's behavior:
146+ # - Leading and trailing spaces are ignored.
147+ # - Multiple spaces inside a paragraph are combined into one single space.
148+ # - Leading tabs are combined into one tab that results in a 4-space indentation.
149+ # - Trailing tabs are ignored.
150+ # - Multiple tabs inside a paragraph are converted to multiple spaces.
151+ # - Mixing tabs and spaces inside a paragraph has a complicated behavior.
152+ # - Newline characters are always converted into spaces.
153+
154+ # Separator for multiple paragraphs.
155+ # "\n\n": the default separator, which results in no blank line between paragraphs.
156+ # " \n\n": add a blank line between paragraphs.
157+ sep = " \n \n " if blankline_between_paragraphs else "\n \n "
158+ # Convert a single string into a list of paragraphs for consistent handling.
159+ # Split the single string on black lines, allowing for whitespaces in between.
160+ if not is_nonstr_iter (text ):
161+ text = re .split (r"\n\s*\n" , text )
162+ # Join multiple paragraphs with a blank line. Remove trailing whitespaces and
163+ # newlines in each paragraph, but keep leading whitespaces and tabs for now.
164+ _textstr = sep .join (t .rstrip ().replace ("\n " , "" ) for t in text )
165+ # Replace two or more consecutive spaces with \040 (octal for space), and replace
166+ # tabs with the appropriate number of \040.
167+ _textstr = re .sub (r" {2,}" , lambda m : r"\040" * len (m .group ()), _textstr )
168+ _textstr = _textstr .replace ("\t " , r"\040" * tab_width )
132169 if _textstr == "" :
133170 raise GMTValueError (
134171 text ,
135172 description = "text" ,
136173 reason = "'text' must be a non-empty string or sequence of strings." ,
137174 )
138175
176+ confdict = {}
139177 # Check the encoding of the text string and convert it to octal if necessary.
140178 if (encoding := _check_encoding (_textstr )) != "ascii" :
141179 _textstr = non_ascii_to_octal (_textstr , encoding = encoding )
0 commit comments