Source code for pantra.components.template

from __future__ import annotations

import typing
from enum import Enum, IntEnum, auto
from pathlib import Path
import re
from dataclasses import dataclass, field
import ast

from pantra.common import UniNode
from pantra.settings import config
from pantra.compiler import CodeMetrics

if typing.TYPE_CHECKING:
    from typing import Self, Optional, Any
    from types import CodeType

    from pantra.session import Session

__all__ = ['HTMLTemplate', 'MacroCode', 'collect_styles', 'collect_template', 'NodeType', 'AttrType', 'MacroType',
           'get_template_path']

def is_expression_id(expr: str) -> str:
    # check expression is field "name"
    return is_expression_id.r.fullmatch(expr) is not None

is_expression_id.r = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]+")

def is_expression_canonical(expr: str) -> bool:
    # check expression is "some.field.name"
    return (is_expression_canonical.r.fullmatch(expr) is not None
        and not expr.startswith("this") and not expr.startswith("node"))

is_expression_canonical.r = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]+([.][a-zA-Z_][a-zA-Z0-9_]+)+$")

[docs] class NodeType(IntEnum): """Enumerated type of the node""" HTML_TAG = auto() TEMPLATE_TAG = auto() ROOT_NODE = auto() MACRO_IF = auto() MACRO_FOR = auto() MACRO_SET = auto() MACRO_INTERNAL = auto() AT_TEXT = auto() AT_COMPONENT = auto() AT_SLOT = auto() AT_PYTHON = auto() AT_SCRIPT = auto() AT_STYLE = auto() AT_REACT = auto() AT_EVENT = auto() @staticmethod def detect(tag_name: str) -> NodeType: if tag_name[0].islower(): return NodeType.HTML_TAG if tag_name[0].isupper(): return NodeType.TEMPLATE_TAG if tag_name[0] == '$': return NodeType.ROOT_NODE if tag_name[0] == '#': if tag_name == '#if': return NodeType.MACRO_IF if tag_name == '#for': return NodeType.MACRO_FOR if tag_name.startswith('#set'): return NodeType.MACRO_SET return NodeType.MACRO_INTERNAL if tag_name[0] == '@': return getattr(NodeType, 'AT_' + tag_name[1:].upper()) raise ValueError(f"Undetected TAG `{tag_name}`")
[docs] class AttrType(IntEnum): """Enumerated type of the attribute""" ATTR = auto() REF_NAME = auto() CREF_NAME = auto() SCOPE = auto() ON_RENDER = auto() ON_INIT = auto() CLASS_SWITCH = auto() DYNAMIC_STYLE = auto() BIND_VALUE = auto() DYNAMIC_SET = auto() DATA = auto() SRC = auto() HREF = auto() REACTIVE = auto() STYLE = auto() CLASS = auto() TYPE = auto() VALUE = auto() LOCALIZE = auto() SET_FALSE = auto() CONSUME = auto() DATA_NODE = auto() @staticmethod def detect(attr: str) -> tuple[AttrType, str | None, bool]: if ':' in attr: if attr.startswith('ref:'): name = attr.split(':')[1].strip() return AttrType.REF_NAME, name, False if attr.startswith('cref:'): name = attr.split(':')[1].strip() return AttrType.CREF_NAME, name, False if attr.startswith('scope:'): name = attr.split(':')[1].strip() return AttrType.SCOPE, name, False if attr == 'on:render': return AttrType.ON_RENDER, None, False if attr == 'on:init': return AttrType.ON_INIT, None, False if attr.startswith('class:'): cls = attr.split(':')[1].strip() return AttrType.CLASS_SWITCH, cls, True if attr.startswith('css:'): attr = attr.split(':')[1].strip() return AttrType.DYNAMIC_STYLE, attr, True if attr == 'bind:value': return AttrType.BIND_VALUE, None, False if attr.startswith('set:'): attr = attr.split(':')[1].strip() return AttrType.DYNAMIC_SET, attr, True if attr.startswith('data:'): attr = attr.split(':')[1].strip() return AttrType.DATA, attr, False if attr.startswith('src:'): attr = attr.split(':')[1].strip() return AttrType.SRC, attr, False if attr.startswith('href:'): attr = attr.split(':')[1].strip() return AttrType.HREF, attr, False if attr.startswith('not:'): attr = attr.split(':')[1].strip() return AttrType.SET_FALSE, attr, True else: if attr == 'reactive': return AttrType.REACTIVE, None, False if attr == 'style': return AttrType.STYLE, None, False if attr == 'class': return AttrType.CLASS, None, False if attr == 'type': return AttrType.TYPE, None, False if attr == 'value': return AttrType.VALUE, None, False if attr == 'localize': return AttrType.LOCALIZE, None, False if attr == 'consume': return AttrType.CONSUME, None, False if attr == 'data-node': return AttrType.DATA_NODE, None, False return AttrType.ATTR, attr, False
[docs] class MacroType(Enum): """Enumerated type of the macro""" STRING = auto() TEMPLATE = auto() BOOLEAN = auto() VALUE = auto() ITERATOR = auto() INDEX = auto()
class AstNamesVisitor(ast.NodeVisitor): def __init__(self): self.vars = set() def visit_Name(self, node): self.vars.add(node.id)
[docs] @dataclass(slots=True) class MacroCode: """Python code adaptation for scripts and expressions Attributes: type: type of this macro code reactive: marked as reactive evaluated: marked as evaluated src: source text code: compiled code in one of three forms vars: reactive variables list """ type: MacroType reactive: bool evaluated: bool code: CodeType | list[str] | str | None src: str vars: set[str] = field(init=False, repr=False) def __post_init__(self) -> None: visitor = AstNamesVisitor() visitor.visit(ast.parse(self.src)) self.vars = visitor.vars if is_expression_id(self.src): self.code = self.src elif is_expression_canonical(self.src): self.code = self.src.split('.')
def get_template_path(t: HTMLTemplate | CodeType) -> Path: if type(t) is HTMLTemplate: return t.filename.parent else: return Path(t.co_filename).parent
[docs] class HTMLTemplate(UniNode): """The rendering node template based on HTML-like source file Attributes: tag_name (str): tag name of the node node_type (NodeType): node type attributes (dict[str, ...]): node attributes attr_specs (dict[str, tuple[AttrType, str | None, bool]]): attribute specifications content (str | MacroCode | None): content of the node name (str): component name for the root node filename (Path): component source file name (for the root node) code (str | CodeType | None): compiled Python script code_metrics (CodeMetrics): compiled Python code metrics script_index (int): JavaScript index for renderer hex_digest (str): hexadecimal digest of the source text file for :ref:`observer <observer>` watchdog """ __slots__ = ('tag_name', 'node_type', 'attributes', 'attr_specs', 'content', 'name', 'filename', 'code', 'script_index', 'hex_digest', 'code_metrics') def __init__(self, tag_name: str, parent: Self | None = None, attributes: dict | None = None, text: str = None): super().__init__(parent) self.tag_name: str = tag_name self.node_type: NodeType = NodeType.detect(tag_name) self.attributes: dict[str, str | MacroCode | None] = attributes or {} self.attr_specs: dict[str, tuple[AttrType, str | None, bool]] = {k: AttrType.detect(k) for k in self.attributes} self.content: str | MacroCode | None = text self.name: str | None = None self.filename: Optional[Path] = None self.code: str | CodeType | None = None self.code_metrics: CodeMetrics = CodeMetrics() self.script_index: int = 0 self.hex_digest: str = '' def __str__(self): return self.tag_name def set_attr(self, attr_name: str, attr_value: Any): self.attributes[attr_name] = attr_value self.attr_specs[attr_name] = AttrType.detect(attr_name)
[docs] def collect_template(name: str, session: Optional[Session] = None, app: Optional[str] = None) -> Optional[HTMLTemplate]: """Use `DEFAULT_RENDERER` to collect template of the specified component name Arguments: name: name of the component session: Optional session instance, used for loading feedback messages app: Optional application instance if session is not defined """ return config.DEFAULT_RENDERER.collect_template(name, session, app)
[docs] def collect_styles(app:str, app_path: Path, error_callback: typing.Callable[[str], None]) -> str: """Collect and compile all CSS styles from all components Arguments: app: application name (needed to generate static file urls) app_path: application path (needed to find template files) error_callback: callback function to handle errors during parsing and compilation """ from .loader import load_styles styles = [] for file in app_path.glob('**/*.html'): if file == config.BOOTSTRAP_FILENAME: continue try: res = load_styles(app, file.stem, file, error_callback) except Exception as e: error_callback(f'{file}> Style collector> {e}') else: if res: styles.append(res) return '\n'.join(styles)