Source code for highcharts_core.js_literal_functions

import datetime
import string
from typing import Optional
from decimal import Decimal
from collections import UserDict

from validator_collection import checkers, validators
import esprima
from esprima.error_handler import Error as ParseError

try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False
    
from highcharts_core import constants, errors, utility_functions


[docs]def serialize_to_js_literal(item, encoding = 'utf-8', ignore_to_array = False, careful_validation = False) -> Optional[str]: """Convert ``item`` to the contents of a JavaScript object literal code snippet. :param item: A value that is to be converted into a JS object literal notation value. :param encoding: The character encoding to apply to the resulting object. Defaults to ``'utf-8'``. :type encoding: :class:`str <python:str>` :param ignore_to_array: If ``True``, will ignore handling of the ``.to_array()`` method to break recursion. Defaults to ``False``. :type ignore_to_array: :class:`bool <python:bool>` :param careful_validation: if ``True``, will carefully validate JavaScript values along the way using the `esprima-python <https://github.com/Kronuz/esprima-python>`__ library. Defaults to ``False``. .. warning:: Setting this value to ``True`` will significantly degrade serialization performance, though it may prove useful for debugging purposes. :type careful_validation: :class:`bool <python:bool>` :returns: A JavaScript object literal code snippet, expressed as a string. Or :obj:`None <python:None>` if ``item`` is not serializable. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ if not ignore_to_array and hasattr(item, 'to_array'): requires_js_objects = getattr(item, 'requires_js_object', True) if requires_js_objects and hasattr(item, 'to_js_literal'): return item.to_js_literal(encoding = encoding, careful_validation = careful_validation) elif requires_js_objects: return serialize_to_js_literal(item, encoding = encoding, ignore_to_array = True, careful_validation = careful_validation) else: return serialize_to_js_literal(item.to_array(), encoding = encoding, careful_validation = careful_validation) elif HAS_NUMPY and utility_functions.is_ndarray(item): return utility_functions.from_ndarray(item) elif hasattr(item, 'to_js_literal'): return item.to_js_literal(encoding = encoding, careful_validation = careful_validation) elif not isinstance(item, (str, bytes, dict, UserDict)) and hasattr(item, '__iter__'): requires_js_objects = False for x in item: try: if getattr(x, 'requires_js_object', True) is True: requires_js_objects = True break except ValueError as error: if utility_functions.is_ndarray(x): continue else: raise error if requires_js_objects: return [serialize_to_js_literal(x, encoding = encoding, ignore_to_array = True, careful_validation = careful_validation) for x in item] else: result = [] for x in item: if not utility_functions.is_ndarray(x): js_literal = serialize_to_js_literal(x.to_array(), encoding = encoding, careful_validation = careful_validation) result.append(js_literal) else: result.append(utility_functions.from_ndarray(x)) return result elif isinstance(item, constants.EnforcedNullType) or item == 'null': return constants.EnforcedNull elif isinstance(item, bool): return item elif isinstance(item, str): return_value = item.replace("'", "\\'") return return_value elif checkers.is_numeric(item) and not isinstance(item, Decimal): return item elif isinstance(item, Decimal): return float(item) elif checkers.is_type(item, ('CallbackFunction')): return str(item) elif isinstance(item, (dict, UserDict)): as_dict = {} for key in item: as_dict[key] = serialize_to_js_literal(item[key], encoding = encoding, careful_validation = careful_validation) return str(as_dict) elif checkers.is_datetime(item): if not item.tzinfo: item = item.replace(tzinfo = datetime.timezone.utc) return item.timestamp() * 1000 elif checkers.is_date(item): return f'Date.UTC({item.year}, {item.month - 1}, {item.day})' elif checkers.is_time(item): return item.isoformat() elif item is None: return None return None
def is_js_object(as_str, careful_validation = False): """Determine whether ``as_str`` is a JavaScript object. :param as_str: The string to evaluate. :type as_str: :class:`str <python:str>` :param careful_validation: if ``True``, will carefully validate JavaScript values along the way using the `esprima-python <https://github.com/Kronuz/esprima-python>`__ library. Defaults to ``False``. .. warning:: Setting this value to ``True`` will significantly degrade serialization performance, though it may prove useful for debugging purposes. :type careful_validation: :class:`bool <python:bool>` :returns: ``True`` if ``as_str`` is a JavaScript function. ``False`` if not. :rtype: :class:`bool <python:bool>` """ if not careful_validation: is_empty = as_str[1:-1].strip() == '' if is_empty: return True colon_count = as_str.count(':') open_brace_count = as_str.count('{') close_brace_count = as_str.count('}') brace_set_count = (open_brace_count + close_brace_count) / 2 if colon_count > 0: if brace_set_count == 1: return True elif brace_set_count > 1 and colon_count >= brace_set_count: return True else: careful_validation = True elif 'new ' in as_str: return True elif 'Object.create(' in as_str: return True else: return False if careful_validation: expression_item = f'const testName = {as_str}' try: parsed = esprima.parseScript(expression_item) except ParseError: try: parsed = esprima.parseModule(expression_item) except ParseError: return False body = parsed.body if not body: return False first_item = body[0] if first_item.type != 'VariableDeclaration': return False init = first_item.declarations[0].init if not init: return False if init.type in ('ObjectExpression'): return True return False return False
[docs]def attempt_variable_declaration(as_str): """Attempt to coerce ``as_str`` to a JavaScript variable declaration form. :param as_str: The string to evaluate. :type as_str: :class:`str <python:str>` :returns: ``True`` if ``as_str`` is a JavaScript function. ``False`` if not. :rtype: :class:`bool <python:bool>` """ expression_item = f'const testName = {as_str}' try: parsed = esprima.parseScript(expression_item) except ParseError: try: parsed = esprima.parseModule(expression_item) except ParseError: return False body = parsed.body if not body: return False first_item = body[0] if first_item.type != 'VariableDeclaration': return False init = first_item.declarations[0].init if not init: return False if init.type in ('FunctionExpression', 'ArrowFunctionExpression', 'ClassExpression'): return True elif init.type == 'NewExpression': callee = init.callee if not callee: return False if callee.name == 'Function': return True return False
[docs]def is_js_function_or_class(as_str, careful_validation = False) -> bool: """Determine whether ``as_str`` is a JavaScript function or not. :param as_str: The string to evaluate. :type as_str: :class:`str <python:str>` :param careful_validation: if ``True``, will carefully validate JavaScript values along the way using the `esprima-python <https://github.com/Kronuz/esprima-python>`__ library. Defaults to ``False``. .. warning:: Setting this value to ``True`` will significantly degrade serialization performance, though it may prove useful for debugging purposes. :type careful_validation: :class:`bool <python:bool>` :returns: ``True`` if ``as_str`` is a JavaScript function. ``False`` if not. :rtype: :class:`bool <python:bool>` """ if not isinstance(as_str, str): return False if not careful_validation: is_function = as_str.startswith('function ') or as_str.startswith('function*') if is_function: return True is_function = 'function(' in as_str or 'function*(' in as_str if is_function: return True is_function = ')=>' in as_str or ') =>' in as_str if is_function: return True is_function = 'new Function(' in as_str if is_function: return True is_class = as_str.startswith('class ') if is_class: return True is_class = 'class {' in as_str or 'class{' in as_str if is_class: return True is_class = '= class' in as_str or '=class' in as_str if is_class: return True return False else: try: parsed = esprima.parseScript(as_str) except ParseError: try: parsed = esprima.parseModule(as_str) except ParseError: if as_str.startswith('function') is False: return False else: return attempt_variable_declaration(as_str) body = parsed.body if not body: return False first_item = body[0] if first_item.type in ('FunctionDeclaration', 'ClassDeclaration'): return True elif as_str.startswith('function') or as_str.startswith('class'): return attempt_variable_declaration(as_str) elif first_item.type == 'VariableDeclaration': init = first_item.declarations[0].init if not init: return False if init.type in ('FunctionExpression', 'ArrowFunctionExpression', 'ClassExpression'): return True elif init.type == 'NewExpression': callee = init.callee if not callee: return False if callee.name == 'Function': return True return False
[docs]def get_js_literal(item, careful_validation = False) -> str: """Convert the value of ``item`` into a JavaScript literal string. :param careful_validation: if ``True``, will carefully validate JavaScript values along the way using the `esprima-python <https://github.com/Kronuz/esprima-python>`__ library. Defaults to ``False``. .. warning:: Setting this value to ``True`` will significantly degrade serialization performance, though it may prove useful for debugging purposes. :type careful_validation: :class:`bool <python:bool>` :returns: The JavaScript literal string. :rtype: :class:`str <python:str>` """ as_str = '' if not isinstance(item, (str, bytes, dict, UserDict)) and hasattr(item, '__iter__'): subitems = [get_js_literal(x) for x in item] as_str += '[' subitem_counter = 0 for subitem in subitems: subitem_counter += 1 if subitem == 'None': subitem = 'null' as_str += f"""{subitem}""" if subitem_counter < len(subitems): as_str += ',\n' as_str += ']' elif isinstance(item, str): if (item.startswith('[') or item.startswith('Date')) and item != 'Date': as_str += f"""{item}""" elif item.startswith('${'): if "'" in item: item = item.replace("'", "\\'") as_str += f'"{item[1:]}"' else: as_str += f"'{item[1:]}'" elif item.startswith('{') and item.endswith('}'): if is_js_object(item, careful_validation = careful_validation): as_str += f"""{item}""" elif "'" in item: item = item.replace("'", "\\'") as_str += f'"{item}"' else: as_str += f"'{item}'" elif item in string.whitespace: as_str += f"""`{item}`""" elif item.startswith == 'HCP: REPLACE-WITH-': item_str = item.replace('HCP: REPLACE-WITH-', '') as_str += f"""{item_str}""" elif not is_js_function_or_class(item, careful_validation = careful_validation): as_str += f"""'{item}'""" else: as_str += f"""{item}""" elif item == constants.EnforcedNull or item is None: as_str += """null""" elif HAS_NUMPY and not isinstance(item, (dict, UserDict, Decimal)) and np.isnan(item): as_str += """null""" elif item is True: as_str += """true""" elif item is False: as_str += """false""" else: as_str += f"""{item}""" return as_str
[docs]def assemble_js_literal(as_dict, keys_as_strings = False, careful_validation = False) -> Optional[str]: """Convert ``as_dict`` into a JavaScript object literal string. :param as_dict: A :class:`dict <python:dict>` representation of the JavaScript object. :type as_dict: :class:`dict <pythoN:dict>` :param keys_as_strings: if ``True``, will return the keys as string values (wrapped in quotation marks). If ``False``, will return the keys as object literals. Defaults to ``False``. :type keys_as_strings: :class:`bool <python:bool>` :param careful_validation: if ``True``, will carefully validate JavaScript values along the way using the `esprima-python <https://github.com/Kronuz/esprima-python>`__ library. Defaults to ``False``. .. warning:: Setting this value to ``True`` will significantly degrade serialization performance, though it may prove useful for debugging purposes. :type careful_validation: :class:`bool <python:bool>` :returns: The JavaScript object literal representation of ``as_dict``. :rtype: :class:`str <python:str>` or :obj:`None <python:None>` """ if careful_validation: as_dict = validators.dict(as_dict, allow_empty = True) if not as_dict: return None as_str = '' keys = len(as_dict) current_key = 0 ended_on_None = False for key in as_dict: current_key += 1 item = as_dict[key] if item is None: keys -= 1 ended_on_None = True continue if keys_as_strings: as_str += f""" '{key}': """ else: as_str += f""" {key}: """ as_str += get_js_literal(item, careful_validation = careful_validation) if current_key < keys: as_str += ',\n' else: ended_on_None = False as_str += '\n' if not as_str: return None elif ended_on_None: as_str = as_str[:-2] as_str += '\n' as_str = '{\n' + as_str + '}' return as_str
[docs]def convert_js_literal_to_python(literal_definition, original_str: None): """Convert a :class:`esprima.nodes.Literal` object into a Python literal. .. note:: The :class:`esprima.nodes.Property` objects are available in the ``value`` sub-item. :rtype: Python objcet """ if not checkers.is_type(literal_definition, ('Literal', 'Identifier')): raise errors.HighchartsParseError(f'literal_definition should contain a ' f'Literal or Identifier instance. Received: ' f'{literal_definition.__class__.__name__}') if literal_definition.raw == 'null': return constants.EnforcedNull elif literal_definition.name == 'undefined': return None elif literal_definition.value is not None: return literal_definition.value else: raise errors.HighchartsParseError('unable to find a literal, array, or object ' 'definition')
[docs]def convert_js_property_to_python(property_definition, original_str = None): """Convert a :class:`esprima.nodes.Property` object into a Python literal. .. note:: The :class:`esprima.nodes.Property` objects are available in the ``value`` sub-item. """ from highcharts_core.utility_classes.javascript_functions import CallbackFunction, \ JavaScriptClass if not checkers.is_type(property_definition, 'Property'): raise errors.HighchartsParseError(f'property_definition should contain a ' f'Property instance. Received: ' f'{property_definition.__class__.__name__}') if property_definition.value.type == 'Literal': if property_definition.value.raw == 'null': return constants.EnforcedNull elif property_definition.value.name == 'undefined': return None elif property_definition.value is not None: return property_definition.value.value else: raise errors.HighchartsParseError(f'unexpected parse error when ' f'interpreting:\n{property_definition}') elif property_definition.value.type == 'TemplateLiteral': template_elements = property_definition.value.quasis if template_elements: element = template_elements[0] if element.type == 'TemplateElement': return element.value.cooked else: raise errors.HighchartsParseError('unable to properly parse a ' 'TemplateLiteral. Specifically could ' 'not find a TemplateElement where one ' 'was expected.') elif property_definition.value.type == 'UnaryExpression': property_value = property_definition.value operator = property_value.operator if operator == '-': multiple = -1 else: multiple = 1 argument_type = property_value.argument.type if argument_type not in ['Literal']: raise errors.HighchartsParseError(f'unable to find a Literal value within' f'a Unary expression. Found: ' f'{argument_type}') value = property_value.argument.value if checkers.is_numeric(value): return value * multiple else: return value elif property_definition.value.type == 'Identifier': if property_definition.value.name == 'undefined': return None else: raise errors.HighchartsParseError(f'unexpected parse error when ' f'interpreting:\n{property_definition}') elif property_definition.value.type == 'ArrayExpression': return [convert_js_to_python(x, original_str) for x in property_definition.value.elements] elif property_definition.value.type == 'ObjectExpression': as_dict = {} key_value_pairs = get_key_value_pairs(property_definition.value.properties, original_str) for pair in key_value_pairs: as_dict[pair[0]] = pair[1] return as_dict elif property_definition.value.type == 'FunctionExpression': return CallbackFunction._convert_from_js_ast(property_definition, original_str) elif property_definition.value.type == 'ClassExpression': return JavaScriptClass._convert_from_js_ast(property_definition.value, original_str) elif property_definition.value.type == 'BinaryExpression': property_value = property_definition.value operator = property_value.operator left, right = property_value.left, property_value.right left_type, right_type = left.type, right.type if (left_type not in ['Literal']) or (right_type not in ['Literal']): raise errors.HighchartsParseError(f'unable to find two Literal values within' f'a Binary expression. Found: ' f'{left_type, right_type}') left_value, right_value = left.value, right.value if operator not in ['+', '-', '/']: raise errors.HighchartsParseError(f'operator "{operator}" not supported within ' f'Binary expression parsing') left_value = validators.string(left_value, allow_empty = False) right_value = validators.string(right_value, allow_empty = False) return left_value + right_value elif property_definition.value.type == 'CallExpression': expression = property_definition.value try: callee_obj = expression.callee.object.name except AttributeError: try: callee_obj = expression.callee.name except AttributeError: raise errors.HighchartsParseError('unable to parse the JS Call ' 'Expression') call_arguments = [x.value for x in expression.arguments] if callee_obj == 'Date': if len(call_arguments) == 1: return validators.datetime(call_arguments[0]) elif len(call_arguments) == 2: return datetime.date(year = call_arguments[0], month = call_arguments[1] + 1) elif len(call_arguments) == 3: return datetime.date(year = call_arguments[0], month = call_arguments[1] + 1, day = call_arguments[2]) elif len(call_arguments) == 4: return datetime.datetime(year = call_arguments[0], month = call_arguments[1] + 1, day = call_arguments[2], hour = call_arguments[3]) elif len(call_arguments) == 5: return datetime.datetime(year = call_arguments[0], month = call_arguments[1] + 1, day = call_arguments[2], hour = call_arguments[3], minute = call_arguments[4]) elif len(call_arguments) == 6: return datetime.datetime(year = call_arguments[0], month = call_arguments[1] + 1, day = call_arguments[2], hour = call_arguments[3], minute = call_arguments[4], second = call_arguments[5]) elif len(call_arguments) == 7: return datetime.datetime(year = call_arguments[0], month = call_arguments[1] + 1, day = call_arguments[2], hour = call_arguments[3], minute = call_arguments[4], second = call_arguments[5], microsecond = call_arguments[6]) else: raise errors.HighchartsParseError('failed to parse the Date() ' 'constructor from the JS literal') else: raise errors.HighchartsParseError('unable to find a literal, array, or object ' 'definition')
[docs]def convert_js_to_python(javascript, original_str = None): """Convert a :class:`esprima.nodes.Property` object into a Python literal. .. note:: The :class:`esprima.nodes.Property` objects are available in the ``value`` sub-item. """ if javascript.type not in ('Property', 'Literal', 'ObjectExpression', 'ArrayExpression', 'UnaryExpression', 'BinaryExpression', 'FunctionExpression'): raise errors.HighchartsParseError(f'javascript should contain a ' f'Property, Literal, ObjectExpression, ' f'ArrayExpression, UnaryExpression, or ' f'FunctionExpression instance. Received: ' f'{javascript.type}') if checkers.is_type(javascript, 'Property'): return convert_js_property_to_python(javascript, original_str) elif checkers.is_type(javascript, 'ObjectExpression'): as_dict = {} key_value_pairs = get_key_value_pairs(javascript.properties, original_str) for pair in key_value_pairs: as_dict[pair[0]] = pair[1] return as_dict elif checkers.is_type(javascript, 'ArrayExpression'): return [convert_js_to_python(x, original_str) for x in javascript.elements] elif checkers.is_type(javascript, 'UnaryExpression'): is_negative = javascript.operator == '-' if is_negative: multiple = -1 converted_value = convert_js_literal_to_python(javascript.argument, original_str) return converted_value * multiple elif checkers.is_type(javascript, 'FunctionExpression'): from highcharts_core.utility_classes.javascript_functions import CallbackFunction start_char = javascript.range[0] end_char = javascript.range[1] function_as_str = original_str[start_char:end_char] return CallbackFunction.from_js_literal(function_as_str) else: return convert_js_literal_to_python(javascript, original_str)
[docs]def get_key_value_pairs(properties, original_str): """Return the key and value pairs for properties defined in ``properties``. :param properties: The definition of a JavaScript object Property. :type properties: :class:`list <python:list>` of :class:`esprima.nodes.Property` instances :rtype: :class:`list <python:list>` of :class:`tuples <python:tuple>` of :class:`str <python:str>` and any value """ key_value_pairs = [] for item in properties: if not checkers.is_type(item, 'Property'): raise errors.HighchartsParseError(f'properties should contain a list of ' f'Property instances. Received: ' f'{item.__class__.__name__}') key = item.key.name or item.key.value if not key: raise errors.HighchartsMissingKeyError('property was not formed correctly, ' 'specifically missing a key') value = convert_js_to_python(item, original_str) pair = (key, value) key_value_pairs.append(pair) return key_value_pairs