xulbux 1.6.7__py3-none-any.whl → 1.6.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xulbux might be problematic. Click here for more details.

xulbux/__init__.py CHANGED
@@ -1,45 +1,13 @@
1
- """
2
- >>> import xulbux as xx
3
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4
- • CUSTOM TYPES:
5
- • rgba(int,int,int,float)
6
- • hsla(int,int,int,float)
7
- • hexa(str)
8
- • PATH OPERATIONS xx.Path
9
- • FILE OPERATIONS xx.File
10
- • JSON FILE OPERATIONS xx.Json
11
- • SYSTEM ACTIONS xx.System
12
- • MANAGE THE ENV PATH VAR xx.EnvPath
13
- • CONSOLE LOG AND ACTIONS xx.Console
14
- • EASY PRETTY PRINTING xx.FormatCodes
15
- • WORKING WITH COLORS xx.Color
16
- • DATA OPERATIONS xx.Data
17
- • STRING OPERATIONS xx.String
18
- • CODE STRING OPERATIONS xx.Code
19
- • REGEX PATTERN TEMPLATES xx.Regex
20
- """
21
-
22
- __version__ = "1.6.7"
1
+ __version__ = "1.6.9"
23
2
  __author__ = "XulbuX"
24
3
  __email__ = "xulbux.real@gmail.com"
25
4
  __license__ = "MIT"
26
- __copyright__ = "Copyright (c) 2024 XulbuX"
5
+ __copyright__ = "Copyright (c) 2025 XulbuX"
27
6
  __url__ = "https://github.com/XulbuX/PythonLibraryXulbuX"
28
7
  __description__ = "A library which includes a lot of really helpful functions."
29
8
  __all__ = [
30
- "_consts_",
31
- "xx_console",
32
- "xx_code",
33
- "xx_color",
34
- "xx_data",
35
- "xx_env_path",
36
- "xx_file",
37
- "xx_format_codes",
38
- "xx_json",
39
- "xx_path",
40
- "xx_regex",
41
- "xx_string",
42
- "xx_system",
9
+ "_consts_", "xx_code", "xx_color", "xx_console", "xx_data", "xx_env_path", "xx_file", "xx_format_codes", "xx_json",
10
+ "xx_path", "xx_regex", "xx_string", "xx_system"
43
11
  ]
44
12
 
45
13
  from ._consts_ import *
xulbux/_cli_.py CHANGED
@@ -1,5 +1,5 @@
1
- from . import __version__
2
1
  from ._consts_ import COLOR
2
+ from . import __version__
3
3
  from .xx_format_codes import FormatCodes
4
4
  from .xx_console import Console
5
5
 
@@ -33,17 +33,17 @@ def help_command():
33
33
  [dim](•) [{color['class']}]rgba[{color['punctuators']}]/([i|{color['types']}]int[_|{color['punctuators']}],[i|{color['types']}]int[_|{color['punctuators']}],[i|{color['types']}]int[_|{color['punctuators']}],[i|{color['types']}]float[_|{color['punctuators']}])[*]
34
34
  [dim](•) [{color['class']}]hsla[{color['punctuators']}]/([i|{color['types']}]int[_|{color['punctuators']}],[i|{color['types']}]int[_|{color['punctuators']}],[i|{color['types']}]int[_|{color['punctuators']}],[i|{color['types']}]float[_|{color['punctuators']}])[*]
35
35
  [dim](•) [{color['class']}]hexa[{color['punctuators']}]/([i|{color['types']}]str[_|{color['punctuators']}]|[i|{color['types']}]int[_|{color['punctuators']}])[*]
36
+ [dim](•) CODE STRING OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Code[*]
37
+ [dim](•) WORKING WITH COLORS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Color[*]
38
+ [dim](•) CONSOLE LOG AND ACTIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Console[*]
36
39
  [dim](•) PATH OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Path[*]
37
40
  [dim](•) FILE OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]File[*]
