diff options
Diffstat (limited to 'thcolor/decoders.py')
-rw-r--r-- | thcolor/decoders.py | 269 |
1 files changed, 166 insertions, 103 deletions
diff --git a/thcolor/decoders.py b/thcolor/decoders.py index 9cda965..3aae9cc 100644 --- a/thcolor/decoders.py +++ b/thcolor/decoders.py @@ -15,6 +15,7 @@ from typing import ( Sequence as _Sequence, Tuple as _Tuple, List as _List, ) from inspect import getfullargspec as _getfullargspec +from itertools import zip_longest as _zip_longest from .angles import Angle as _Angle from .colors import ( @@ -25,6 +26,7 @@ from .angles import ( RadiansAngle as _RadiansAngle, TurnsAngle as _TurnsAngle, ) from .errors import ColorExpressionSyntaxError as _ColorExpressionSyntaxError +from .utils import factor as _factor __all__ = [ 'ColorDecoder', 'MetaColorDecoder', @@ -40,14 +42,6 @@ def _canonicalkey(x): return str(x).replace('-', '_').casefold() -def _factor(x, max_=100): - """ Return a factor based on if something is a float or an int. """ - - if isinstance(x, float): - return x - return x / max_ - - def _issubclass(cls, types): """ Check if ``cls`` is a subclass of ``types``. @@ -110,6 +104,8 @@ def _get_args(func) -> _Sequence[_Tuple[str, _Any, _Any]]: value, the function will raise an exception. """ + # TODO: manage functions with variable positional arguments. + argspec = _getfullargspec(func) try: kwarg = next( @@ -259,6 +255,18 @@ def _make_function(func, name: str, args: _Optional[_Sequence[_Any]]): return func +def _ncol_func( + angle: _Angle, + whiteness: _Union[int, float] = 0, + blackness: _Union[int, float] = 0, +): + return _HWBColor( + hue=angle, + whiteness=_factor(whiteness), + blackness=_factor(blackness), + ) + + # --- # Lexer. # --- @@ -460,12 +468,24 @@ def _get_color_tokens(string: str): rawtext=result['flt_val'], ) elif result['ncol'] is not None: - yield _ColorExpressionToken( - _ColorExpressionToken.TYPE_NCOL, - value=result['ncol'], - column=column, - rawtext=result['ncol'], - ) + letter = result['ncol'][0] + number = float(result['ncol'][1:]) + + if number < 0 or number >= 100: + yield _ColorExpressionToken( + _ColorExpressionToken.TYPE_NAME, + value=result['ncol'], + column=column, + ) + else: + yield _ColorExpressionToken( + _ColorExpressionToken.TYPE_NCOL, + value=_DegreesAngle( + 'RYGCBM'.find(letter) * 60 + number / 100 * 60 + ), + column=column, + rawtext=result['ncol'], + ) elif result['hex'] is not None: value_s = result['hex'] if len(value_s) <= 4: @@ -660,13 +680,30 @@ class ColorDecoder(_Mapping): defaults_to_netscape = bool(self._defaults_to_netscape_color) # Parsing stage; the results will be in ``current``. + # + # * ``stack``: the stack of current values. + # * ``func_stack``: the function name or special type, as tokens. + # Accepted token types here are ``NAME`` and ``NCOL``. + # * ``implicit_stack``: when the current function is an implicit + # one, this stack contains the arguments left to read in this + # implicit function. + # * ``current``: the current list of elements, which is put on + # top of the stack when a call is started and is popped out of + # the top of the stack when a call is ended. stack: _List[_List[_ColorExpressionToken]] = [] - func_stack = [_ColorExpressionToken( - _ColorExpressionToken.TYPE_NAME, - value=None, - )] - current = [] + func_stack: _List[_ColorExpressionToken] = [] + implicit_stack: _List[int] = [] + current: _List[_ColorExpressionToken] = [] + + def _get_parent_func(): + """ Get the parent function name for exceptions. """ + + if not func_stack: + return None + if func_stack[0].type_ == _ColorExpressionToken.TYPE_NCOL: + return '<implicit ncol function>' + return func_stack[0].value token_iter = _get_color_tokens(expr) for token in token_iter: @@ -674,59 +711,10 @@ class ColorDecoder(_Mapping): token.type_ == _ColorExpressionToken.TYPE_NCOL and ncol_support ): - letter = token.value[0] - number = float(token.value[1:]) - - if number < 0 or number >= 100: - raise _ColorExpressionSyntaxError( - 'ncol number should be between 0 and 100 excluded, ' - f'is {number!r}', - column=token.column, - func=func_stack[0].value, - ) - - # Get the two next tokens, which should be numbers. - # Note that they cannot result from function calls, they - # should be defined in the syntax directly. - # - # TODO: make this an implicit function with two arguments, - # so that percentages can be determined dynamically. - # TODO: if the NColor is not valid, maybe it is a symbol - # name, in which case a default should be set. - - first_token, second_token = None, None - try: - first_token = next(token_iter) - second_token = next(token_iter) - except StopIteration: - break - - if first_token is None or second_token is None or any( - token.type_ not in ( - _ColorExpressionToken.TYPE_INTEGER, - _ColorExpressionToken.TYPE_FLOAT, - _ColorExpressionToken.TYPE_PERCENTAGE, - ) - for token in (first_token, second_token) - ): - raise _ColorExpressionSyntaxError( - 'ncol should be followed by the whiteness and ' - 'the blackness as two constant values', - column=token.column, - func=func_stack[0].value, - ) - - current.append(_ColorExpressionToken( - type_=_ColorExpressionToken.TYPE_HEX, - column=token.column, - value=_HWBColor( - hue=_DegreesAngle( - 'RYGCBM'.find(letter) * 60 + number / 100 * 60 - ), - whiteness=_factor(first_token.value), - blackness=_factor(second_token.value), - ), - )) + func_stack.insert(0, token) + stack.insert(0, current) + implicit_stack.insert(0, 3) + current = [] elif token.type_ == _ColorExpressionToken.TYPE_NCOL: current.append(_ColorExpressionToken( _ColorExpressionToken.TYPE_NAME, @@ -747,29 +735,55 @@ class ColorDecoder(_Mapping): current.append(token) elif token.type_ == _ColorExpressionToken.TYPE_CALL_START: + if func_stack and ( + func_stack[0].type_ == _ColorExpressionToken.TYPE_NCOL + ): + implicit_stack[0] += 1 + name_token = current.pop(-1) if name_token.type_ != _ColorExpressionToken.TYPE_NAME: raise _ColorExpressionSyntaxError( 'expected the name of the function to call, ' f'got a {name_token.type_.name}', column=name_token.column, - func=func_stack[0].value, + func=_get_parent_func(), ) func_stack.insert(0, name_token) stack.insert(0, current) current = [] elif token.type_ == _ColorExpressionToken.TYPE_CALL_END: - try: + # First, pop out all of the implicit functions. + + while func_stack: + try: + name_token = func_stack.pop(0) + except IndexError: + raise _ColorExpressionSyntaxError( + 'extraneous closing parenthesis', + column=token.column, + func=_get_parent_func(), + ) from None + + if name_token.type_ == _ColorExpressionToken.TYPE_NAME: + break + + # We just pop the function out of the stack with the + # arguments we have. + + implicit_stack.pop(0) old_current = stack.pop(0) - except IndexError: - raise _ColorExpressionSyntaxError( - 'extraneous closing parenthesis', - column=token.column, - func=func_stack[0].value, - ) from None + old_current.append(_ColorExpressionToken( + type_=_ColorExpressionToken.TYPE_CALL, + column=name_token.column, + name=_ncol_func, + value=(name_token, *current), + )) + current = old_current - name_token = func_stack.pop(0) + # We have a function name and it is not implicit. + + old_current = stack.pop(0) old_current.append(_ColorExpressionToken( _ColorExpressionTokenType.CALL, name=name_token.value, @@ -782,12 +796,55 @@ class ColorDecoder(_Mapping): f'unknown token type: {token.type_!r}', ) - if stack: - raise _ColorExpressionSyntaxError( - 'missing closing parenthesis', - column=len(expr), - func=func_stack[0].value, - ) + if func_stack and ( + func_stack[0].type_ == _ColorExpressionToken.TYPE_NCOL + ): + implicit_stack[0] -= 1 + if implicit_stack[0] <= 0: + ncol = func_stack.pop(0) + implicit_stack.pop(0) + + # We have the required number of tokens for this + # to work. + + old_current = stack.pop(0) + old_current.append(_ColorExpressionToken( + type_=_ColorExpressionToken.TYPE_CALL, + column=ncol.column, + name=_ncol_func, + value=(ncol, *current), + )) + current = old_current + + # We want to pop out all implicit functions we have. + # If we still have an explicit function in the function stack, + # that means a parenthesis was omitted. + + while func_stack: + try: + name_token = func_stack.pop(0) + except IndexError: + break + + if name_token.type_ == _ColorExpressionToken.TYPE_NAME: + raise _ColorExpressionSyntaxError( + 'missing closing parenthesis', + column=len(expr), + func=_get_parent_func(), + ) + + # We just pop the function out of the stack with the + # arguments we have. + + implicit_stack.pop(0) + old_current = stack.pop(0) + old_current.append(_ColorExpressionToken( + type_=_ColorExpressionToken.TYPE_CALL, + column=name_token.column, + name=_ncol_func, + value=(name_token, *current), + )) + current = old_current # Evaluating stage. @@ -847,15 +904,18 @@ class ColorDecoder(_Mapping): return (element.value, element.rawtext) func_name = element.name - - try: - func = self[func_name] - except KeyError: - raise _ColorExpressionSyntaxError( - f'function {func_name!r} not found', - column=element.column, - func=parent_func, - ) + if callable(func_name): + func = func_name + func_name = '<implicit ncol function>' + else: + try: + func = self[func_name] + except KeyError: + raise _ColorExpressionSyntaxError( + f'function {func_name!r} not found', + column=element.column, + func=parent_func, + ) # Check the function. @@ -895,16 +955,19 @@ class ColorDecoder(_Mapping): new_args = [] - for token, (argname, argtype, argdefvalue) in zip( - unevaluated_args, args_spec + for token, (argname, argtype, argdefvalue) in _zip_longest( + unevaluated_args, args_spec, fillvalue=None, ): - arg, *_, fallback_string = evaluate(token) - arg = arg if arg is not None else argdefvalue + arg, fallback_string, column = None, None, element.column + if token is not None: + column = token.column + arg, *_, fallback_string = evaluate(token) - if arg is None: + arg = arg if arg is not None else argdefvalue + if arg is _NO_DEFAULT_VALUE: raise _ColorExpressionSyntaxError( f'expected a value for {argname!r}', - column=token.column, + column=column, func=func_name, ) @@ -924,7 +987,7 @@ class ColorDecoder(_Mapping): raise _ColorExpressionSyntaxError( f'{arg!r} did not match expected type {argtype!r}' f' for argument {argname!r}', - column=token.column, + column=column, func=func_name, ) |