import configparser import os import re import gzip import subprocess import sys SRC_DIR = "src" CROSSPOINT_NAME = "CrossPoint Reader" PLACEHOLDERS = { "%%CROSSPOINT%%": CROSSPOINT_NAME, } def warn(msg: str) -> None: print(f"WARNING [build_html.py]: {msg}", file=sys.stderr) def get_base_version(project_dir: str) -> str: ini_path = os.path.join(project_dir, "platformio.ini") if not os.path.isfile(ini_path): warn(f"platformio.ini not found at {ini_path}; using 0.0.0") return "0.0.0" config = configparser.ConfigParser() config.read(ini_path) if not config.has_option("crosspoint", "version"): warn("No [crosspoint] section or version in platformio.ini; using 0.0.0") return "0.0.0" return config.get("crosspoint", "version") def get_git_branch(project_dir: str) -> str: try: branch = subprocess.check_output( ["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True, stderr=subprocess.PIPE, cwd=project_dir, ).strip() if branch == "HEAD": branch = subprocess.check_output( ["git", "rev-parse", "--short", "HEAD"], text=True, stderr=subprocess.PIPE, cwd=project_dir, ).strip() return "".join(c for c in branch if c not in '"\\') except FileNotFoundError: warn('git not found on PATH; branch suffix will be "unknown"') return "unknown" except subprocess.CalledProcessError as e: warn( f'git command failed (exit {e.returncode}): {e.stderr.strip()}; branch suffix will be "unknown"' ) return "unknown" except Exception as e: warn( f'Unexpected error reading git branch: {e}; branch suffix will be "unknown"' ) return "unknown" def get_version_string(project_dir: str) -> str: env_version = os.environ.get("CROSSPOINT_VERSION") if env_version: return env_version.strip('"') base_version = get_base_version(project_dir) pioenv = os.environ.get("PIOENV", "default") if pioenv == "default": branch = get_git_branch(project_dir) return f"{base_version}-dev+{branch}" return base_version def replace_placeholders(html: str, replacements: dict) -> str: for placeholder, replacement in replacements.items(): html = html.replace(placeholder, replacement) return html def strip_js_comments(js: str) -> str: """Remove JS comments while preserving string literals and URLs.""" result = [] i = 0 length = len(js) while i < length: # String literals — pass through unchanged if js[i] in ('"', "'", "`"): quote = js[i] result.append(js[i]) i += 1 while i < length: if js[i] == "\\" and i + 1 < length: result.append(js[i : i + 2]) i += 2 elif js[i] == quote: result.append(js[i]) i += 1 break else: result.append(js[i]) i += 1 # Block comment /* ... */ elif js[i] == "/" and i + 1 < length and js[i + 1] == "*": end = js.find("*/", i + 2) i = end + 2 if end != -1 else length # Line comment // ... elif js[i] == "/" and i + 1 < length and js[i + 1] == "/": end = js.find("\n", i) if end == -1: i = length else: # Keep the newline to preserve line structure result.append("\n") i = end + 1 # Regex literal — pass through unchanged # Heuristic: / after = ( , ; ! & | ? : [ { } ~ ^ or line start elif js[i] == "/" and i > 0: # Look back for operator context (skip whitespace) j = i - 1 while j >= 0 and js[j] in " \t": j -= 1 if j >= 0 and js[j] in "=(!,;:&|?[{}>~^+-*%": result.append(js[i]) i += 1 while i < length: if js[i] == "\\" and i + 1 < length: result.append(js[i : i + 2]) i += 2 elif js[i] == "/": result.append(js[i]) i += 1 # Regex flags while i < length and js[i].isalpha(): result.append(js[i]) i += 1 break elif js[i] == "[": # Character class — / doesn't end regex inside [] result.append(js[i]) i += 1 while i < length and js[i] != "]": if js[i] == "\\" and i + 1 < length: result.append(js[i : i + 2]) i += 2 else: result.append(js[i]) i += 1 else: result.append(js[i]) i += 1 else: result.append(js[i]) i += 1 else: result.append(js[i]) i += 1 return "".join(result) def minify_html(html: str) -> str: # Tags where whitespace should be preserved preserve_tags = ["pre", "code", "textarea"] script_style_tags = ["script", "style"] preserve_regex = "|".join(preserve_tags) script_style_regex = "|".join(script_style_tags) # Protect preserve blocks (pre/code/textarea) with placeholders preserve_blocks = [] def preserve(match): preserve_blocks.append(match.group(0)) return f"__PRESERVE_BLOCK_{len(preserve_blocks) - 1}__" html = re.sub( rf"<({preserve_regex})[\s\S]*?", preserve, html, flags=re.IGNORECASE ) # Strip JS/CSS comments inside