38
41
  [dim](•) JSON FILE OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Json[*]
39
42
  [dim](•) SYSTEM ACTIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]System[*]
40
43
  [dim](•) MANAGE THE ENV PATH VAR [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]EnvPath[*]
41
- [dim](•) CONSOLE LOG AND ACTIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Console[*]
42
44
  [dim](•) EASY PRETTY PRINTING [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]FormatCodes[*]
43
- [dim](•) WORKING WITH COLORS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Color[*]
44
45
  [dim](•) DATA OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Data[*]
45
46
  [dim](•) STRING OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]String[*]
46
- [dim](•) CODE STRING OPERATIONS [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Code[*]
47
47
  [dim](•) REGEX PATTERN TEMPLATES [{color['lib']}]xx[{color['punctuators']}].[{color['class']}]Regex[*]
48
48
  [_]
49
49
  [dim](Press any key to exit...)
xulbux/xx_code.py CHANGED
@@ -55,46 +55,61 @@ class Code:
55
55
  @staticmethod
56
56
  def is_js(code: str, funcs: list = ["__", "$t", "$lang"]) -> bool:
57
57
  """Will check if the code is very likely to be JavaScript."""
58
- funcs = "|".join(funcs)
59
- js_pattern = _rx.compile(
60
- Regex.outside_strings(
61
- r"""^(?:
62
- (\$[\w_]+)\s* # JQUERY-STYLE VARIABLES
63
- |(\$[\w_]+\s*\() # JQUERY-STYLE FUNCTION CALLS
64
- |((""" + funcs + r")" + Regex.brackets("()") + r"""\s*) # PREDEFINED FUNCTION CALLS
65
- |(\bfunction\s*\() # FUNCTION DECLARATIONS
66
- |(\b(var|let|const)\s+[\w_]+\s*=) # VARIABLE DECLARATIONS
67
- |(\b(if|for|while|switch)\s*\() # CONTROL STRUCTURES
68
- |(\b(return|throw)\s+) # RETURN OR THROW STATEMENTS
69
- |(\bnew\s+[\w_]+\() # OBJECT INSTANTIATION
70
- |(\b[\w_]+\s*=>\s*{) # ARROW FUNCTIONS
71
- |(\b(true|false|null|undefined)\b) # JAVASCRIPT LITERALS
72
- |(\b(document|window|console)\.) # BROWSER OBJECTS
73
- |(\b[\w_]+\.(forEach|map|filter|reduce)\() # ARRAY METHODS
74
- |(/[^/\n\r]*?/[gimsuy]*) # REGULAR EXPRESSIONS
75
- |(===|!==|\+\+|--|\|\||&&) # JAVASCRIPT-SPECIFIC OPERATORS
76
- |(\bclass\s+[\w_]+) # CLASS DECLARATIONS
77
- |(\bimport\s+.*?from\s+) # IMPORT STATEMENTS
78
- |(\bexport\s+(default\s+)?) # EXPORT STATEMENTS
79
- |(\basync\s+function) # ASYNC FUNCTIONS
80
- |(\bawait\s+) # AWAIT KEYWORD
81
- |(\btry\s*{) # TRY-CATCH BLOCKS
82
- |(\bcatch\s*\()
83
- |(\bfinally\s*{)
84
- |(\byield\s+) # GENERATOR FUNCTIONS
85
- |(\[.*?\]\s*=) # DESTRUCTURING ASSIGNMENT
86
- |(\.\.\.) # SPREAD OPERATOR
87
- |(==|!=|>=|<=|>|<) # COMPARISON OPERATORS
88
- |(\+=|-=|\*=|/=|%=|\*\*=) # COMPOUND ASSIGNMENT OPERATORS
89
- |(\+|-|\*|/|%|\*\*) # ARITHMETIC OPERATORS
90
- |(&|\||\^|~|<<|>>|>>>) # BITWISE OPERATORS
91
- |(\?|:) # TERNARY OPERATOR
92
- |(\bin\b) # IN OPERATOR
93
- |(\binstanceof\b) # INSTANCEOF OPERATOR
94
- |(\bdelete\b) # DELETE OPERATOR
95
- |(\btypeof\b) # TYPEOF OPERATOR
96
- |(\bvoid\b) # VOID OPERATOR
97
- )[\s\S]*$"""
98
- ), _rx.VERBOSE | _rx.IGNORECASE
99
- )
100
- return bool(js_pattern.fullmatch(code))
58
+ if not code or len(code.strip()) < 3:
59
+ return False
60
+ for func in funcs:
61
+ if _rx.match(r"^[\s\n]*" + _rx.escape(func) + r"\([^\)]*\)[\s\n]*$", code):
62
+ return True
63
+ direct_js_patterns = [
64
+ r"^[\s\n]*\$\(['\"][^'\"]+['\"]\)\.[\w]+\([^\)]*\);?[\s\n]*$", # jQuery calls
65
+ r"^[\s\n]*\$\.[a-zA-Z]\w*\([^\)]*\);?[\s\n]*$", # $.ajax(), etc.
66
+ r"^[\s\n]*\(\s*function\s*\(\)\s*\{.*\}\s*\)\(\);?[\s\n]*$", # IIFE
67
+ r"^[\s\n]*document\.[a-zA-Z]\w*\([^\)]*\);?[\s\n]*$", # document.getElementById()
68
+ r"^[\s\n]*window\.[a-zA-Z]\w*\([^\)]*\);?[\s\n]*$", # window.alert()
69
+ r"^[\s\n]*console\.[a-zA-Z]\w*\([^\)]*\);?[\s\n]*$", # console.log()
70
+ ]
71
+ for pattern in direct_js_patterns:
72
+ if _rx.match(pattern, code):
73
+ return True
74
+ arrow_function_patterns = [
75
+ r"^[\s\n]*\b[\w_]+\s*=\s*\([^\)]*\)\s*=>\s*[^;{]*[;]?[\s\n]*$", # const x = (y) => y*2;
76
+ r"^[\s\n]*\b[\w_]+\s*=\s*[\w_]+\s*=>\s*[^;{]*[;]?[\s\n]*$", # const x = y => y*2;
77
+ r"^[\s\n]*\(\s*[\w_,\s]+\s*\)\s*=>\s*[^;{]*[;]?[\s\n]*$", # (x) => x*2
78
+ r"^[\s\n]*[\w_]+\s*=>\s*[^;{]*[;]?[\s\n]*$", # x => x*2
79
+ ]
80
+ for pattern in arrow_function_patterns:
81
+ if _rx.match(pattern, code):
82
+ return True
83
+ funcs_pattern = r"(" + "|".join(_rx.escape(f) for f in funcs) + r")" + Regex.brackets("()")
84
+ js_indicators = [(r"\b(var|let|const)\s+[\w_$]+", 2), # JS variable declarations
85
+ (r"\$[\w_$]+\s*=", 2), # jQuery-style variables
86
+ (r"\$[\w_$]+\s*\(", 2), # jQuery function calls
87
+ (r"\bfunction\s*[\w_$]*\s*\(", 2), # Function declarations
88
+ (r"[\w_$]+\s*=\s*function\s*\(", 2), # Function assignments
89
+ (r"\b[\w_$]+\s*=>\s*[\{\(]", 2), # Arrow functions
90
+ (r"\(function\s*\(\)\s*\{", 2), # IIFE pattern
91
+ (funcs_pattern, 2), # Custom predefined functions
92
+ (r"\b(true|false|null|undefined)\b", 1), # JS literals
93
+ (r"===|!==|\+\+|--|\|\||&&", 1.5), # JS-specific operators
94
+ (r"\bnew\s+[\w_$]+\s*\(", 1.5), # Object instantiation with new
95
+ (r"\b(document|window|console|Math|Array|Object|String|Number)\.", 2), # JS objects
96
+ (r"\basync\s+function|\bawait\b", 2), # Async/await
97
+ (r"\b(if|for|while|switch)\s*\([^)]*\)\s*\{", 1), # Control structures with braces
98
+ (r"\btry\s*\{[^}]*\}\s*catch\s*\(", 1.5), # Try-catch
99
+ (r";[\s\n]*$", 0.5), # Semicolon line endings
100
+ ]
101
+ js_score = 0
102
+ line_endings = [line.strip() for line in code.splitlines() if line.strip()]
103
+ semicolon_endings = sum(1 for line in line_endings if line.endswith(';'))
104
+ if semicolon_endings >= 1:
105
+ js_score += min(semicolon_endings, 2)
106
+ opening_braces = code.count('{')
107
+ closing_braces = code.count('}')
108
+ if opening_braces > 0 and opening_braces == closing_braces:
109
+ js_score += 1
110
+ for pattern, score in js_indicators:
111
+ regex = _rx.compile(pattern, _rx.IGNORECASE)
112
+ matches = regex.findall(code)
113
+ if matches:
114
+ js_score += len(matches) * score
115
+ return js_score >= 2
xulbux/xx_color.py CHANGED
@@ -97,12 +97,7 @@ class rgba:
97
97
  def __eq__(self, other: "rgba") -> bool:
98
98
  if not isinstance(other, rgba):
99
99
  return False
100
- return (self.r, self.g, self.b, self.a) == (
101
- other[0],
102
- other[1],
103
- other[2],
104
- other[3],
105
- )
100
+ return (self.r, self.g, self.b, self.a) == (other.r, other.g, other.b, other.a)
106
101
 
107
102
  def dict(self) -> dict:
108
103
  """Returns the color components as a dictionary with keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
@@ -156,9 +151,15 @@ class rgba:
156
151
  self.a = 1 - self.a
157
152
  return rgba(self.r, self.g, self.b, self.a, _validate=False)
158
153
 
159
- def grayscale(self) -> "rgba":
160
- """Converts the color to grayscale using the luminance formula"""
161
- self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
154
+ def grayscale(self, method: str = "wcag2") -> "rgba":
155
+ """Converts the color to grayscale using the luminance formula.\n
156
+ ------------------------------------------------------------------
157
+ The `method` is the luminance calculation method to use:
158
+ - `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
159
+ - `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
160
+ - `"simple"` Simple arithmetic mean (less accurate)
161
+ - `"bt601"` ITU-R BT.601 standard (older TV standard)"""
162
+ self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b, method=method)
162
163
  return rgba(self.r, self.g, self.b, self.a, _validate=False)
163
164
 
164
165
  def blend(self, other: "rgba", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
@@ -286,20 +287,15 @@ class hsla:
286
287
  return ((self.h, self.s, self.l) + (() if self.a is None else (self.a, )))[index]
287
288
 
288
289
  def __repr__(self) -> str:
289
- return f'hsla({self.h}, {self.s}, {self.l}{"" if self.a is None else f", {self.a}"})'
290
+ return f'hsla({self.h}°, {self.s}%, {self.l}%{"" if self.a is None else f", {self.a}"})'
290
291
 
291
292
  def __str__(self) -> str:
292
- return f'({self.h}, {self.s}, {self.l}{"" if self.a is None else f", {self.a}"})'
293
+ return f'({self.h}°, {self.s}%, {self.l}%{"" if self.a is None else f", {self.a}"})'
293
294
 
294
295
  def __eq__(self, other: "hsla") -> bool:
295
296
  if not isinstance(other, hsla):
296
297
  return False
297
- return (self.h, self.s, self.l, self.a) == (
298
- other[0],
299
- other[1],
300
- other[2],
301
- other[3],
302
- )
298
+ return (self.h, self.s, self.l, self.a) == (other.h, other.s, other.l, other.a)
303
299
 
304
300
  def dict(self) -> dict:
305
301
  """Returns the color components as a dictionary with keys `'h'`, `'s'`, `'l'` and optionally `'a'`"""
@@ -363,9 +359,15 @@ class hsla:
363
359
  self.a = 1 - self.a
364
360
  return hsla(self.h, self.s, self.l, self.a, _validate=False)
365
361
 
366
- def grayscale(self) -> "hsla":
367
- """Converts the color to grayscale using the luminance formula"""
368
- l = Color.luminance(*self._hsl_to_rgb(self.h, self.s, self.l))
362
+ def grayscale(self, method: str = "wcag2") -> "hsla":
363
+ """Converts the color to grayscale using the luminance formula.\n
364
+ ------------------------------------------------------------------
365
+ The `method` is the luminance calculation method to use:
366
+ - `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
367
+ - `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
368
+ - `"simple"` Simple arithmetic mean (less accurate)
369
+ - `"bt601"` ITU-R BT.601 standard (older TV standard)"""
370
+ l = Color.luminance(*self._hsl_to_rgb(self.h, self.s, self.l), method=method)
369
371
  self.h, self.s, self.l, _ = rgba(l, l, l, _validate=False).to_hsla().values()
370
372
  return hsla(self.h, self.s, self.l, self.a, _validate=False)
371
373
 
@@ -520,14 +522,10 @@ class hexa:
520
522
  return f'#{self.r:02X}{self.g:02X}{self.b:02X}{"" if self.a is None else f"{int(self.a * 255):02X}"}'
521
523
 
522
524
  def __eq__(self, other: "hexa") -> bool:
525
+ """Returns whether the other color is equal to this one."""
523
526
  if not isinstance(other, hexa):
524
527
  return False
525
- return (self.r, self.g, self.b, self.a) == (
526
- other[0],
527
- other[1],
528
- other[2],
529
- other[3],
530
- )
528
+ return (self.r, self.g, self.b, self.a) == (other.r, other.g, other.b, other.a)
531
529
 
532
530
  def dict(self) -> dict:
533
531
  """Returns the color components as a dictionary with hex string values for keys `'r'`, `'g'`, `'b'` and optionally `'a'`"""
@@ -540,9 +538,9 @@ class hexa:
540
538
  )
541
539
  )
542
540
 
543
- def values(self) -> tuple:
541
+ def values(self, round_alpha: bool = True) -> tuple:
544
542
  """Returns the color components as separate values `r, g, b, a`"""
545
- return self.r, self.g, self.b, self.a
543
+ return self.r, self.g, self.b, None if self.a is None else (round(self.a, 2) if round_alpha else self.a)
546
544
 
547
545
  def to_rgba(self, round_alpha: bool = True) -> "rgba":
548
546
  """Returns the color as a `rgba()` color"""
@@ -594,9 +592,15 @@ class hexa:
594
592
  self.a = 1 - self.a
595
593
  return hexa("", self.r, self.g, self.b, self.a)
596
594
 
597
- def grayscale(self) -> "hexa":
598
- """Converts the color to grayscale using the luminance formula"""
599
- self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b)
595
+ def grayscale(self, method: str = "wcag2") -> "hexa":
596
+ """Converts the color to grayscale using the luminance formula.\n
597
+ ------------------------------------------------------------------
598
+ The `method` is the luminance calculation method to use:
599
+ - `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
600
+ - `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
601
+ - `"simple"` Simple arithmetic mean (less accurate)
602
+ - `"bt601"` ITU-R BT.601 standard (older TV standard)"""
603
+ self.r = self.g = self.b = Color.luminance(self.r, self.g, self.b, method=method)
600
604
  return hexa("", self.r, self.g, self.b, self.a)
601
605
 
602
606
  def blend(self, other: "hexa", ratio: float = 0.5, additive_alpha: bool = False) -> "rgba":
@@ -647,14 +651,20 @@ class Color:
647
651
  0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
648
652
  and (0 <= color[3] <= 1 or color[3] is None)
649
653
  )
650
- return 0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
654
+ elif len(color) == 3:
655
+ return 0 <= color[0] <= 255 and 0 <= color[1] <= 255 and 0 <= color[2] <= 255
656
+ else:
657
+ return False
651
658
  elif isinstance(color, dict):
652
659
  if allow_alpha and Color.has_alpha(color):
653
660
  return (
654
661
  0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
655
662
  and (0 <= color["a"] <= 1 or color["a"] is None)
656
663
  )
657
- return 0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
664
+ elif len(color) == 3:
665
+ return 0 <= color["r"] <= 255 and 0 <= color["g"] <= 255 and 0 <= color["b"] <= 255
666
+ else:
667
+ return False
658
668
  elif isinstance(color, str):
659
669
  return bool(_re.fullmatch(Regex.rgba_str(allow_alpha=allow_alpha), color))
660
670
  return False
@@ -672,16 +682,20 @@ class Color:
672
682
  0 <= color[0] <= 360 and 0 <= color[1] <= 100 and 0 <= color[2] <= 100
673
683
  and (0 <= color[3] <= 1 or color[3] is None)
674
684
  )
675
- else:
685
+ elif len(color) == 3:
676
686
  return 0 <= color[0] <= 360 and 0 <= color[1] <= 100 and 0 <= color[2] <= 100
687
+ else:
688
+ return False
677
689
  elif isinstance(color, dict):
678
690
  if allow_alpha and Color.has_alpha(color):
679
691
  return (
680
692
  0 <= color["h"] <= 360 and 0 <= color["s"] <= 100 and 0 <= color["l"] <= 100
681
693
  and (0 <= color["a"] <= 1 or color["a"] is None)
682
694
  )
683
- else:
695
+ elif len(color) == 3:
684
696
  return 0 <= color["h"] <= 360 and 0 <= color["s"] <= 100 and 0 <= color["l"] <= 100
697
+ else:
698
+ return False
685
699
  elif isinstance(color, str):
686
700
  return bool(_re.fullmatch(Regex.hsla_str(allow_alpha=allow_alpha), color))
687
701
  except Exception:
@@ -691,9 +705,9 @@ class Color:
691
705
  def is_valid_hexa(color: str | int, allow_alpha: bool = True, get_prefix: bool = False) -> bool | tuple[bool, str]:
692
706
  try:
693
707
  if isinstance(color, hexa):
694
- return (True, "#")
708
+ return (True, "#") if get_prefix else True
695
709
  elif isinstance(color, int):
696
- is_valid = 0 <= color <= (0xFFFFFFFF if allow_alpha else 0xFFFFFF)
710
+ is_valid = 0x000000 <= color <= (0xFFFFFFFF if allow_alpha else 0xFFFFFF)
697
711
  return (is_valid, "0x") if get_prefix else is_valid
698
712
  elif isinstance(color, str):
699
713
  color, prefix = ((color[1:], "#") if color.startswith("#") else
@@ -705,7 +719,7 @@ class Color:
705
719
 
706
720
  @staticmethod
707
721
  def is_valid(color: str | list | tuple | dict, allow_alpha: bool = True) -> bool:
708
- return (
722
+ return bool(
709
723
  Color.is_valid_rgba(color, allow_alpha) or Color.is_valid_hsla(color, allow_alpha)
710
724
  or Color.is_valid_hexa(color, allow_alpha)
711
725
  )
@@ -777,7 +791,7 @@ class Color:
777
791
  --------------------------------------------------------------------------------------------------
778
792
  If `only_first` is `True` only the first found color will be returned (not as a list)."""
779
793
  if only_first:
780
- match = _re.search(Regex.rgb_str(allow_alpha=True), string)
794
+ match = _re.search(Regex.rgba_str(allow_alpha=True), string)
781
795
  if not match:
782
796
  return None
783
797
  m = match.groups()
@@ -789,7 +803,7 @@ class Color:
789
803
  _validate=False,
790
804
  )
