diff --git a/README.md b/README.md index 65975cc..3b171d5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ ANSI String Formatter in Python for CLI Color and Style Formatting ## Introduction -This code was originally written for [greplica](https://pypi.org/project/greplica/), but I felt it deserved its own, separate library. The main goals for this project are: +This code was originally written for [greplica](https://pypi.org/project/greplica/), but I felt it deserved its own, separate library. + +The main goals for this project are: - To provide a simple way to construct an object with ANSI formatting without requiring the developer to know how ANSI formatting works - Provide a way to further format the object using format string - Allow for concatenation of the object @@ -22,6 +24,8 @@ To install, ensure you are connected to the internet and execute: `python3 -m pi ![Examples](https://raw.githubusercontent.com/Tails86/ansi-string/0e2c943f25ccc219256204511fd308652a8075c0/docs/examples.jpg) +Refer to the [test file](https://github.com/Tails86/ansi-string/blob/main/tests/test_ansi_string.py) for more examples on how to use the AnsiString class. + ## Usage To begin, import AnsiString from the ansi_string module. @@ -30,35 +34,105 @@ To begin, import AnsiString from the ansi_string module. from ansi_string import en_tty_ansi, AnsiFormat, AnsiString ``` +### Enabling ANSI Formatting + +Windows requires ANSI formatting to be enabled before it can be used. This can either be set in the environment or by simply calling the following before printing so that ANSI is enabled locally. This returns True when successful. +```py +en_tty_ansi() +``` + +If this also needs to be enabled for stderr, stderr may also be passed to this method. +```py +import sys +en_tty_ansi(sys.stderr) +``` + +This function serves no purpose outside of Windows OS and will simply return True without action in those cases. + ### Construction -The AnsiString class contains the following `__init__` method. The first argument, `s`, is a string to be formatted. The next 0 to N arguments are formatting directives that can be applied to the entire string. These arguments can be in the form of any of the following: +The AnsiString class contains the following `__init__` method. + +```py +class AnsiString: + def __init__(self, s:str='', *setting_or_settings:Union[List[str], str, List[int], int, List[AnsiFormat], AnsiFormat]): ... +``` + +The first argument, `s`, is a string to be formatted. The next 0 to N arguments are formatting directives that can be applied to the entire string. These arguments can be in the form of any of the following: - A string color name for a formatting directive (i.e. any name of the AnsiFormat enum in lower or upper case) - An AnsiFormat directive (ex: `AnsiFormat.BOLD`) -- An rgb() function directive as a string (ex: `"rgb(255, 255, 255)"`) - - rgb() or fg_rgb() to adjust text color - - bg_rgb() to adjust background color - - ul_rgb() to enable underline and set the underline color - - Value given may be either a 24-bit value or 3 x 8-bit values, separated by commas - - Each given value within the parenthesis is treated as a hexadecimal value if it starts with "0x", otherwise it will be treated as a decimal value +- An `rgb(...)` function directive as a string (ex: `"rgb(255, 255, 255)"`) + - `rgb(...)` or `fg_rgb(...)` to adjust text color + - `bg_rgb(...)` to adjust background color + - `ul_rgb(...)` to enable underline and set the underline color + - Value given may be either a 24-bit integer or 3 x 8-bit integers, separated by commas + - Each given value within the parenthesis is treated as hexadecimal if the value starts with "0x", otherwise it will be treated as a decimal value - A string containing known ANSI directives (ex: `"01;31"` for BOLD and FG_RED) - The string will normally be parsed and verified unless the character "[" is the first character of the string - A single ANSI directive as an integer +Examples: ```py -class AnsiString: - def __init__(self, s:str='', *setting_or_settings:Union[List[str], str, List[int], int, List[AnsiFormat], AnsiFormat]): ... +# Set foreground to light_sea_green using string directive +# Set background to chocolate using AnsiFormat directive +# Underline in gray using ul_rgb() directive +# Enable italics using explicit string directive ("3") +# Enable bold using explicit integer directive (1) +s = AnsiString("This is an ANSI string", "light_sea_green", AnsiFormat.BG_CHOCOLATE, "ul_rgb(0x808080)", "3", 1) +print(s) ``` ### Concatenation -- The static method `AnsiString.join()` is provided to join together 0 to many `str` ans `AnsiString` values into a single `AnsiString`. +- The static method `AnsiString.join()` is provided to join together 0 to many `str` and `AnsiString` values into a single `AnsiString`. - The `+` operator may be used to join an `AnsiString` with another `AnsiString` or `str` into a new `AnsiString` - The `+` operator may not be used if the left-hand-side value is a `str` and the right-hand-side values is an `AnsiString` - The `+=` operator may be used to append an `AnsiString` or `str` to an `AnsiString` +Examples: +```py +s = AnsiString.join("This ", AnsiString("string", AnsiFormat.BOLD)) +s += AnsiString(" contains ") + AnsiString("multiple", AnsiFormat.BG_BLUE) +s += AnsiString(" color ", AnsiFormat.FG_ORANGE, AnsiFormat.ITALIC) + "settings accross different ranges" +print(s) +``` + ### Formatting The method `AnsiString.apply_formatting()` is provided to append formatting to a previously constructed `AnsiString`. +Example: +```py +s = AnsiString("This string contains multiple color settings across different ranges") +s.apply_formatting(AnsiFormat.BOLD, 5, 11) +s.apply_formatting(AnsiFormat.BG_BLUE, 21, 29) +s.apply_formatting([AnsiFormat.FG_ORANGE, AnsiFormat.ITALIC], 21, 35) +print(s) +``` + +A format string may be used to format an AnsiString before printing. The format specification string must be in the format `"[string_format][:ansi_format]"` where `string_format` is the standard string format specifier and `ansi_format` contains 0 or more ANSI directives separated by semicolons (;). The ANSI directives may be any of the same string values that can be passed to the `AnsiString` constructor. If no `string_format` is desired, then it can be set to an empty string. +Examples: +```py +ansi_str = AnsiString("This is an ANSI string") +# Right justify with width of 100, formatted bold and red +print("{:>100:bold;red}".format(ansi_str)) +# No justification settings, formatted bold and red +print("{::bold;red}".format(ansi_str)) +# No justification settings, formatted bold and red +print("{::bold;rgb(255, 0, 0)}".format(ansi_str)) +# No justification settings, formatted bold and red +print(f"{ansi_str::bold;red}") +# Format text, background, and underline with custom colors +fg_color = 0x8A2BE2 +bg_colors = [100, 232, 170] +ul_colors = [0xFF, 0x63, 0x47] +print(f"{ansi_str::rgb({fg_color});bg_rgb({bg_colors});ul_rgb({ul_colors})}") +``` -A format string may be used to format an AnsiString before printing (ex: `"{:>10:bold;red}".format(ansi_str)`). The format specification string must be in the format `"[string_format][:ansi_format]"` where `string_format` is the standard string format specifier and `ansi_format` contains 0 or more ANSI directives separated by semicolons (;). The ANSI directives may be any of the same string values that can be passed to the `AnsiString` constructor. If no `string_format` is desired, then it can be set to an empty string (ex: `"{::bold;red}".format(ansi_str)`). This can also be set as a F-String (ex: `f"{ansi_str::bold;red}"`). +The method `AnsiString.format_matching()` is provided to apply formatting to an `AnsiString` based on a match specification. +Example: +```py +s = AnsiString("Here is a strING that I will match formatting") +# This will make the word "formatting" cyan with a pink background +s.format_matching("[A-Za-z]+ing", "cyan", AnsiFormat.BG_PINK, regex=True, match_case=True) +print(s) +``` \ No newline at end of file diff --git a/src/ansi_string/ansi_string.py b/src/ansi_string/ansi_string.py index 051e004..9cda2e8 100644 --- a/src/ansi_string/ansi_string.py +++ b/src/ansi_string/ansi_string.py @@ -27,7 +27,7 @@ import io from typing import Any, Union, List, Dict, Tuple -__version__ = '1.0.0' +__version__ = '1.0.1' PACKAGE_NAME = 'ansi-string' WHITESPACE_CHARS = ' \t\n\r\v\f' @@ -1069,6 +1069,20 @@ def apply_formatting_for_match( e = match_object.end(group) self.apply_formatting(setting_or_settings, s, e) + def format_matching(self, matchspec:str, *format, regex:bool=False, match_case=False): + ''' + Apply formatting for anything matching the matchspec + matchspec: the string to match + format: 0 to many format specifiers + regex: set to True to treat matchspec as a regex string + match_case: set to True to make matching case-sensitive (false by default) + ''' + if not regex: + matchspec = re.escape(matchspec) + + for match in re.finditer(matchspec, self._s, re.IGNORECASE if not match_case else 0): + self.apply_formatting_for_match(format, match) + def clear_formatting(self): ''' Clears all internal formatting. diff --git a/tests/test_ansi_string.py b/tests/test_ansi_string.py index 553b105..c1ee648 100755 --- a/tests/test_ansi_string.py +++ b/tests/test_ansi_string.py @@ -101,10 +101,10 @@ def test_no_format_and_rgb_functions(self): def test_no_format_and_rgb_functions2(self): s = AnsiString('Manually adjust colors of foreground, background, and underline') fg_color = 0x8A2BE2 - bg_colors = [100, 232, 170] + bg_colors = (100, 232, 170) ul_colors = [0xFF, 0x63, 0x47] self.assertEqual( - f'{s::rgb({fg_color});bg_rgb({bg_colors});ul_rgb({ul_colors})}', + f'{s::rgb({fg_color});bg_rgb{bg_colors};ul_rgb({ul_colors})}', '\x1b[38;2;138;43;226;48;2;100;232;170;4;58;2;255;99;71mManually adjust colors of foreground, background, and underline\x1b[m' ) @@ -441,6 +441,38 @@ def test_cat_edge_case3(self): s = s + AnsiString('multiple', AnsiFormat.BG_BLUE) self.assertEqual(str(s), 'This \x1b[38;5;214mstring\x1b[m contains \x1b[44mmultiple\x1b[m') + def test_format_matching(self): + s = AnsiString('Here is a string that I will match formatting') + s.format_matching('InG', 'cyan', AnsiFormat.BG_PINK) + self.assertEqual( + str(s), + 'Here is a str\x1b[36;48;2;255;192;203ming\x1b[m that I will match formatt\x1b[36;48;2;255;192;203ming\x1b[m' + ) + + def test_format_matching_ensure_escape(self): + s = AnsiString('Here is a (string) that I will match formatting') + s.format_matching('(string)', 'cyan', AnsiFormat.BG_PINK) + self.assertEqual( + str(s), + 'Here is a \x1b[36;48;2;255;192;203m(string)\x1b[m that I will match formatting' + ) + + def test_format_matching_case_sensitive(self): + s = AnsiString('Here is a strING that I will match formatting') + s.format_matching('ing', 'cyan', AnsiFormat.BG_PINK, match_case=True) + self.assertEqual( + str(s), + 'Here is a strING that I will match formatt\x1b[36;48;2;255;192;203ming\x1b[m' + ) + + def test_format_matching_regex_match_case(self): + s = AnsiString('Here is a strING that I will match formatting') + s.format_matching('[A-Za-z]+ing', 'cyan', AnsiFormat.BG_PINK, regex=True, match_case=True) + self.assertEqual( + str(s), + 'Here is a strING that I will match \x1b[36;48;2;255;192;203mformatting\x1b[m' + ) + if __name__ == '__main__': unittest.main()