aboutsummaryrefslogtreecommitdiff
path: root/thcolor/decoders.py
diff options
context:
space:
mode:
Diffstat (limited to 'thcolor/decoders.py')
-rw-r--r--thcolor/decoders.py269
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,
)