791
805
  else:
792
- matches = _re.findall(Regex.rgb_str(allow_alpha=True), string)
806
+ matches = _re.findall(Regex.rgba_str(allow_alpha=True), string)
793
807
  if not matches:
794
808
  return None
795
809
  return [
@@ -865,28 +879,47 @@ class Color:
865
879
  raise ValueError(f"Invalid HEX integer '0x{hex_str}': expected in range [0x000000, 0xFFFFFF]")
866
880
 
867
881
  @staticmethod
868
- def luminance(r: int, g: int, b: int, output_type: type = None) -> int | float:
869
- """Gets the colors luminance using the luminance formula.\n
870
- ------------------------------------------------------------
871
- The param `output_type` can be set to:
872
- - `int` =⠀integer in [0, 100]
873
- - `float` =⠀float in [0.0, 1.0]
874
- - `None` =⠀integer in [0, 255]"""
882
+ def luminance(r: int, g: int, b: int, output_type: type = None, method: str = "wcag2") -> int | float:
883
+ """Calculates the relative luminance of a color according to various standards.\n
884
+ ----------------------------------------------------------------------------------
885
+ The `output_type` controls the range of the returned luminance value:
886
+ - `int` returns integer in [0, 100]
887
+ - `float` returns float in [0.0, 1.0]
888
+ - `None` returns integer in [0, 255]\n
889
+ The `method` is the luminance calculation method to use:
890
+ - `"wcag2"` WCAG 2.0 standard (default and most accurate for perception)
891
+ - `"wcag3"` Draft WCAG 3.0 standard with improved coefficients
892
+ - `"simple"` Simple arithmetic mean (less accurate)
893
+ - `"bt601"` ITU-R BT.601 standard (older TV standard)"""
875
894
  r, g, b = r / 255.0, g / 255.0, b / 255.0
876
- if r < 0.03928:
877
- r = r / 12.92
895
+ if method == "simple":
896
+ luminance = (r + g + b) / 3
897
+ elif method == "bt601":
898
+ luminance = 0.299 * r + 0.587 * g + 0.114 * b
899
+ elif method == "wcag3":
900
+ r = Color._linearize_srgb(r)
901
+ g = Color._linearize_srgb(g)
902
+ b = Color._linearize_srgb(b)
903
+ luminance = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b
878
904
  else:
879
- r = ((r + 0.055) / 1.055)**2.4
880
- if g < 0.03928:
881
- g = g / 12.92
905
+ r = Color._linearize_srgb(r)
906
+ g = Color._linearize_srgb(g)
907
+ b = Color._linearize_srgb(b)
908
+ luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
909
+ if output_type == int:
910
+ return round(luminance * 100)
911
+ elif output_type == float:
912
+ return luminance
882
913
  else:
883
- g = ((g + 0.055) / 1.055)**2.4
884
- if b < 0.03928:
885
- b = b / 12.92
914
+ return round(luminance * 255)
915
+
916
+ @staticmethod
917
+ def _linearize_srgb(c: float) -> float:
918
+ """Helper method to linearize sRGB component following the WCAG standard."""
919
+ if c <= 0.03928:
920
+ return c / 12.92
886
921
  else:
887
- b = ((b + 0.055) / 1.055)**2.4
888
- l = 0.2126 * r + 0.7152 * g + 0.0722 * b
889
- return round(l * 100) if isinstance(output_type, int) else round(l * 255) if output_type is None else l
922
+ return ((c + 0.055) / 1.055)**2.4
890
923
 
891
924
  @staticmethod
892
925
  def text_color_for_on_bg(text_bg_color: rgba | hexa) -> rgba | hexa: