Skip to content

Commit

Permalink
Added format_matching(); updated documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Tails86 committed Apr 21, 2024
1 parent 5ca14aa commit 5c99f05
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 15 deletions.
98 changes: 86 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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)
```
16 changes: 15 additions & 1 deletion src/ansi_string/ansi_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down
36 changes: 34 additions & 2 deletions tests/test_ansi_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)

Expand Down Expand Up @@ -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()

0 comments on commit 5c99f05

Please sign in to comment.