Source code for pantra.components.render.renderer_html

from __future__ import annotations

import sys
import typing

from pantra.common import typename, DynamicStyles, DynamicClasses
from pantra.compiler import ContextInitFailed, compile_context_code
from pantra.settings import config
from ..template import AttrType, NodeType, collect_template, MacroCode, MacroType
from ..context import HTMLElement, Context, Slot, ConditionNode, Condition, LoopNode, SetNode, ScriptNode, \
    EventNode, TextNode, ReactNode
from ..shot import NullContextShot
from .renderer_base import RendererBase, ValueOrCode, ForLoopType
from .render_node import RenderNode

if typing.TYPE_CHECKING:
    from typing import *
    from pathlib import Path
    from pantra.session import Session
    from ..template import HTMLTemplate

[docs] class RendererHTML(RendererBase): """Renderer from HTML-like files""" templates: ClassVar[dict[str, HTMLTemplate | None]] = {} @staticmethod def _search_component(path: Path, name: str) -> Path | None: for file in path.glob(f"**/{name}.html"): return file return None @classmethod def collect_template(cls, name: str, session: Optional[Session] = None, app: Optional[str] = None) -> Optional[HTMLTemplate]: app = session and session.app or app key = app + '/' + name if app is not None else name if key in cls.templates: return cls.templates[key] path = app and cls._search_component(config.APPS_PATH / app, name) if not path: if name in cls.templates: cls.templates[key] = cls.templates[name] return cls.templates[name] # elif config.PRODUCTIVE: # templates[key] = None # return None path = cls._search_component(config.COMPONENTS_PATH, name) if not path: cls.templates[key] = None return None key = name from ..loader import load template = load(path, session.error) if session is not None else load(path, lambda s: print(s, file=sys.stderr)) if template: template.name = name cls.templates[key] = template return template def arrange_the_block(self, node: RenderNode, template): # we have to render control nodes as Stubs to hold middle DOM position if (node.index() < len(template.parent.children) - 1 and node.context.locals.has_reactions_to(node)): node.render_this_node = True self.ctx.shot += node #region Attribute processors @staticmethod def _return_false(*args): return False @staticmethod def _return_true(*args): return True def _process_attr_ref_name(self, attr_name, value, node): self.ctx.refs[attr_name] = node node.ref_name = attr_name return True def _process_attr_cref_name(self, attr_name, value, node): if typename(node) != 'Context': self.ctx.session.error('Use cref: with components only') return True self.ctx.refs[attr_name] = node.locals return True def _process_attr_scope(self, attr_name, value, node): node.scope[attr_name] = self.makeup_value(value, node) return True def _process_attr_on_init(self, attr_name, value, node): #run_safe(self.ctx.session, lambda: self.ctx[value](node), dont_refresh=True) self.ctx[value](node) return True def _process_attr_class_switch(self, attr_name, value, node): if value is None: value = MacroCode(MacroType.VALUE, False, False, None, attr_name) node.classes[attr_name] = self.makeup_value(value, node) return True def _process_attr_dynamic_style(self, attr_name, value, node): if value is None: value = MacroCode(MacroType.STRING, False, False, None, attr_name) node.style[attr_name] = self.makeup_value(value, node) return True def _process_attr_bind_value(self, attr_name, value, node): if value is None: value = 'value' node.attributes['bind:value'] = value node.value = self.ctx.locals[value] self.ctx.locals.register_reactions({value}, node) return True def _process_attr_dynamic_set(self, attr_name, value, node): if value is None: value = MacroCode(MacroType.STRING, False, False, None, attr_name) if attr_name in ('focused', 'localize'): if isinstance(value, str) and value == "yes": value = True elif isinstance(value, str) and value == "no": value = False else: value = self.makeup_value(value, node, True) if attr_name == 'focused': node._set_focused = bool(value) else: node.localize = bool(value) elif attr_name == 'type': node.value_type = self.makeup_value(value, node, True) else: node.attributes[attr_name] = self.makeup_value(value, node) return True def _process_attr_dynamic_set_ctx(self, attr_name, value, node): if value is None: value = True elif isinstance(value, str) and value == "yes": value = True elif isinstance(value, str) and value == "no": value = False elif isinstance(value, MacroCode): node.attributes[attr_name] = self.makeup_value(value, node) value = node.attributes[attr_name] node.locals[attr_name] = value return True def _process_attr_data(self, attr_name, value, node): node.data[attr_name] = self.makeup_value(value, node) return True def _process_attr_src_href(self, attr_name, subdir, value, node): node.attributes[attr_name] = ( self.ctx.static(subdir, self.makeup_value(value, node, True) if type(value) is MacroCode else value)) return True def _process_attr_style(self, attr_name, value, node): expr = self.makeup_value(value, node) if type(value) is MacroCode: node.style['$'] = expr else: node.style += expr return True def _process_attr_class(self, attr_name, value, node): expr = self.makeup_value(value, node) if type(value) is MacroCode: node.classes['$'] = expr else: node.classes += expr return True def _process_attr_type(self, attr_name, value, node): node.value_type = self.makeup_value(value, node, True) return True def _process_attr_value(self, attr_name, value, node): node.value = self.makeup_value(value, node, True) return True def _process_attr_localize(self, attr_name, value, node): node.localize = bool(self.makeup_value(value, node, True)) return True def _process_attr_set_false_ctx(self, attr_name, value, node): node.locals[attr_name] = False return True def _process_attr_consume(self, attr_name, value, node): if value is not None: if value == '*': node.locals |= node.context.locals else: for at in value.split(','): at = at.strip() # copy local values into consumed context if at in node.context.locals: node.locals[at] = node.context[at] return True PROCESS_HTML_ATTR: dict[int, Callable[[Self, str, Any, HTMLElement], bool]] = { AttrType.ATTR.value: _return_false, AttrType.REF_NAME.value: _process_attr_ref_name, AttrType.CREF_NAME.value: _process_attr_cref_name, AttrType.SCOPE.value: _process_attr_scope, AttrType.ON_RENDER.value: _return_true, AttrType.ON_INIT.value: _process_attr_on_init, AttrType.CLASS_SWITCH.value: _process_attr_class_switch, AttrType.DYNAMIC_STYLE.value: _process_attr_dynamic_style, AttrType.BIND_VALUE.value: _process_attr_bind_value, AttrType.DYNAMIC_SET.value: _process_attr_dynamic_set, AttrType.DATA.value: _process_attr_data, AttrType.SRC.value: lambda self, *args: self._process_attr_src_href("src", *args), AttrType.HREF.value: lambda self, *args: self._process_attr_src_href("href", *args), AttrType.REACTIVE.value: _return_true, AttrType.STYLE.value: _process_attr_style, AttrType.CLASS.value: _process_attr_class, AttrType.TYPE.value: _process_attr_type, AttrType.VALUE.value: _process_attr_value, AttrType.LOCALIZE.value: _process_attr_localize, AttrType.SET_FALSE.value: _return_false, AttrType.CONSUME.value: _return_false, AttrType.DATA_NODE.value: _return_true, } PROCESS_CTX_ATTR: dict[int, Callable[[Self, str, Any, Context], bool]] = { AttrType.ATTR.value: _return_false, AttrType.REF_NAME.value: _process_attr_ref_name, AttrType.CREF_NAME.value: _process_attr_cref_name, AttrType.SCOPE.value: _process_attr_scope, AttrType.ON_RENDER.value: _return_true, AttrType.ON_INIT.value: _process_attr_on_init, AttrType.CLASS_SWITCH.value: _return_false, AttrType.DYNAMIC_STYLE.value: _return_false, AttrType.BIND_VALUE.value: _return_false, AttrType.DYNAMIC_SET.value: _process_attr_dynamic_set_ctx, AttrType.DATA.value: _return_false, AttrType.SRC.value: _return_false, AttrType.HREF.value: _return_false, AttrType.REACTIVE.value: _return_false, AttrType.STYLE.value: _return_false, AttrType.CLASS.value: _return_false, AttrType.TYPE.value: _return_false, AttrType.VALUE.value: _return_false, AttrType.LOCALIZE.value: _return_false, AttrType.SET_FALSE.value: _process_attr_set_false_ctx, AttrType.CONSUME.value: _process_attr_consume, AttrType.DATA_NODE.value: _return_false, } #endregion def process_special_attribute_html(self, attr: tuple[AttrType, str | None], value: ValueOrCode, node: Union[HTMLElement, Context]) -> bool: return self.PROCESS_HTML_ATTR[attr[0].value](self, attr[1], value, node) def process_special_attribute_ctx(self, attr: tuple[AttrType, str | None], value: ValueOrCode, node: Union[HTMLElement, Context]) -> bool: return self.PROCESS_CTX_ATTR[attr[0].value](self, attr[1], value, node) #region Node builders def _call_after_render(self, template, node): if (func_name := template.attributes.get('on:render')) is not None: if func_name not in self.ctx.locals: raise ValueError(f'No renderer named `{func_name}` found in `{self.ctx.template.name}`') #run_safe(self.ctx.session, lambda: self.ctx[func_name](node), dont_refresh=True) self.ctx[func_name](node) def _build_group(self, template, parent): for child in template.children: self.build_node(child, parent) def _build_html_tag(self, template, parent): if 'not:render' in template.attributes: # forget it return None node = self.add(template.tag_name, parent=parent) # evaluate attributes for attr, value in template.attributes.items(): if not self.process_special_attribute_html(template.attr_specs[attr], value, node): node.attributes[attr] = self.makeup_value(value, node) # evaluate body if self.ctx.code_metrics.has_node_processor: #run_safe(self.ctx.session, lambda: self.ctx['node_processor'](node), dont_refresh=True) self.ctx['node_processor'](node) # evaluate children if len(template.children) == 1: single_child = template.children[0] if single_child.tag_name == '@text': node.attributes['$'] = self.makeup_value(single_child.content, node) node.text = node.attributes['$'] else: self.build_node(single_child, node) else: self._build_group(template, node) self._call_after_render(template, node) return node def _build_at_text(self, template, parent): node = TextNode(parent, self.makeup_value(template.content, parent)) return node def _build_template_tag(self, template, parent): node_template = collect_template(template.tag_name, self.ctx.session) if not node_template: return None if 'not:render' in template.attributes: return None node = Context(template.tag_name, parent) if template.attributes.get('consume', '') is None: node.attributes = node.context.attributes node.locals = node.context.locals #for k in ('init', 'on_restart'): # if k in node.locals: # del node.locals[k] node.refs = node.context.refs # evaluate attributes for attr, value in template.attributes.items(): if not self.process_special_attribute_ctx(template.attr_specs[attr], value, node): if value is None: value = True elif type(value) is MacroCode: node.attributes[attr] = self.makeup_value(value, node) value = node.attributes[attr] node.locals[attr] = value # attach slots if template.children: node.slot = Slot(self.ctx, template, False) for child in template.children: if child.tag_name == 'section': if name := child.attributes.get('name'): node.slot[name] = Slot(self.ctx, child, 'reuse' in child.attributes) else: node.slot.template = child node.slot.for_reuse = 'reuse' in child.attributes if self.ctx.code_metrics.has_node_processor: #run_safe(self.ctx.session, lambda: self.ctx['node_processor'](node), dont_refresh=True) self.ctx['node_processor'](node) try: node.renderer.build() except ContextInitFailed: node.remove() return None self._call_after_render(template, node) return node def _build_root_node(self, template, parent): node = self.ctx #if template.children and template.children[0].tag_name != "@python": # we have no python script here, so, we need to make `locals` from `attributes` directly # node.locals.update(node.attributes) self._build_group(template, node) if self.ctx.code_metrics.has_on_render: #run_safe(self.ctx.session, self.ctx["on_render"], dont_refresh=True) self.ctx["on_render"]() return node def _build_macro_if(self, template, parent): node = ConditionNode(parent, template) for child_template in template.children: # type: HTMLTemplate if child_template.tag_name != '#else': item = Condition(self.makeup_value(child_template.attributes['condition'], node), child_template) else: item = Condition((lambda: True), child_template) node.conditions.append(item) self.arrange_the_block(node, template) self.update(node) return node def _build_macro_for(self, template, parent): node: LoopNode = LoopNode(parent, template) loop_template = template[0] node.var_name = loop_template.attributes['var_name'] node.iterator = self.makeup_value(loop_template.attributes['iter'], node) if (index_func:=loop_template.attributes.get('index_func')) is not None: node.index_func = self.makeup_value(index_func, node) node.loop_template = loop_template if len(template.children) > 1: node.else_template = template[1] self.arrange_the_block(node, template) self.update(node) return node def _build_macro_set(self, template, parent): node: SetNode = SetNode(parent, template) for k, v in template.attributes.items(): node.variables[k] = self.makeup_value(v, node) node.scoped = template.tag_name == '#set:scope' node.self_clear = template.tag_name == '#set:clear' self.update(node) if not node.self_clear: self._build_group(template, node) return node def _build_at_component(self, template, parent): if 'render' in template.attributes: renderer = template.attributes['render'] if renderer not in self.ctx.locals: self.ctx.session.error(f'component renderer {renderer} not found') return None self.ctx.locals[renderer](parent) self._build_group(template, parent) return None def _build_at_slot(self, template, parent): if (name := template.attributes.get('name')) is not None: slot: Slot = parent.context.slot and parent.context.slot[name] else: slot: Slot = parent.context.slot.get_top() if not slot: self._build_group(template, parent) self._call_after_render(template, parent) return parent with self.override_ns_type(slot): for child in slot.template.children: if not name and child.tag_name == 'section': continue #self.build_node(child, parent) slot.ctx.renderer.build_node(child, parent) self._call_after_render(template, parent) return None def _build_at_python(self, template, parent): if not self.ctx._executed: self.ctx._executed = True compile_context_code(self.ctx, template) self.ctx.code_metrics = template.code_metrics return parent def _build_at_script(self, template, parent): attributes = template.attributes.copy() put_to_head = attributes.pop('location', 'head') == 'head' node = ScriptNode(parent, f'{self.ctx.template.name}_{template.script_index}', attributes, self.makeup_value(template.content, parent, True), put_to_head) return node def _build_at_style(self, template, parent): # styles collected elsewhere if 'global' not in template.attributes: self.ctx._restyle = True return parent def _build_at_event(self, template, parent): node = EventNode(parent) for k, v in template.attributes.items(): if k == 'selector': if 'global' in template.attributes: node.selector = v else: node.selector = ','.join(f'.{self.ctx.template.name} {s}' for s in v.split(',')) self.ctx._restyle = True elif k.startswith('on:'): node.events[k] = self.makeup_value(v, node) return node def _build_at_react(self, template, parent): var_name = template.attributes.get('to') action = template.attributes.get('action') if not var_name: self.ctx.session.error('<react> tag must have attributes `to`') return None node = ReactNode(parent, var_name, action) # take in account consumed contexts self.ctx.locals.register_reactions(set(v.strip() for v in var_name.split(',')), node) self._build_group(template, node) return node NODE_BUILDERS: dict[int, Callable[[Self, HTMLTemplate, RenderNode], RenderNode]] = { NodeType.HTML_TAG.value: _build_html_tag, NodeType.AT_TEXT: _build_at_text, NodeType.TEMPLATE_TAG.value: _build_template_tag, NodeType.ROOT_NODE.value: _build_root_node, NodeType.MACRO_IF.value: _build_macro_if, NodeType.MACRO_FOR.value: _build_macro_for, NodeType.MACRO_SET.value: _build_macro_set, NodeType.AT_COMPONENT.value: _build_at_component, NodeType.AT_SLOT.value: _build_at_slot, NodeType.AT_PYTHON.value: _build_at_python, NodeType.AT_SCRIPT.value: _build_at_script, NodeType.AT_STYLE.value: _build_at_style, NodeType.AT_EVENT.value: _build_at_event, NodeType.AT_REACT.value: _build_at_react, } #endregion def build_node(self, template: HTMLTemplate, parent: Optional[RenderNode] = None) -> Optional[RenderNode | list[RenderNode]]: if (node_name := template.attributes.get('data-node')) is not None: data_node = RenderNode(None, NullContextShot(), parent.session) data_node.context = parent.context if not self.ctx.data_nodes: self.ctx.data_nodes = {} self.ctx.data_nodes[node_name] = data_node parent = data_node node = self.NODE_BUILDERS[template.node_type.value](self, template, parent) return node #region Node updaters def _update_html_node(self, node: HTMLElement, recursive): # attributes, classes, styles and text evaluation node.attributes.refresh() if (v := node.attributes.get('bind:value')) is not None: node.value = self.ctx.locals[v] if isinstance(node.classes, DynamicClasses): node.classes.refresh() if isinstance(node.style, DynamicStyles): node.style.refresh() node.data.refresh() node.text = node.attributes.get('$') node.shot(node) return recursive def _update_context_node(self, node, recursive): for attr, value in node.attributes.refresh_items(): node.set_quietly(attr, value) return recursive def _update_text_node(self, node, recursive): node.refresh() return False def _update_condition_node(self, node, recursive): state: int condition: Optional[Condition] for i, c in enumerate(node.conditions): if c.func(): state = i condition = c break else: state = -1 condition = None if node.state != state: if node.state >= 0: node.empty() node.state = state if state == -1: return False self._build_group(condition.template, node) return False return recursive def _update_loop_node(self, node: LoopNode, recursive): if not node.index_func: node.empty() empty = True iter = node.iterator() if iter: parentloop = self.ctx.locals.get('forloop') for_loop = ForLoopType(parentloop) self.ctx.locals['forloop'] = for_loop for i, value in enumerate(iter): empty = False self.ctx.locals[node.var_name] = value for_loop.counter = i + 1 for_loop.counter0 = i self._build_group(node.loop_template, node) if node.var_name in self.ctx.locals: del self.ctx.locals[node.var_name] if parentloop: self.ctx.locals['forloop'] = parentloop else: del self.ctx.locals['forloop'] else: empty = not node.index_map if empty: node.empty() iter = node.iterator() if iter: oldmap = node.index_map node.index_map = newmap = {} pos = 0 parentloop = self.ctx.locals.get('forloop') for_loop = ForLoopType(parentloop) self.ctx.locals['forloop'] = for_loop for i, value in enumerate(iter): self.ctx.locals[node.var_name] = value for_loop.counter = i + 1 for_loop.counter0 = i index = node.index_func() for_loop.index = index if index in oldmap: for sub in oldmap[index]: # type: RenderNode sub.move(pos) sub.rebind() pos += 1 newmap[index] = oldmap[index] del oldmap[index] else: newmap[index] = [] for temp_child in node.loop_template: sub = self.build_node(temp_child, node) sub.move(pos) newmap[index].append(sub) pos += 1 if node.var_name in self.ctx.locals: del self.ctx.locals[node.var_name] if parentloop: self.ctx.locals['forloop'] = parentloop else: del self.ctx.locals['forloop'] for lst in oldmap.values(): for sub in lst: sub.remove() lst.clear() empty = not node.index_map if empty and node.else_template: for temp_child in node.else_template.children: self.build_node(temp_child, node) return False def _update_set_node(self, node: SetNode, recursive): node.variables.refresh() if node.scoped: self.ctx.scope.update(node.variables) else: self.ctx.locals.update(node.variables) if node.self_clear: node.empty() self._build_group(node.template, node) #del self.ctx.locals[node.var_name] return False else: return recursive NODE_UPDATERS: dict[str, Callable[[Self, RenderNode, bool], bool]] = { 'HTMLElement': _update_html_node, 'NSElement': _update_html_node, 'Context': _update_context_node, 'TextNode': _update_text_node, 'ConditionNode': _update_condition_node, 'LoopNode': _update_loop_node, 'SetNode': _update_set_node, 'ReactNode': lambda *args: True, } #endregion def update(self, node: RenderNode, recursive: bool = False): if self.NODE_UPDATERS[typename(node)](self, node, recursive): self.update_children(node)