# Enhanced Toy Web Browser - With JavaScript/React Support and Music # run: pip install requests pillow urllib3 numpy simpleaudio # then this script import socket import tkinter as tk from tkinter import ttk, messagebox import struct import re import json import gzip import zlib import warnings from urllib3.exceptions import InsecureRequestWarning warnings.simplefilter('ignore', InsecureRequestWarning) import random import sys import traceback import urllib.parse import requests from tkinter.font import Font from PIL import Image, ImageTk import threading import time import io from collections import OrderedDict, defaultdict from concurrent.futures import ThreadPoolExecutor, as_completed from functools import lru_cache import queue import math # Try to import audio libraries try: import numpy as np HAS_NUMPY = True except ImportError: HAS_NUMPY = False print("Warning: numpy not installed. Music disabled. Install with: pip install numpy") try: import simpleaudio as sa HAS_SIMPLEAUDIO = True except ImportError: HAS_SIMPLEAUDIO = False print("Warning: simpleaudio not installed. Music disabled. Install with: pip install simpleaudio") # Try to import js2py for JavaScript execution try: import js2py HAS_JS2PY = True except ImportError: HAS_JS2PY = False ############################################################################### # CONSTANTS AND CONFIGURATION ############################################################################### DEFAULT_FONT_FAMILY = "Arial" DEFAULT_FONT_SIZE = 14 MAX_FONT_CACHE_SIZE = 100 CONNECTION_TIMEOUT = 5 READ_TIMEOUT = 15 USER_AGENT = "ToyBrowser/1.0 (Educational Project) AppleWebKit/537.36" REQUEST_DELAY = 0.1 LAST_REQUEST_TIME = {} HTML_ENTITIES = { 'nbsp': '\u00a0', 'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': "'", 'cent': '¢', 'pound': '£', 'yen': '¥', 'euro': '€', 'copy': '©', 'reg': '®', 'trade': '™', 'times': '×', 'divide': '÷', 'mdash': '—', 'ndash': '–', 'lsquo': ''', 'rsquo': ''', 'ldquo': '"', 'rdquo': '"', 'bull': '•', 'hellip': '…', 'larr': '←', 'rarr': '→', 'uarr': '↑', 'darr': '↓', } INLINE_ELEMENTS = frozenset({ "text", "span", "a", "b", "strong", "i", "em", "u", "small", "code", "mark", "img", "sub", "sup", "del", "ins", "abbr", "kbd", "q", "var", "s", "cite", "time", "font", "tt", "big", "strike", "nobr", "wbr", "bdo", "dfn", "samp", "label", "data", "ruby", "rt", "rp", "bdi", "output", "meter", "progress", "picture", "source", "slot" }) VOID_ELEMENTS = frozenset({ "br", "hr", "meta", "link", "img", "input", "area", "base", "col", "command", "embed", "keygen", "param", "source", "track", "wbr", "frame", "spacer", "basefont", "isindex" }) SKIP_ELEMENTS = frozenset({ "script", "style", "head", "meta", "link", "title", "noscript", "template" }) # Pre-compiled regex patterns RE_COMMENT = re.compile(r'', re.DOTALL) RE_DOCTYPE = re.compile(r']*>', re.IGNORECASE) RE_ENTITY_NAMED = re.compile(r'&([a-zA-Z]+);') RE_ENTITY_NUM = re.compile(r'&#(\d+);') RE_ENTITY_HEX = re.compile(r'&#[xX]([0-9a-fA-F]+);') RE_WHITESPACE = re.compile(r'[\r\n\t]+') RE_MULTI_SPACE = re.compile(r' +') RE_ATTR = re.compile(r'([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]*)))?') RE_CSS_COMMENT = re.compile(r'/\*.*?\*/', re.DOTALL) RE_TAG_ONLY = re.compile(r'^[a-zA-Z][a-zA-Z0-9]*$') RE_WORD_SPLIT = re.compile(r'(\S+|\s+)') ############################################################################### # MUSIC SYSTEM - Classical Music Synthesizer ############################################################################### class MusicPlayer: """Synthesizes and plays classical music for the Pentrix game""" # Note frequencies (Hz) NOTE_FREQS = { 'C2': 65.41, 'D2': 73.42, 'E2': 82.41, 'F2': 87.31, 'G2': 98.00, 'A2': 110.00, 'B2': 123.47, 'C3': 130.81, 'D3': 146.83, 'E3': 164.81, 'F3': 174.61, 'G3': 196.00, 'A3': 220.00, 'B3': 246.94, 'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23, 'F#4': 369.99, 'G4': 392.00, 'A4': 440.00, 'B4': 493.88, 'C5': 523.25, 'D5': 587.33, 'E5': 659.25, 'F5': 698.46, 'F#5': 739.99, 'G5': 783.99, 'A5': 880.00, 'B5': 987.77, } def __init__(self, sample_rate=22050): self.sample_rate = sample_rate self.is_playing = False self.play_thread = None self.stop_flag = threading.Event() self.current_playback = None self.music_enabled = HAS_NUMPY and HAS_SIMPLEAUDIO self.volume = 0.3 def note_to_freq(self, note): """Convert note name to frequency""" return self.NOTE_FREQS.get(note, 440.0) def generate_tone(self, freq, duration, wave_type='square', volume=0.3): """Generate a tone with the specified parameters""" if not HAS_NUMPY: return np.array([]) t = np.linspace(0, duration, int(self.sample_rate * duration), False) if wave_type == 'square': # Square wave (8-bit style) wave = np.sign(np.sin(2 * np.pi * freq * t)) elif wave_type == 'triangle': # Triangle wave (bass) wave = 2 * np.abs(2 * (t * freq - np.floor(t * freq + 0.5))) - 1 elif wave_type == 'sine': wave = np.sin(2 * np.pi * freq * t) else: wave = np.sin(2 * np.pi * freq * t) # Apply envelope (ADSR-like) attack = int(0.01 * self.sample_rate) decay = int(0.05 * self.sample_rate) release = int(0.05 * self.sample_rate) envelope = np.ones(len(wave)) if attack > 0 and attack < len(envelope): envelope[:attack] = np.linspace(0, 1, attack) if release > 0 and release < len(envelope): envelope[-release:] = np.linspace(1, 0, release) wave = wave * envelope * volume return wave def generate_bach_minuet(self): """Generate Bach's Minuet in G Major""" bpm = 120 beat_duration = 60.0 / bpm # Melody notes (simplified) melody = [ ('D5', 1), ('G4', 0.5), ('A4', 0.5), ('B4', 0.5), ('C5', 0.5), ('D5', 1), ('G4', 1), ('G4', 1), ('E5', 1), ('C5', 0.5), ('D5', 0.5), ('E5', 0.5), ('F#5', 0.5), ('G5', 1), ('G4', 1), ('G4', 1), ('C5', 1), ('D5', 0.5), ('C5', 0.5), ('B4', 0.5), ('A4', 0.5), ('B4', 1), ('C5', 0.5), ('B4', 0.5), ('A4', 0.5), ('G4', 0.5), ('F#4', 1), ('G4', 0.5), ('A4', 0.5), ('B4', 0.5), ('G4', 0.5), ('A4', 1), ('D4', 1), ('D4', 1), ] # Bass notes bass = [ ('G2', 1), ('G3', 1), ('G2', 1), ('G3', 1), ('G2', 1), ('B2', 1), ('C3', 1), ('D3', 1), ('E3', 1), ('D3', 1), ('G2', 1), ('G3', 1), ('A2', 1), ('B2', 1), ('G2', 1), ('G3', 1), ('D3', 1), ('C3', 1), ('D3', 1), ('D2', 1), ('G2', 1), ('D3', 1), ('D3', 1), ('D2', 1), ] return self._generate_piece(melody, bass, beat_duration) def generate_ode_to_joy(self): """Generate Beethoven's Ode to Joy""" bpm = 120 beat_duration = 60.0 / bpm melody = [ ('E5', 1), ('E5', 1), ('F5', 1), ('G5', 1), ('G5', 1), ('F5', 1), ('E5', 1), ('D5', 1), ('C5', 1), ('C5', 1), ('D5', 1), ('E5', 1), ('E5', 1.5), ('D5', 0.5), ('D5', 2), ('E5', 1), ('E5', 1), ('F5', 1), ('G5', 1), ('G5', 1), ('F5', 1), ('E5', 1), ('D5', 1), ('C5', 1), ('C5', 1), ('D5', 1), ('E5', 1), ('D5', 1.5), ('C5', 0.5), ('C5', 2), ] bass = [ ('C3', 2), ('G2', 2), ('C3', 2), ('G2', 2), ('C3', 2), ('G2', 2), ('C3', 2), ('G2', 2), ('C3', 2), ('G2', 2), ('C3', 2), ('G2', 2), ('G2', 2), ('C3', 2), ] return self._generate_piece(melody, bass, beat_duration) def generate_eine_kleine(self): """Generate Mozart's Eine Kleine Nachtmusik""" bpm = 130 beat_duration = 60.0 / bpm melody = [ ('G4', 0.5), ('D5', 0.5), ('D5', 1), ('G4', 0.5), ('D5', 0.5), ('D5', 1), ('G4', 0.5), ('D5', 0.5), ('G5', 0.5), ('F#5', 0.5), ('G5', 0.5), ('F#5', 0.5), ('G5', 0.5), ('D5', 0.5), ('B4', 0.5), ('G5', 0.5), ('G5', 1), ('B4', 0.5), ('G5', 0.5), ('G5', 1), ('B4', 0.5), ('G5', 0.5), ('B5', 0.5), ('A5', 0.5), ('B5', 0.5), ('A5', 0.5), ('B5', 0.5), ('G5', 0.5), ('D5', 1), ('D5', 0.5), ('E5', 0.5), ('F#5', 0.5), ('G5', 0.5), ('A5', 1), ('D5', 1), ('D5', 0.5), ('E5', 0.5), ('F#5', 0.5), ('G5', 0.5), ('A5', 1), ] bass = [ ('G2', 1), ('G3', 1), ('G2', 1), ('G3', 1), ('G2', 1), ('D3', 1), ('G2', 1), ('D3', 1), ('G2', 1), ('G3', 1), ('G2', 1), ('G3', 1), ('G2', 1), ('D3', 1), ('G2', 1), ('D3', 1), ('D3', 1), ('A2', 1), ('D3', 1), ('D2', 1), ('D3', 1), ('A2', 1), ('D3', 1), ('D2', 1), ] return self._generate_piece(melody, bass, beat_duration) def _generate_piece(self, melody, bass, beat_duration): """Generate audio for a piece with melody and bass""" if not HAS_NUMPY: return np.array([]) # Calculate total duration melody_duration = sum(dur for _, dur in melody) * beat_duration bass_duration = sum(dur for _, dur in bass) * beat_duration total_duration = max(melody_duration, bass_duration) # Generate melody track melody_audio = np.array([]) for note, dur in melody: freq = self.note_to_freq(note) tone = self.generate_tone(freq, dur * beat_duration, 'square', self.volume * 0.6) melody_audio = np.concatenate([melody_audio, tone]) # Generate bass track bass_audio = np.array([]) for note, dur in bass: freq = self.note_to_freq(note) tone = self.generate_tone(freq, dur * beat_duration, 'triangle', self.volume * 0.4) bass_audio = np.concatenate([bass_audio, tone]) # Mix tracks (pad to same length) max_len = max(len(melody_audio), len(bass_audio)) if len(melody_audio) < max_len: melody_audio = np.pad(melody_audio, (0, max_len - len(melody_audio))) if len(bass_audio) < max_len: bass_audio = np.pad(bass_audio, (0, max_len - len(bass_audio))) mixed = melody_audio + bass_audio # Normalize if np.max(np.abs(mixed)) > 0: mixed = mixed / np.max(np.abs(mixed)) * 0.8 return mixed def generate_full_soundtrack(self): """Generate the complete game soundtrack""" if not HAS_NUMPY: return np.array([]) parts = [] # Add each piece parts.append(self.generate_bach_minuet()) parts.append(np.zeros(int(self.sample_rate * 0.5))) # Brief pause parts.append(self.generate_ode_to_joy()) parts.append(np.zeros(int(self.sample_rate * 0.5))) parts.append(self.generate_eine_kleine()) return np.concatenate(parts) def start(self): """Start playing the soundtrack in a loop""" if not self.music_enabled: return if self.is_playing: return self.is_playing = True self.stop_flag.clear() def play_loop(): while not self.stop_flag.is_set(): try: # Generate soundtrack audio = self.generate_full_soundtrack() if len(audio) == 0: break # Convert to 16-bit audio audio_int = (audio * 32767).astype(np.int16) # Play audio self.current_playback = sa.play_buffer(audio_int, 1, 2, self.sample_rate) # Wait for playback to finish or stop signal while self.current_playback.is_playing() and not self.stop_flag.is_set(): time.sleep(0.1) if self.stop_flag.is_set(): if self.current_playback: self.current_playback.stop() break except Exception as e: print(f"Music playback error: {e}") break self.is_playing = False self.play_thread = threading.Thread(target=play_loop, daemon=True) self.play_thread.start() def stop(self): """Stop the soundtrack""" self.stop_flag.set() if self.current_playback: try: self.current_playback.stop() except: pass self.is_playing = False def toggle(self): """Toggle music on/off""" if self.is_playing: self.stop() return False else: self.start() return True ############################################################################### # 1) DNS + URL PARSING ############################################################################### DNS_CACHE = {} def resolve_hostname_dns(hostname, dns_server="8.8.8.8", port=53, timeout=2): hostname = hostname.strip().lower() if hostname in DNS_CACHE: return DNS_CACHE[hostname] try: socket.inet_aton(hostname) return hostname except OSError: pass tid = random.randint(0, 65535) header = struct.pack(">HHHHHH", tid, 0x0100, 1, 0, 0, 0) qname = b"".join(bytes([len(part)]) + part.encode("ascii") for part in hostname.split(".")) question = qname + b"\x00" + struct.pack(">HH", 1, 1) query = header + question s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(timeout) try: s.sendto(query, (dns_server, port)) data, _ = s.recvfrom(512) except: s.close() return None s.close() if struct.unpack(">H", data[:2])[0] != tid: return None idx = 12 while idx < len(data) and data[idx] != 0: idx += 1 idx += 5 while idx < len(data): if data[idx] & 0xC0 == 0xC0: idx += 2 else: while idx < len(data) and data[idx] != 0: idx += 1 idx += 1 if idx + 10 > len(data): break rtype, rclass, _, rdlength = struct.unpack(">HHIH", data[idx:idx+10]) idx += 10 if rtype == 1 and rclass == 1 and rdlength == 4: ip_addr = ".".join(map(str, data[idx:idx+4])) DNS_CACHE[hostname] = ip_addr return ip_addr idx += rdlength return None class ParsedURL: __slots__ = ['scheme', 'host', 'port', 'path', 'query', 'fragment'] def __init__(self, scheme="http", host="", port=80, path="/", query="", fragment=""): self.scheme = scheme self.host = host self.port = port self.path = path self.query = query self.fragment = fragment def __str__(self): url = f"{self.scheme}://{self.host}" if (self.scheme == "http" and self.port != 80) or \ (self.scheme == "https" and self.port != 443): url += f":{self.port}" url += self.path if self.query: url += f"?{self.query}" return url def full_path(self): return f"{self.path}?{self.query}" if self.query else self.path _URL_CACHE = {} def parse_url(url): if url in _URL_CACHE: cached = _URL_CACHE[url] return ParsedURL(cached.scheme, cached.host, cached.port, cached.path, cached.query, cached.fragment) url = url.strip() scheme = "http" if url.startswith("http://"): after, scheme = url[7:], "http" elif url.startswith("https://"): after, scheme = url[8:], "https" elif url.startswith("//"): after, scheme = url[2:], "https" else: after = url fragment = "" if "#" in after: after, fragment = after.split("#", 1) query = "" if "?" in after: after, query = after.split("?", 1) slash = after.find("/") if slash == -1: host_port, path = after, "/" else: host_port, path = after[:slash], after[slash:] or "/" if ":" in host_port: h, p = host_port.rsplit(":", 1) try: port, host = int(p), h except ValueError: host, port = host_port, 443 if scheme == "https" else 80 else: host = host_port port = 443 if scheme == "https" else 80 result = ParsedURL(scheme, host.strip().lower(), port, path, query, fragment) if len(_URL_CACHE) < 1000: _URL_CACHE[url] = result return result def resolve_url(base_url, relative_url): relative_url = relative_url.strip() if not relative_url: return base_url if relative_url.startswith(("http://", "https://")): return parse_url(relative_url) if relative_url.startswith("//"): return parse_url(f"{base_url.scheme}:{relative_url}") if relative_url.startswith("/"): qpart = relative_url.split("?", 1) return ParsedURL(base_url.scheme, base_url.host, base_url.port, qpart[0], qpart[1] if len(qpart) > 1 else "", "") if relative_url.startswith("#"): return ParsedURL(base_url.scheme, base_url.host, base_url.port, base_url.path, base_url.query, relative_url[1:]) if relative_url.startswith("?"): return ParsedURL(base_url.scheme, base_url.host, base_url.port, base_url.path, relative_url[1:], "") base_dir = base_url.path.rsplit("/", 1)[0] if "/" in base_url.path else "" new_path = f"{base_dir}/{relative_url}" parts = new_path.split("/") normalized = [] for part in parts: if part == "..": if normalized and normalized[-1] != "": normalized.pop() elif part != ".": normalized.append(part) final_path = "/".join(normalized) if not final_path.startswith("/"): final_path = "/" + final_path qpart = final_path.split("?", 1) return ParsedURL(base_url.scheme, base_url.host, base_url.port, qpart[0], qpart[1] if len(qpart) > 1 else "", "") ############################################################################### # 2) HTTP - With connection pooling and parallel requests ############################################################################### _session = requests.Session() _session.verify = False adapter = requests.adapters.HTTPAdapter( pool_connections=10, pool_maxsize=20, max_retries=1 ) _session.mount('http://', adapter) _session.mount('https://', adapter) HTTP_EXECUTOR = ThreadPoolExecutor(max_workers=8) RESPONSE_CACHE = {} RESPONSE_CACHE_MAX = 100 def http_request(url_obj, method="GET", headers=None, body="", max_redirects=10): if headers is None: headers = {} cur_url, cur_method, cur_body = url_obj, method, body for _ in range(max_redirects): r_headers, r_body, r_url = _single_http_request(cur_url, cur_method, headers, cur_body) status_code = int(r_headers.get(":status_code", "0")) if status_code in (301, 302, 303, 307, 308): location = r_headers.get("location", "") if not location: return r_headers, r_body, r_url cur_url = resolve_url(cur_url, location) if status_code in (302, 303): cur_method, cur_body = "GET", "" else: return r_headers, r_body, r_url return r_headers, r_body, r_url def http_request_parallel(url_objs, method="GET", headers=None): if headers is None: headers = {} futures = {} for url_obj in url_objs: future = HTTP_EXECUTOR.submit(_single_http_request, url_obj, method, headers, "") futures[future] = url_obj results = {} for future in as_completed(futures, timeout=READ_TIMEOUT): url_obj = futures[future] try: results[str(url_obj)] = future.result() except Exception: results[str(url_obj)] = ({}, b"", url_obj) return results def _single_http_request(url_obj, method="GET", headers=None, body=""): cache_key = (str(url_obj), method) if method == "GET" and cache_key in RESPONSE_CACHE: return RESPONSE_CACHE[cache_key] if url_obj.scheme == "https": result = _requests_https(url_obj, method, headers, body) else: result = _raw_http(url_obj, method, headers, body) if method == "GET" and len(RESPONSE_CACHE) < RESPONSE_CACHE_MAX: RESPONSE_CACHE[cache_key] = result return result def _requests_https(url_obj, method="GET", headers=None, body=""): if headers is None: headers = {} host = url_obj.host now = time.time() if host in LAST_REQUEST_TIME: elapsed = now - LAST_REQUEST_TIME[host] if elapsed < REQUEST_DELAY: time.sleep(REQUEST_DELAY - elapsed) LAST_REQUEST_TIME[host] = time.time() final_h = { "User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", } final_h.update({k: v for k, v in headers.items() if k.lower() not in ["host", "content-length"]}) try: resp = _session.request( method=method, url=str(url_obj), headers=final_h, data=body.encode("utf-8") if body else None, allow_redirects=False, timeout=(CONNECTION_TIMEOUT, READ_TIMEOUT) ) r_h = {":status_code": str(resp.status_code)} r_h.update({k.lower(): v for k, v in resp.headers.items()}) return r_h, resp.content, url_obj except Exception as e: raise Exception(f"HTTPS request failed: {e}") def _raw_http(url_obj, method="GET", headers=None, body=""): if headers is None: headers = {} host = url_obj.host now = time.time() if host in LAST_REQUEST_TIME: elapsed = now - LAST_REQUEST_TIME[host] if elapsed < REQUEST_DELAY: time.sleep(REQUEST_DELAY - elapsed) LAST_REQUEST_TIME[host] = time.time() ip_addr = resolve_hostname_dns(url_obj.host) if not ip_addr: raise Exception(f"DNS fail => {url_obj.host}") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(READ_TIMEOUT) try: sock.connect((ip_addr, url_obj.port)) except Exception as e: sock.close() raise Exception(f"Connection failed: {e}") lines = [ f"{method} {url_obj.full_path()} HTTP/1.1", f"Host: {url_obj.host}", f"User-Agent: {USER_AGENT}", "Accept: text/html,*/*", "Accept-Encoding: gzip, deflate", "Connection: close", f"Content-Length: {len(body)}", ] lines.extend(f"{k}: {v}" for k, v in headers.items() if k.lower() not in ["host", "connection", "user-agent"]) req_str = "\r\n".join(lines) + "\r\n\r\n" + body try: sock.sendall(req_str.encode("utf-8")) chunks = [] while True: try: chunk = sock.recv(32768) if not chunk: break chunks.append(chunk) except socket.timeout: break response = b"".join(chunks) finally: sock.close() hd_end = response.find(b"\r\n\r\n") if hd_end == -1: return {}, b"", url_obj raw_header = response[:hd_end].decode("utf-8", "replace") raw_body = response[hd_end+4:] lines = raw_header.split("\r\n") parts = lines[0].split(" ", 2) headers_dict = {":status_code": parts[1]} if len(parts) >= 2 else {} for line in lines[1:]: if ":" in line: kk, vv = line.split(":", 1) headers_dict[kk.strip().lower()] = vv.strip() te = headers_dict.get("transfer-encoding", "").lower() if "chunked" in te: raw_body = decode_chunked_body(raw_body) ce = headers_dict.get("content-encoding", "").lower() if "gzip" in ce: try: raw_body = gzip.decompress(raw_body) except: pass elif "deflate" in ce: try: raw_body = zlib.decompress(raw_body, -zlib.MAX_WBITS) except: try: raw_body = zlib.decompress(raw_body) except: pass return headers_dict, raw_body, url_obj def decode_chunked_body(rb): i, decoded = 0, [] while True: newline = rb.find(b"\r\n", i) if newline == -1: break try: chunk_size = int(rb[i:newline].decode("utf-8", "replace").split(";")[0].strip(), 16) except: chunk_size = 0 if chunk_size == 0: break i = newline + 2 decoded.append(rb[i:i+chunk_size]) i += chunk_size if rb[i:i+2] == b"\r\n": i += 2 return b"".join(decoded) ############################################################################### # 3) HTML PARSING - Optimized ############################################################################### _ENTITY_CACHE = {} def decode_html_entities(text): if text in _ENTITY_CACHE: return _ENTITY_CACHE[text] original = text def replace_named(m): return HTML_ENTITIES.get(m.group(1), m.group(0)) text = RE_ENTITY_NAMED.sub(replace_named, text) text = RE_ENTITY_NUM.sub(lambda m: chr(int(m.group(1))) if int(m.group(1)) < 65536 else m.group(0), text) text = RE_ENTITY_HEX.sub(lambda m: chr(int(m.group(1), 16)) if int(m.group(1), 16) < 65536 else m.group(0), text) if len(_ENTITY_CACHE) < 10000: _ENTITY_CACHE[original] = text return text class DOMNode: __slots__ = [ 'tag_name', 'attributes', 'children', 'parent', 'text', 'styles', 'computed_styles', 'inline_css', 'script_code', 'is_form', 'method', 'action', 'form_fields', 'is_inline', 'id', 'classes', 'event_handlers' ] def __init__(self, tag_name="document", parent=None): self.tag_name = tag_name.lower() if tag_name else "document" self.attributes = {} self.children = [] self.parent = parent self.text = "" self.styles = {} self.computed_styles = {} self.inline_css = "" self.script_code = "" self.is_form = (self.tag_name == "form") self.method = "get" self.action = "" self.form_fields = {} self.is_inline = (self.tag_name in INLINE_ELEMENTS) self.id = "" self.classes = [] self.event_handlers = {} def get_text_content(self): if self.tag_name == "text": return self.text return "".join(c.get_text_content() for c in self.children) def parse_html(html_text): html_text = RE_COMMENT.sub('', html_text) html_text = RE_DOCTYPE.sub('', html_text) root = DOMNode("document") current = root text_buffer = [] i, length = 0, len(html_text) def flush_text(): nonlocal text_buffer, current if not text_buffer: return raw = "".join(text_buffer) text_buffer = [] raw = decode_html_entities(raw) if current.tag_name not in ("pre", "code", "textarea"): raw = RE_WHITESPACE.sub(' ', raw) raw = RE_MULTI_SPACE.sub(' ', raw) if raw and (raw.strip() or (raw == ' ' and current.children)): n = DOMNode("text", current) n.text = raw current.children.append(n) while i < length: if html_text[i] == "<": if html_text[i:i+2] == "", i) i = close_i + 1 if close_i != -1 else length continue flush_text() close_i = html_text.find(">", i) if close_i == -1: break tag_content = html_text[i+1:close_i].strip() if tag_content.startswith("/"): close_tag = tag_content[1:].split()[0].lower() temp = current while temp and temp.tag_name != close_tag: temp = temp.parent if temp and temp.parent: current = temp.parent elif current.parent: current = current.parent i = close_i + 1 continue space_idx = next((idx for idx, c in enumerate(tag_content) if c.isspace()), None) if space_idx: tag_name = tag_content[:space_idx].lower().rstrip("/") attr_string = tag_content[space_idx:].rstrip("/") else: tag_name = tag_content.lower().rstrip("/") attr_string = "" if not tag_name or tag_name.startswith("!"): i = close_i + 1 continue if tag_name == "body": while current and current.tag_name in ("head", "title", "meta", "link", "style", "script"): if current.parent: current = current.parent else: break elif tag_name == "li" and current.tag_name == "li": if current.parent: current = current.parent elif tag_name == "p" and current.tag_name == "p": if current.parent: current = current.parent elif tag_name in ("dt", "dd") and current.tag_name in ("dt", "dd"): if current.parent: current = current.parent elif tag_name in ("div", "table", "form", "ul", "ol", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre") and current.tag_name == "p": if current.parent: current = current.parent nd = DOMNode(tag_name, current) for m in RE_ATTR.finditer(attr_string): name = m.group(1).lower() value = m.group(2) or m.group(3) or m.group(4) or "" nd.attributes[name] = decode_html_entities(value) nd.id = nd.attributes.get("id", "") if "class" in nd.attributes: nd.classes = nd.attributes["class"].split() if "style" in nd.attributes: for part in nd.attributes["style"].split(";"): if ":" in part: p, v = part.split(":", 1) nd.styles[p.strip().lower()] = v.strip() nd.event_handlers = {k[2:]: v for k, v in nd.attributes.items() if k.startswith("on")} if "bgcolor" in nd.attributes: nd.styles["background-color"] = nd.attributes["bgcolor"] if "color" in nd.attributes and tag_name == "font": nd.styles["color"] = nd.attributes["color"] if "align" in nd.attributes: nd.styles["text-align"] = nd.attributes["align"] if "valign" in nd.attributes: nd.styles["vertical-align"] = nd.attributes["valign"] if "width" in nd.attributes and tag_name in ("table", "td", "th", "img"): w = nd.attributes["width"] nd.styles["width"] = w if w.endswith(("%", "px")) else w + "px" if "height" in nd.attributes and tag_name in ("table", "td", "th", "img", "tr"): h = nd.attributes["height"] nd.styles["height"] = h if h.endswith(("%", "px")) else h + "px" if tag_name == "form": nd.is_form = True nd.method = nd.attributes.get("method", "get").lower() nd.action = nd.attributes.get("action", "") current.children.append(nd) is_self_closing = tag_content.endswith("/") or tag_name in VOID_ELEMENTS if tag_name == "input": nm = nd.attributes.get("name", "") val = nd.attributes.get("value", "") fa = current while fa and not fa.is_form: fa = fa.parent if fa and nm: fa.form_fields[nm] = [val, nd] if is_self_closing: i = close_i + 1 continue if tag_name in ("title", "textarea", "style", "script"): close_tag = f"" close_t = html_text.lower().find(close_tag, close_i+1) if close_t == -1: i = length continue content = html_text[close_i+1:close_t] if tag_name == "title": nd.text = decode_html_entities(content) elif tag_name == "textarea": nd.text = decode_html_entities(content) nm = nd.attributes.get("name", "") fa = current while fa and not fa.is_form: fa = fa.parent if fa and nm: fa.form_fields[nm] = [nd.text, nd] elif tag_name == "style": nd.inline_css = content elif tag_name == "script": nd.script_code = content i = close_t + len(close_tag) continue current = nd i = close_i + 1 else: text_buffer.append(html_text[i]) i += 1 flush_text() return root ############################################################################### # 4) CSS PARSING - With rule indexing ############################################################################### class CSSRule: __slots__ = ['selector', 'properties', 'specificity', 'tag', 'id_sel', 'class_sel'] def __init__(self, selector, properties): self.selector = selector.strip() self.properties = properties self.specificity = ( selector.count("#"), selector.count("."), len(re.findall(r'(?:^|[\s>+~])([a-zA-Z][a-zA-Z0-9]*)', selector)) ) sel = self.selector.split()[-1] if " " in self.selector else self.selector self.tag = None self.id_sel = None self.class_sel = None if sel.startswith("#"): self.id_sel = sel[1:].split(".")[0] elif sel.startswith("."): self.class_sel = sel[1:] elif RE_TAG_ONLY.match(sel): self.tag = sel.lower() class CSSIndex: __slots__ = ['by_tag', 'by_id', 'by_class', 'universal'] def __init__(self, rules): self.by_tag = defaultdict(list) self.by_id = defaultdict(list) self.by_class = defaultdict(list) self.universal = [] for rule in rules: if rule.tag: self.by_tag[rule.tag].append(rule) elif rule.id_sel: self.by_id[rule.id_sel].append(rule) elif rule.class_sel: self.by_class[rule.class_sel].append(rule) else: self.universal.append(rule) def get_matching_rules(self, node): candidates = list(self.universal) candidates.extend(self.by_tag.get(node.tag_name, [])) if node.id: candidates.extend(self.by_id.get(node.id, [])) for cls in node.classes: candidates.extend(self.by_class.get(cls, [])) return candidates def parse_css(css_text): rules = [] css_text = RE_CSS_COMMENT.sub('', css_text) css_text = re.sub(r'@media[^{]+\{', '{', css_text) css_text = re.sub(r'@supports[^{]+\{', '{', css_text) css_text = re.sub(r'@keyframes[^{]+\{[^}]*\}[^}]*\}', '', css_text) css_text = re.sub(r'@-webkit-keyframes[^{]+\{[^}]*\}[^}]*\}', '', css_text) css_text = re.sub(r'@font-face[^}]+\}', '', css_text) css_text = re.sub(r'@[a-z-]+[^;{]+[;{]', '', css_text) i = 0 while i < len(css_text): bo = css_text.find("{", i) if bo == -1: break sel_text = css_text[i:bo].strip() depth, bc = 1, bo + 1 while bc < len(css_text) and depth > 0: if css_text[bc] == '{': depth += 1 elif css_text[bc] == '}': depth -= 1 bc += 1 bc -= 1 if bc <= bo: i = bo + 1 continue block = css_text[bo+1:bc].strip() i = bc + 1 props = {} for decl in block.split(";"): if ":" in decl: colon = decl.find(":") prop = decl[:colon].strip().lower() val = decl[colon+1:].strip() val = re.sub(r'\s*!important\s*$', '', val, flags=re.IGNORECASE) if not prop.startswith(("-", "--")) and not val.startswith("var("): props[prop] = val for s in sel_text.split(","): s = s.strip() if s and not s.startswith("@") and "::" not in s: s_clean = re.sub(r':[a-zA-Z-]+(\([^)]*\))?', '', s) if s_clean.strip(): rules.append(CSSRule(s_clean.strip(), dict(props))) return rules _SELECTOR_CACHE = {} def selector_matches(sel, node): cache_key = (sel, id(node)) if cache_key in _SELECTOR_CACHE: return _SELECTOR_CACHE[cache_key] result = _selector_matches_impl(sel, node) if len(_SELECTOR_CACHE) < 50000: _SELECTOR_CACHE[cache_key] = result return result def _selector_matches_impl(sel, node): sel = sel.strip() tag = node.tag_name if RE_TAG_ONLY.match(sel): return sel.lower() == tag if sel.startswith("#"): return node.id == sel[1:].split(".")[0] if sel.startswith("."): return sel[1:] in node.classes if sel == "*": return True if " " in sel: parts = sel.split() if len(parts) >= 2 and selector_matches(parts[-1], node): p = node.parent ancestor_sel = " ".join(parts[:-1]) while p: if selector_matches(ancestor_sel, p): return True p = p.parent return False if "." in sel and not sel.startswith("."): parts = sel.split(".") elem = parts[0] if elem and elem.lower() != tag: return False return all(c in node.classes for c in parts[1:] if c) return False def apply_css_rules_indexed(node, css_index): candidates = css_index.get_matching_rules(node) matched = [] for r in candidates: if selector_matches(r.selector, node): matched.append((r.specificity, r)) matched.sort(key=lambda x: x[0]) for _, r in matched: node.styles.update(r.properties) for c in node.children: apply_css_rules_indexed(c, css_index) _PX_CACHE = {} def _px_to_int(v, default=0, base=None, viewport_width=1000, viewport_height=800): cache_key = (v, default, base) if cache_key in _PX_CACHE: return _PX_CACHE[cache_key] try: s = str(v).strip().lower() if not s or s in ('auto', 'none'): result = default elif ' ' in s: s = s.split()[0] result = _px_to_int_impl(s, default, base, viewport_width, viewport_height) else: result = _px_to_int_impl(s, default, base, viewport_width, viewport_height) except: result = default if len(_PX_CACHE) < 10000: _PX_CACHE[cache_key] = result return result def _px_to_int_impl(s, default, base, viewport_width, viewport_height): if s.endswith("%") and base is not None: return int(float(s[:-1]) * 0.01 * base) if s.endswith("px"): return int(float(s[:-2])) if s.endswith("pt"): return int(float(s[:-2]) * 1.33) if s.endswith("em"): return int(float(s[:-2]) * 16) if s.endswith("rem"): return int(float(s[:-3]) * 16) if s.endswith("vw"): return int(float(s[:-2]) * viewport_width / 100) if s.endswith("vh"): return int(float(s[:-2]) * viewport_height / 100) return int(float(s)) _COLOR_CACHE = {} _NAMED_COLORS = { 'black': '#000000', 'white': '#ffffff', 'red': '#ff0000', 'green': '#008000', 'blue': '#0000ff', 'yellow': '#ffff00', 'gray': '#808080', 'grey': '#808080', 'orange': '#ffa500', 'purple': '#800080', 'pink': '#ffc0cb', 'brown': '#a52a2a', 'cyan': '#00ffff', 'magenta': '#ff00ff', 'lime': '#00ff00', 'navy': '#000080', 'teal': '#008080', 'olive': '#808000', 'maroon': '#800000', 'silver': '#c0c0c0', 'aqua': '#00ffff', 'fuchsia': '#ff00ff', 'darkgray': '#a9a9a9', 'darkgrey': '#a9a9a9', 'lightgray': '#d3d3d3', 'lightgrey': '#d3d3d3', 'dimgray': '#696969', 'gold': '#ffd700', 'coral': '#ff7f50', 'tomato': '#ff6347', 'salmon': '#fa8072', 'wheat': '#f5deb3', 'beige': '#f5f5dc', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'violet': '#ee82ee', 'indigo': '#4b0082', 'crimson': '#dc143c', 'chocolate': '#d2691e', } def parse_color(color_str): if not color_str: return None if color_str in _COLOR_CACHE: return _COLOR_CACHE[color_str] original = color_str color_str = color_str.strip().lower() if color_str in ('transparent', 'inherit', 'initial', 'unset', 'currentcolor', 'none'): return None result = None if color_str in _NAMED_COLORS: result = _NAMED_COLORS[color_str] elif color_str.startswith('#'): if len(color_str) == 4: result = f"#{color_str[1]*2}{color_str[2]*2}{color_str[3]*2}" elif len(color_str) >= 7: result = color_str[:7] else: result = color_str elif color_str.startswith('rgb'): m = re.match(r'rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)', color_str) if m: r, g, b = int(m.group(1)), int(m.group(2)), int(m.group(3)) result = f"#{r:02x}{g:02x}{b:02x}" _COLOR_CACHE[original] = result return result def is_color_visible(color, bg_color="#ffffff"): if not color or color in ('transparent', 'inherit', 'none'): return False try: if color.startswith('#') and len(color) >= 7: r = int(color[1:3], 16) g = int(color[3:5], 16) b = int(color[5:7], 16) if r > 250 and g > 250 and b > 250: return False if (r * 299 + g * 587 + b * 114) / 1000 > 245: return False return True except: return True def compute_styles(node, parent_computed=None): defaults = { "color": "black", "background-color": "transparent", "font-size": f"{DEFAULT_FONT_SIZE}px", "font-weight": "normal", "font-style": "normal", "font-family": DEFAULT_FONT_FAMILY, "text-decoration": "none", "display": "inline" if node.is_inline else "block", "margin-top": "0", "margin-right": "0", "margin-bottom": "0", "margin-left": "0", "padding-top": "0", "padding-right": "0", "padding-bottom": "0", "padding-left": "0", "text-align": "left", "vertical-align": "baseline", "white-space": "normal", "visibility": "visible", "opacity": "1" } if parent_computed: for p in ("color", "font-size", "font-family", "font-weight", "font-style", "text-align", "visibility"): if p in parent_computed: defaults[p] = parent_computed[p] for key, val in node.styles.items(): if key == "display": if val == "none": defaults["display"] = "none" elif val in ("block", "flex", "grid", "table", "list-item", "inline-block"): defaults["display"] = "block" elif val == "inline": defaults["display"] = "inline" elif key == "visibility" and val == "hidden": defaults["visibility"] = "hidden" elif key == "color": parsed = parse_color(val) if parsed and is_color_visible(parsed): defaults["color"] = parsed elif key in ("background", "background-color"): bg_val = val.split()[0] if val else "" parsed = parse_color(bg_val) if parsed: defaults["background-color"] = parsed elif key == "opacity": try: if float(val) >= 0.1: defaults["opacity"] = val except: pass elif key in defaults: defaults[key] = val for prop in ("margin", "padding"): if prop in node.styles: parts = node.styles[prop].split() dirs = ["top", "right", "bottom", "left"] if len(parts) == 1: for d in dirs: defaults[f"{prop}-{d}"] = parts[0] elif len(parts) == 2: defaults[f"{prop}-top"] = defaults[f"{prop}-bottom"] = parts[0] defaults[f"{prop}-right"] = defaults[f"{prop}-left"] = parts[1] elif len(parts) == 3: defaults[f"{prop}-top"] = parts[0] defaults[f"{prop}-right"] = defaults[f"{prop}-left"] = parts[1] defaults[f"{prop}-bottom"] = parts[2] elif len(parts) >= 4: for i, d in enumerate(dirs): defaults[f"{prop}-{d}"] = parts[i] t = node.tag_name if t in ("h1", "h2", "h3", "h4", "h5", "h6"): defaults["font-weight"] = "bold" sizes = {"h1": "28px", "h2": "24px", "h3": "20px", "h4": "18px", "h5": "16px", "h6": "14px"} defaults["font-size"] = sizes.get(t, "16px") defaults["margin-top"] = defaults["margin-bottom"] = "15px" elif t in ("b", "strong"): defaults["font-weight"] = "bold" elif t in ("i", "em"): defaults["font-style"] = "italic" elif t == "u": defaults["text-decoration"] = "underline" elif t == "a": if defaults["color"] == "black" or not is_color_visible(defaults["color"]): defaults["color"] = "#0000EE" elif t == "th": defaults["font-weight"] = "bold" defaults["text-align"] = "center" elif t == "p": if _px_to_int(defaults["margin-bottom"]) < 10: defaults["margin-bottom"] = "10px" elif t in ("code", "pre", "tt"): defaults["font-family"] = "Courier New" elif t in ("ul", "ol"): defaults["margin-top"] = "10px" defaults["margin-bottom"] = "10px" defaults["padding-left"] = "30px" elif t == "li": defaults["margin-bottom"] = "5px" if not is_color_visible(defaults.get("color")): defaults["color"] = "black" node.computed_styles = defaults for c in node.children: compute_styles(c, defaults) ############################################################################### # 5) PENTRIX GAME ENGINE - Native Implementation with Music ############################################################################### class PentrixGame: """Native Python implementation of the Pentrix game with music""" BOARD_WIDTH = 12 BOARD_HEIGHT = 20 CELL_SIZE = 18 PENTOMINOES = { 'F': {'shape': [[0,1,1],[1,1,0],[0,1,0]], 'color': '#ff3d00'}, 'I': {'shape': [[1,1,1,1,1]], 'color': '#00f5ff'}, 'L': {'shape': [[1,0],[1,0],[1,0],[1,1]], 'color': '#ff9100'}, 'N': {'shape': [[0,1],[1,1],[1,0],[1,0]], 'color': '#ff00aa'}, 'P': {'shape': [[1,1],[1,1],[1,0]], 'color': '#aa00ff'}, 'T': {'shape': [[1,1,1],[0,1,0],[0,1,0]], 'color': '#bf00ff'}, 'U': {'shape': [[1,0,1],[1,1,1]], 'color': '#ffea00'}, 'V': {'shape': [[1,0,0],[1,0,0],[1,1,1]], 'color': '#00ff5f'}, 'W': {'shape': [[1,0,0],[1,1,0],[0,1,1]], 'color': '#00ff99'}, 'X': {'shape': [[0,1,0],[1,1,1],[0,1,0]], 'color': '#ff5577'}, 'Y': {'shape': [[0,1],[1,1],[0,1],[0,1]], 'color': '#0066ff'}, 'Z': {'shape': [[1,1,0],[0,1,0],[0,1,1]], 'color': '#77ff00'}, } def __init__(self, canvas, root, status_callback=None): self.canvas = canvas self.root = root self.status_callback = status_callback self.board = self.create_empty_board() self.current_piece = None self.position = {'x': 0, 'y': 0} self.next_piece = None self.score = 0 self.lines = 0 self.level = 1 self.game_over = False self.is_paused = False self.game_started = False self.game_loop_id = None self.cell_ids = [] self.ui_elements = {} # Music player self.music_player = MusicPlayer() self.music_on = True # Touch handling self.touch_start = {'x': 0, 'y': 0} self.touch_move = {'x': 0, 'y': 0} self.has_moved = False self.setup_ui() self.bind_events() def create_empty_board(self): return [[None for _ in range(self.BOARD_WIDTH)] for _ in range(self.BOARD_HEIGHT)] def random_pentomino(self): key = random.choice(list(self.PENTOMINOES.keys())) p = self.PENTOMINOES[key] return {'shape': [row[:] for row in p['shape']], 'color': p['color'], 'type': key} def rotate(self, matrix): rows = len(matrix) cols = len(matrix[0]) rotated = [[0 for _ in range(rows)] for _ in range(cols)] for r in range(rows): for c in range(cols): rotated[c][rows - 1 - r] = matrix[r][c] return rotated def check_collision(self, piece, pos): if not piece: return False for r, row in enumerate(piece['shape']): for c, cell in enumerate(row): if cell: new_x = pos['x'] + c new_y = pos['y'] + r if new_x < 0 or new_x >= self.BOARD_WIDTH or new_y >= self.BOARD_HEIGHT: return True if new_y >= 0 and self.board[new_y][new_x]: return True return False def merge_piece(self): if not self.current_piece: return for r, row in enumerate(self.current_piece['shape']): for c, cell in enumerate(row): if cell: new_y = self.position['y'] + r new_x = self.position['x'] + c if 0 <= new_y < self.BOARD_HEIGHT and 0 <= new_x < self.BOARD_WIDTH: self.board[new_y][new_x] = self.current_piece['color'] def clear_lines(self): lines_cleared = 0 new_board = [] for row in self.board: if not all(cell is not None for cell in row): new_board.append(row) else: lines_cleared += 1 while len(new_board) < self.BOARD_HEIGHT: new_board.insert(0, [None for _ in range(self.BOARD_WIDTH)]) self.board = new_board return lines_cleared def spawn_piece(self): piece = self.next_piece or self.random_pentomino() start_x = (self.BOARD_WIDTH - len(piece['shape'][0])) // 2 start_y = 0 self.next_piece = self.random_pentomino() if self.check_collision(piece, {'x': start_x, 'y': start_y}): self.game_over = True self.music_player.stop() self.render() return self.current_piece = piece self.position = {'x': start_x, 'y': start_y} def move_down(self): if not self.current_piece or self.game_over or self.is_paused: return new_pos = {'x': self.position['x'], 'y': self.position['y'] + 1} if self.check_collision(self.current_piece, new_pos): self.merge_piece() lines_cleared = self.clear_lines() if lines_cleared > 0: points = [0, 100, 300, 500, 800, 1000][min(lines_cleared, 5)] * self.level self.score += points self.lines += lines_cleared self.level = self.lines // 10 + 1 self.current_piece = None self.spawn_piece() else: self.position = new_pos self.render() def move_left(self): if not self.current_piece or self.game_over or self.is_paused: return new_pos = {'x': self.position['x'] - 1, 'y': self.position['y']} if not self.check_collision(self.current_piece, new_pos): self.position = new_pos self.render() def move_right(self): if not self.current_piece or self.game_over or self.is_paused: return new_pos = {'x': self.position['x'] + 1, 'y': self.position['y']} if not self.check_collision(self.current_piece, new_pos): self.position = new_pos self.render() def rotate_piece(self): if not self.current_piece or self.game_over or self.is_paused: return rotated = {'shape': self.rotate(self.current_piece['shape']), 'color': self.current_piece['color'], 'type': self.current_piece['type']} if not self.check_collision(rotated, self.position): self.current_piece = rotated else: for offset in [-1, 1, -2, 2]: new_pos = {'x': self.position['x'] + offset, 'y': self.position['y']} if not self.check_collision(rotated, new_pos): self.current_piece = rotated self.position = new_pos break self.render() def hard_drop(self): if not self.current_piece or self.game_over or self.is_paused: return new_y = self.position['y'] while not self.check_collision(self.current_piece, {'x': self.position['x'], 'y': new_y + 1}): new_y += 1 self.position['y'] = new_y self.merge_piece() lines_cleared = self.clear_lines() if lines_cleared > 0: points = [0, 100, 300, 500, 800, 1000][min(lines_cleared, 5)] * self.level self.score += points self.lines += lines_cleared self.level = self.lines // 10 + 1 self.current_piece = None self.spawn_piece() self.render() def get_ghost_position(self): if not self.current_piece: return None ghost_y = self.position['y'] while not self.check_collision(self.current_piece, {'x': self.position['x'], 'y': ghost_y + 1}): ghost_y += 1 return {'x': self.position['x'], 'y': ghost_y} def start_game(self): self.board = self.create_empty_board() self.score = 0 self.lines = 0 self.level = 1 self.game_over = False self.is_paused = False self.game_started = True self.next_piece = self.random_pentomino() self.current_piece = None self.spawn_piece() self.start_game_loop() # Start music if self.music_on: self.music_player.start() self.render() def toggle_pause(self): if not self.game_over and self.game_started: self.is_paused = not self.is_paused if self.is_paused: self.music_player.stop() elif self.music_on: self.music_player.start() if not self.is_paused: self.start_game_loop() self.render() def toggle_music(self): self.music_on = not self.music_on if self.music_on and self.game_started and not self.game_over and not self.is_paused: self.music_player.start() else: self.music_player.stop() self.update_music_button() def update_music_button(self): if hasattr(self, 'music_btn'): text = "🎵 ON" if self.music_on else "🔇 OFF" color = '#9b59b6' if self.music_on else '#7f8c8d' self.music_btn.configure(text=text, bg=color) def start_game_loop(self): if self.game_loop_id: self.root.after_cancel(self.game_loop_id) def game_tick(): if self.game_started and not self.game_over and not self.is_paused: self.move_down() speed = max(100, 800 - (self.level - 1) * 80) self.game_loop_id = self.root.after(speed, game_tick) speed = max(100, 800 - (self.level - 1) * 80) self.game_loop_id = self.root.after(speed, game_tick) def setup_ui(self): self.canvas.delete("all") self.canvas.configure(bg='#0f172a') # Title self.canvas.create_text(200, 25, text="PENTRIX", fill='#22d3ee', font=('Arial', 24, 'bold')) # Stats area self.ui_elements['score_text'] = self.canvas.create_text( 60, 60, text="Score: 0", fill='#22d3ee', font=('Arial', 12), anchor='w') self.ui_elements['level_text'] = self.canvas.create_text( 180, 60, text="Lvl: 1", fill='#4ade80', font=('Arial', 12), anchor='w') self.ui_elements['lines_text'] = self.canvas.create_text( 280, 60, text="Lines: 0", fill='#facc15', font=('Arial', 12), anchor='w') # Board background board_x = 20 board_y = 80 board_w = self.BOARD_WIDTH * (self.CELL_SIZE + 2) + 4 board_h = self.BOARD_HEIGHT * (self.CELL_SIZE + 2) + 4 self.canvas.create_rectangle(board_x, board_y, board_x + board_w, board_y + board_h, fill='#020617', outline='#334155', width=2) self.board_offset = {'x': board_x + 2, 'y': board_y + 2} # Next piece area next_x = board_x + board_w + 20 self.canvas.create_rectangle(next_x, board_y, next_x + 80, board_y + 90, fill='#1e293b', outline='#334155') self.canvas.create_text(next_x + 40, board_y + 15, text="NEXT", fill='#94a3b8', font=('Arial', 10)) self.next_piece_offset = {'x': next_x + 10, 'y': board_y + 30} # Buttons btn_x = next_x btn_y = board_y + 100 # Start button self.start_btn = tk.Button(self.canvas, text="START", command=self.start_game, bg='#22c55e', fg='white', font=('Arial', 10, 'bold'), relief=tk.FLAT, padx=10, pady=5) self.canvas.create_window(btn_x + 40, btn_y, window=self.start_btn) # Pause button self.pause_btn = tk.Button(self.canvas, text="PAUSE", command=self.toggle_pause, bg='#f59e0b', fg='white', font=('Arial', 10, 'bold'), relief=tk.FLAT, padx=10, pady=5) self.canvas.create_window(btn_x + 40, btn_y + 40, window=self.pause_btn) # Music button self.music_btn = tk.Button(self.canvas, text="🎵 ON", command=self.toggle_music, bg='#9b59b6', fg='white', font=('Arial', 10, 'bold'), relief=tk.FLAT, padx=10, pady=5) self.canvas.create_window(btn_x + 40, btn_y + 80, window=self.music_btn) # Instructions inst_y = btn_y + 130 self.canvas.create_text(btn_x + 40, inst_y, text="Controls:", fill='#94a3b8', font=('Arial', 9, 'bold')) self.canvas.create_text(btn_x + 40, inst_y + 20, text="← → Move", fill='#64748b', font=('Arial', 8)) self.canvas.create_text(btn_x + 40, inst_y + 35, text="↑ Rotate", fill='#64748b', font=('Arial', 8)) self.canvas.create_text(btn_x + 40, inst_y + 50, text="↓ Soft drop", fill='#64748b', font=('Arial', 8)) self.canvas.create_text(btn_x + 40, inst_y + 65, text="Space Hard drop", fill='#64748b', font=('Arial', 8)) self.canvas.create_text(btn_x + 40, inst_y + 80, text="P Pause", fill='#64748b', font=('Arial', 8)) # Music info music_status = "♪ Classical Music" if self.music_player.music_enabled else "♪ Music unavailable" self.canvas.create_text(btn_x + 40, inst_y + 110, text=music_status, fill='#a855f7', font=('Arial', 8)) def bind_events(self): self.root.bind('', lambda e: self.move_left()) self.root.bind('', lambda e: self.move_right()) self.root.bind('', lambda e: self.move_down()) self.root.bind('', lambda e: self.rotate_piece()) self.root.bind('', lambda e: self.hard_drop()) self.root.bind('

', lambda e: self.toggle_pause()) self.root.bind('

', lambda e: self.toggle_pause()) self.root.bind('', lambda e: self.toggle_music()) self.root.bind('', lambda e: self.toggle_music()) # Touch/click events on canvas self.canvas.bind('', self.on_click) self.canvas.bind('', self.on_drag) self.canvas.bind('', self.on_release) def on_click(self, event): self.touch_start = {'x': event.x, 'y': event.y} self.touch_move = {'x': event.x, 'y': event.y} self.has_moved = False def on_drag(self, event): if not self.game_started or self.game_over or self.is_paused: return delta_x = event.x - self.touch_move['x'] if abs(delta_x) > 25: self.has_moved = True if delta_x > 0: self.move_right() else: self.move_left() self.touch_move['x'] = event.x self.touch_move['y'] = event.y def on_release(self, event): if not self.game_started or self.game_over or self.is_paused: return delta_y = event.y - self.touch_start['y'] delta_x = abs(event.x - self.touch_start['x']) if not self.has_moved: self.rotate_piece() elif delta_y > 50 and abs(delta_y) > delta_x: self.hard_drop() def render(self): # Clear old cells for cell_id in self.cell_ids: self.canvas.delete(cell_id) self.cell_ids = [] # Update stats self.canvas.itemconfig(self.ui_elements['score_text'], text=f"Score: {self.score}") self.canvas.itemconfig(self.ui_elements['level_text'], text=f"Lvl: {self.level}") self.canvas.itemconfig(self.ui_elements['lines_text'], text=f"Lines: {self.lines}") # Build display board display_board = [row[:] for row in self.board] # Draw ghost piece ghost_pos = self.get_ghost_position() if self.current_piece and ghost_pos and ghost_pos['y'] != self.position['y']: for r, row in enumerate(self.current_piece['shape']): for c, cell in enumerate(row): if cell: y = ghost_pos['y'] + r x = ghost_pos['x'] + c if 0 <= y < self.BOARD_HEIGHT and 0 <= x < self.BOARD_WIDTH: if not display_board[y][x]: display_board[y][x] = ('ghost', self.current_piece['color']) # Draw current piece if self.current_piece: for r, row in enumerate(self.current_piece['shape']): for c, cell in enumerate(row): if cell: y = self.position['y'] + r x = self.position['x'] + c if 0 <= y < self.BOARD_HEIGHT and 0 <= x < self.BOARD_WIDTH: display_board[y][x] = self.current_piece['color'] # Render board for r in range(self.BOARD_HEIGHT): for c in range(self.BOARD_WIDTH): cell = display_board[r][c] x = self.board_offset['x'] + c * (self.CELL_SIZE + 2) y = self.board_offset['y'] + r * (self.CELL_SIZE + 2) if cell: if isinstance(cell, tuple) and cell[0] == 'ghost': # Ghost piece color = cell[1] cid = self.canvas.create_rectangle( x, y, x + self.CELL_SIZE, y + self.CELL_SIZE, fill='', outline=color, width=1, stipple='gray25') else: # Normal piece cid = self.canvas.create_rectangle( x, y, x + self.CELL_SIZE, y + self.CELL_SIZE, fill=cell, outline='') self.cell_ids.append(cid) else: cid = self.canvas.create_rectangle( x, y, x + self.CELL_SIZE, y + self.CELL_SIZE, fill='#1e293b', outline='') self.cell_ids.append(cid) # Render next piece if self.next_piece: for r, row in enumerate(self.next_piece['shape']): for c, cell in enumerate(row): if cell: x = self.next_piece_offset['x'] + c * 14 y = self.next_piece_offset['y'] + r * 14 cid = self.canvas.create_rectangle( x, y, x + 12, y + 12, fill=self.next_piece['color'], outline='') self.cell_ids.append(cid) # Game over overlay if self.game_over: overlay = self.canvas.create_rectangle( self.board_offset['x'], self.board_offset['y'], self.board_offset['x'] + self.BOARD_WIDTH * (self.CELL_SIZE + 2), self.board_offset['y'] + self.BOARD_HEIGHT * (self.CELL_SIZE + 2), fill='black', stipple='gray50') self.cell_ids.append(overlay) txt = self.canvas.create_text( self.board_offset['x'] + self.BOARD_WIDTH * (self.CELL_SIZE + 2) // 2, self.board_offset['y'] + self.BOARD_HEIGHT * (self.CELL_SIZE + 2) // 2, text="GAME OVER!", fill='#ef4444', font=('Arial', 18, 'bold')) self.cell_ids.append(txt) # Paused overlay if self.is_paused and not self.game_over: overlay = self.canvas.create_rectangle( self.board_offset['x'], self.board_offset['y'], self.board_offset['x'] + self.BOARD_WIDTH * (self.CELL_SIZE + 2), self.board_offset['y'] + self.BOARD_HEIGHT * (self.CELL_SIZE + 2), fill='black', stipple='gray50') self.cell_ids.append(overlay) txt = self.canvas.create_text( self.board_offset['x'] + self.BOARD_WIDTH * (self.CELL_SIZE + 2) // 2, self.board_offset['y'] + self.BOARD_HEIGHT * (self.CELL_SIZE + 2) // 2, text="PAUSED", fill='white', font=('Arial', 18, 'bold')) self.cell_ids.append(txt) def cleanup(self): """Clean up game resources and unbind events""" if self.game_loop_id: self.root.after_cancel(self.game_loop_id) self.music_player.stop() # Unbind keyboard events self.root.unbind('') self.root.unbind('') self.root.unbind('') self.root.unbind('') self.root.unbind('') self.root.unbind('

') self.root.unbind('

') self.root.unbind('') self.root.unbind('') # Unbind canvas events self.canvas.unbind('') self.canvas.unbind('') self.canvas.unbind('') ############################################################################### # 6) JAVASCRIPT ENGINE ############################################################################### class JSEngine: def __init__(self, dom_root, browser=None): self.dom_root = dom_root self.browser = browser self.global_vars = {} self.timers = {} self.timer_id = 0 self.mounted_games = [] def execute_scripts(self): for sc in self._collect_scripts(self.dom_root): try: self._exec(sc) except Exception as e: print(f"Script error: {e}") def _collect_scripts(self, node): arr = [] if node.tag_name == "script" and node.script_code: arr.append(node.script_code) for c in node.children: arr.extend(self._collect_scripts(c)) return arr def _exec(self, sc): if 'PENTOMINOES' in sc or 'Pentrix' in sc or ('React' in sc and 'useState' in sc): return self._exec_pentrix(sc) sc = re.sub(r'//.*$', '', sc, flags=re.MULTILINE) sc = re.sub(r'/\*.*?\*/', '', sc, flags=re.DOTALL) for line in sc.split(";"): line = line.strip() if line and "=" in line and "==" not in line: parts = line.split("=", 1) var = parts[0].strip() for kw in ("var ", "let ", "const "): if var.startswith(kw): var = var[len(kw):].strip() break val = parts[1].strip() if val.startswith(('"', "'")) and val.endswith(('"', "'")): self.global_vars[var] = val[1:-1] elif val.isdigit(): self.global_vars[var] = int(val) def _exec_pentrix(self, sc): if self.browser: game = PentrixGame(self.browser.canvas, self.browser.root, lambda msg: self.browser.status.config(text=msg)) self.mounted_games.append(game) self.browser.status.config(text="Pentrix loaded! Press M to toggle music.") def handle_event(self, node, event_type): if event_type in node.event_handlers: try: self._exec(node.event_handlers[event_type]) except: pass def cleanup(self): for tid in list(self.timers.keys()): try: self.browser.root.after_cancel(self.timers[tid]) except: pass self.timers.clear() for game in self.mounted_games: try: game.cleanup() except: pass self.mounted_games.clear() ############################################################################### # 7) LAYOUT ENGINE ############################################################################### GLOBAL_MEASURE_CANVAS = None FONT_CACHE = OrderedDict() MEASURE_CACHE = {} LINEHEIGHT_CACHE = {} class LayoutBox: __slots__ = ['dom_node', 'x', 'y', 'width', 'height', 'children', 'style', 'widget', 'is_image', 'is_input', 'is_button', 'is_textarea', 'is_inline'] def __init__(self, node): self.dom_node = node self.x = self.y = self.width = self.height = 0 self.children = [] self.style = { "bold": False, "italic": False, "underline": False, "color": "black", "size": DEFAULT_FONT_SIZE, "family": DEFAULT_FONT_FAMILY, "margin": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "background_color": "transparent", "text_align": "left", "valign": "baseline" } self.widget = None self.is_image = self.is_input = self.is_button = self.is_textarea = False self.is_inline = node.is_inline if node else False def get_font(style): weight = "bold" if style.get("bold") else "normal" slant = "italic" if style.get("italic") else "roman" underline = 1 if style.get("underline") else 0 size = max(8, min(72, style.get("size", DEFAULT_FONT_SIZE))) family = style.get("family", DEFAULT_FONT_FAMILY) key = (family, size, weight, slant, underline) if key not in FONT_CACHE: if len(FONT_CACHE) > MAX_FONT_CACHE_SIZE: FONT_CACHE.popitem(last=False) try: FONT_CACHE[key] = Font(family=family, size=size, weight=weight, slant=slant, underline=underline) except: FONT_CACHE[key] = Font(family="Arial", size=size, weight=weight, slant=slant, underline=underline) return FONT_CACHE[key] def measure_text(txt, style): if not txt: return 0 size = style.get("size", DEFAULT_FONT_SIZE) bold = style.get("bold", False) italic = style.get("italic", False) family = style.get("family", DEFAULT_FONT_FAMILY) key = (txt, size, bold, italic, family) if key in MEASURE_CACHE: return MEASURE_CACHE[key] if GLOBAL_MEASURE_CANVAS: f = get_font(style) w = f.measure(txt) else: avg_width = size * (0.6 if bold else 0.55) w = len(txt) * avg_width if len(MEASURE_CACHE) < 50000: MEASURE_CACHE[key] = w return w def measure_lineheight(style): size = style.get("size", DEFAULT_FONT_SIZE) bold = style.get("bold", False) italic = style.get("italic", False) family = style.get("family", DEFAULT_FONT_FAMILY) key = (size, bold, italic, family) if key in LINEHEIGHT_CACHE: return LINEHEIGHT_CACHE[key] if GLOBAL_MEASURE_CANVAS: f = get_font(style) h = f.metrics("linespace") else: h = size + 4 LINEHEIGHT_CACHE[key] = h return h def is_inside_link(node): p = node.parent if hasattr(node, 'parent') else None while p: if p.tag_name == "a": return True p = p.parent return False def combine_styles(parent_style, node): s = { "bold": parent_style.get("bold", False), "italic": parent_style.get("italic", False), "underline": False, "color": parent_style.get("color", "black"), "size": parent_style.get("size", DEFAULT_FONT_SIZE), "family": parent_style.get("family", DEFAULT_FONT_FAMILY), "margin": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "background_color": "transparent", "text_align": parent_style.get("text_align", "left"), "valign": parent_style.get("valign", "baseline") } cs = getattr(node, 'computed_styles', {}) if cs: color_val = cs.get("color") if color_val: parsed = parse_color(color_val) if parsed and is_color_visible(parsed): s["color"] = parsed fw = cs.get("font-weight", "normal") s["bold"] = fw in ("bold", "bolder", "700", "800", "900") or parent_style.get("bold", False) if cs.get("font-style") == "italic": s["italic"] = True s["size"] = _px_to_int(cs.get("font-size", f"{DEFAULT_FONT_SIZE}px"), DEFAULT_FONT_SIZE) ff = cs.get("font-family", "") if ff: s["family"] = ff.split(",")[0].strip().strip("'\"") bc = cs.get("background-color", "transparent") if bc and bc not in ("transparent", "inherit", "initial"): parsed_bg = parse_color(bc) if parsed_bg: s["background_color"] = parsed_bg s["margin"] = { "top": _px_to_int(cs.get("margin-top", "0")), "right": _px_to_int(cs.get("margin-right", "0")), "bottom": _px_to_int(cs.get("margin-bottom", "0")), "left": _px_to_int(cs.get("margin-left", "0")) } s["padding"] = { "top": _px_to_int(cs.get("padding-top", "0")), "right": _px_to_int(cs.get("padding-right", "0")), "bottom": _px_to_int(cs.get("padding-bottom", "0")), "left": _px_to_int(cs.get("padding-left", "0")) } s["text_align"] = cs.get("text-align", s["text_align"]) s["valign"] = cs.get("vertical-align", s["valign"]) t = node.tag_name if hasattr(node, 'tag_name') else "" if t in ("b", "strong"): s["bold"] = True if t in ("i", "em"): s["italic"] = True if t == "u": s["underline"] = True if t == "a": s["underline"] = True if s["color"] == "black" or not is_color_visible(s["color"]): s["color"] = "#0000EE" elif t == "text" and is_inside_link(node): s["underline"] = True if s["color"] == "black" or not is_color_visible(s["color"]): s["color"] = "#0000EE" if not is_color_visible(s.get("color")): s["color"] = "black" return s def layout_tree(dom_node, container_width=800, offset_x=0, offset_y=0): root_box = LayoutBox(dom_node) root_box.style = { "bold": False, "italic": False, "underline": False, "color": "black", "size": DEFAULT_FONT_SIZE, "family": DEFAULT_FONT_FAMILY, "margin": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "background_color": "white", "text_align": "left", "valign": "baseline" } def layout_block(node, pbox, x, y, avail_w, parent_st): box = LayoutBox(node) pbox.children.append(box) st = combine_styles(parent_st, node) box.style = st mt, ml, mr, mb = st["margin"]["top"], st["margin"]["left"], st["margin"]["right"], st["margin"]["bottom"] pt, pl, pr, pb = st["padding"]["top"], st["padding"]["left"], st["padding"]["right"], st["padding"]["bottom"] box.x = x + ml box.y = y + mt content_x = box.x + pl content_y = box.y + pt content_w = max(10, avail_w - ml - mr - pl - pr) tag = node.tag_name if tag in SKIP_ELEMENTS: box.width = box.height = 0 return box cs = getattr(node, 'computed_styles', {}) if cs.get("display") == "none": box.width = box.height = 0 return box if tag == "hr": box.width = content_w box.height = 2 + mt + mb return box if tag == "img": box.is_image = True wv = node.attributes.get("width", "") or node.styles.get("width", "") hv = node.attributes.get("height", "") or node.styles.get("height", "") w = _px_to_int(wv, 0, content_w) if wv else 0 h = _px_to_int(hv, 0, 800) if hv else 0 if w == 0: w = min(content_w - 20, 600) if h == 0: h = int(w * 0.6) box.width = min(max(1, w), content_w) + pl + pr box.height = min(max(1, h), 800) + pt + pb return box if tag == "input": input_type = node.attributes.get("type", "text").lower() if input_type == "submit": box.is_button = True box.width = min(120, content_w) box.height = 28 elif input_type in ("checkbox", "radio"): box.is_input = True box.width = box.height = 20 elif input_type == "hidden": box.width = box.height = 0 else: box.is_input = True size = int(node.attributes.get("size", "20")) box.width = min(size * 8 + 16, content_w) box.height = 24 return box if tag == "textarea": box.is_textarea = True cols = int(node.attributes.get("cols", "40")) rows = int(node.attributes.get("rows", "4")) box.width = min(cols * 8, content_w) box.height = rows * 18 return box if tag == "button": box.is_button = True box.width = min(100, content_w) box.height = 28 return box if tag == "br": box.width = 0 box.height = measure_lineheight(st) return box current_y = content_y line_items = [] line_h = 0 line_x = content_x def flush_line(): nonlocal line_items, current_y, line_h, line_x if not line_items: line_x = content_x return for it in line_items: it.y = current_y current_y += line_h line_items = [] line_x = content_x line_h = 0 for child in node.children: if child.tag_name in ("script", "style"): continue child_cs = getattr(child, 'computed_styles', {}) if child_cs.get("display") == "none": continue is_block = child.tag_name not in INLINE_ELEMENTS and child.tag_name != "text" if is_block: if line_items: flush_line() cb = layout_block(child, box, content_x, current_y, content_w, st) current_y = cb.y + cb.height else: if child.tag_name == "text": text = child.text if not text: continue tokens = RE_WORD_SPLIT.findall(text) child_st = combine_styles(st, child) th = measure_lineheight(child_st) for tok in tokens: if tok.isspace() and not line_items: continue tw = measure_text(tok, child_st) if line_x + tw > content_x + content_w and not tok.isspace() and line_items: flush_line() if tok.isspace(): continue tbox = LayoutBox(child) tbox.style = child_st tbox.x = line_x tbox.y = current_y tbox.width = tw tbox.height = th tbox.dom_node = DOMNode("text") tbox.dom_node.text = tok box.children.append(tbox) line_items.append(tbox) line_x += tw line_h = max(line_h, th) else: cbox = layout_inline(child, box, line_x, current_y, content_x + content_w - line_x, st) if line_x + cbox.width > content_x + content_w and line_items: flush_line() cbox.x = line_x line_items.append(cbox) line_x += cbox.width line_h = max(line_h, cbox.height) if line_items: flush_line() if current_y == content_y: current_y = content_y + measure_lineheight(st) box.width = avail_w - ml - mr box.height = (current_y - box.y) + pb + mb return box def layout_inline(node, pbox, x, y, avail_w, parent_st): box = LayoutBox(node) pbox.children.append(box) st = combine_styles(parent_st, node) box.style = st box.is_inline = True box.x = x box.y = y if node.tag_name == "img": box.is_image = True wv = node.attributes.get("width", "") or node.styles.get("width", "") hv = node.attributes.get("height", "") or node.styles.get("height", "") w = _px_to_int(wv, 0, avail_w) if wv else 0 h = _px_to_int(hv, 0, 800) if hv else 0 if w == 0: w = min(avail_w - 20, 400) if h == 0: h = int(w * 0.6) box.width = min(max(1, w), avail_w) box.height = max(1, h) return box if node.tag_name == "br": box.width = 0 box.height = measure_lineheight(st) return box cx = x max_h = measure_lineheight(st) for child in node.children: if child.tag_name == "text": text = child.text if not text: continue tokens = RE_WORD_SPLIT.findall(text) child_st = combine_styles(st, child) th = measure_lineheight(child_st) for tok in tokens: tw = measure_text(tok, child_st) tbox = LayoutBox(child) tbox.style = child_st tbox.x = cx tbox.y = y tbox.width = tw tbox.height = th tbox.dom_node = DOMNode("text") tbox.dom_node.text = tok box.children.append(tbox) cx += tw max_h = max(max_h, th) else: cbox = layout_inline(child, box, cx, y, avail_w - (cx - x), st) cx += cbox.width max_h = max(max_h, cbox.height) box.width = cx - x box.height = max_h return box top_box = layout_block(dom_node, root_box, offset_x, offset_y, container_width, root_box.style) return top_box def find_box_bottom(lb): my = lb.y + lb.height for c in lb.children: my = max(my, find_box_bottom(c)) return my ############################################################################### # 8) RENDERING ############################################################################### class LinkArea: __slots__ = ['x1', 'y1', 'x2', 'y2', 'href'] def __init__(self, x1, y1, x2, y2, href): self.x1, self.y1, self.x2, self.y2, self.href = x1, y1, x2, y2, href def collect_image_urls(lb, base_url, collected): node = lb.dom_node if lb.is_image and node: src = node.attributes.get("src", "") if src: try: url = resolve_url(base_url, src) collected.append(str(url)) except: pass for c in lb.children: collect_image_urls(c, base_url, collected) def render_layout_box(browser, lb, canvas, widget_list, link_areas, y_min=0, y_max=50000): if lb.y > y_max + 200 or lb.y + lb.height < y_min - 200: return st = lb.style x, y, w, h = lb.x, lb.y, lb.width, lb.height node = lb.dom_node tag = node.tag_name if node else "" if not lb.is_image: bc = st.get("background_color", "transparent") if bc and bc not in ("transparent", "inherit") and w > 0 and h > 0: try: canvas.create_rectangle(x, y, x+w, y+h, fill=bc, outline="", width=0) except: pass if tag == "hr": canvas.create_line(x+5, y+h//2, x+w-5, y+h//2, fill="#ccc", width=1) return if lb.is_image: src = node.attributes.get("src", "") if node else "" if src: browser.draw_image(canvas, src, x, y, w, h) return if lb.is_button: label = node.attributes.get("value", "Submit") if tag == "input" else (node.get_text_content().strip() or "Submit") btn = tk.Button(canvas, text=label, command=lambda n=node: browser.on_button_click(n), font=("Arial", 9), bg="#f5f5f5", relief=tk.RAISED, padx=4, pady=2) canvas.create_window(x+1, y+1, anchor="nw", window=btn, width=w-2, height=h-2) lb.widget = btn widget_list.append(lb) return if lb.is_input: input_type = node.attributes.get("type", "text").lower() if node else "text" if input_type in ("checkbox", "radio"): var = tk.BooleanVar(value="checked" in node.attributes) cb = tk.Checkbutton(canvas, variable=var, bg="white") if input_type == "checkbox" else tk.Radiobutton(canvas, variable=var, bg="white") cb.var = var canvas.create_window(x, y, anchor="nw", window=cb) lb.widget = cb widget_list.append(lb) elif input_type != "hidden": e_var = tk.StringVar(value=node.attributes.get("value", "")) e = tk.Entry(canvas, textvariable=e_var, font=("Arial", 10), show="*" if input_type == "password" else "") canvas.create_window(x+1, y+1, anchor="nw", window=e, width=w-2, height=h-2) lb.widget = e widget_list.append(lb) return if lb.is_textarea: txt = tk.Text(canvas, font=("Courier New", 9), wrap=tk.WORD) txt.insert("1.0", node.text if node else "") canvas.create_window(x+1, y+1, anchor="nw", window=txt, width=w-2, height=h-2) lb.widget = txt widget_list.append(lb) return if tag == "a": href = node.attributes.get("href", "") if node else "" coords = [] for c in lb.children: render_layout_box(browser, c, canvas, widget_list, link_areas, y_min, y_max) if c.width > 0 and c.height > 0: coords.append((c.x, c.y, c.x + c.width, c.y + c.height)) if coords and href: link_areas.append(LinkArea( min(c[0] for c in coords), min(c[1] for c in coords), max(c[2] for c in coords), max(c[3] for c in coords), href )) return if tag == "text" and node and node.text: try: f = get_font(st) color = st.get("color", "black") text_color = color if is_color_visible(color) else "black" canvas.create_text(x, y, anchor="nw", text=node.text, fill=text_color, font=f) except: pass return for c in lb.children: render_layout_box(browser, c, canvas, widget_list, link_areas, y_min, y_max) ############################################################################### # 9) HELPER ############################################################################### def find_form_ancestor(node): p = node.parent if node else None while p and not p.is_form: p = p.parent return p ############################################################################### # 10) UI COMPONENTS ############################################################################### class ModernButton(tk.Canvas): def __init__(self, parent, text, command=None, width=80, height=30, bg_color="#2c3e50", hover_color="#3498db", text_color="white", font=("Arial", 10, "bold"), **kwargs): super().__init__(parent, width=width, height=height, highlightthickness=0, bg=parent["bg"], **kwargs) self.command = command self.bg_color = bg_color self.hover_color = hover_color self.text_color = text_color self.btn_width = width self.btn_height = height self.text = text self.font = font self._draw(bg_color) self.bind("", lambda e: self._draw(hover_color)) self.bind("", lambda e: self._draw(bg_color)) self.bind("", lambda e: self.command() if self.command else None) def _draw(self, color): self.delete("all") self.create_rectangle(0, 0, self.btn_width, self.btn_height, fill=color, outline="") self.create_text(self.btn_width//2, self.btn_height//2, text=self.text, fill=self.text_color, font=self.font) ############################################################################### # 11) BROWSER ############################################################################### class ToyBrowser: def __init__(self): self.root = tk.Tk() self.root.title("Enhanced Web Browser with React/Pentrix Support") self.root.geometry("1100x850") bg = "#f0f2f5" self.root.configure(bg=bg) global GLOBAL_MEASURE_CANVAS GLOBAL_MEASURE_CANVAS = tk.Canvas(self.root) GLOBAL_MEASURE_CANVAS.pack_forget() self.history = [] self.hist_pos = -1 top = tk.Frame(self.root, bg="#2c3e50", pady=5, padx=10) top.pack(side=tk.TOP, fill=tk.X) ModernButton(top, "◀", self.go_back, 40, 30, "#34495e", "#3498db").pack(side=tk.LEFT, padx=2) ModernButton(top, "▶", self.go_fwd, 40, 30, "#34495e", "#3498db").pack(side=tk.LEFT, padx=2) ModernButton(top, "↻", self.refresh, 40, 30, "#34495e", "#3498db").pack(side=tk.LEFT, padx=2) ModernButton(top, "⌂", self.go_home, 40, 30, "#34495e", "#3498db").pack(side=tk.LEFT, padx=5) url_frame = tk.Frame(top, bg="#34495e", padx=2, pady=2, highlightthickness=1, highlightbackground="#1abc9c") url_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10) self.url_bar = tk.Entry(url_frame, font=("Arial", 12), bg="#ecf0f1", fg="#2c3e50", bd=0) self.url_bar.pack(fill=tk.BOTH, expand=True, ipady=3) self.url_bar.bind("", lambda e: self.on_go()) ModernButton(top, "Go", self.on_go, 50, 30, "#2ecc71", "#27ae60").pack(side=tk.LEFT, padx=5) ModernButton(top, "Pentrix", self.launch_pentrix, 70, 30, "#9b59b6", "#8e44ad").pack(side=tk.LEFT, padx=5) bm = tk.Frame(self.root, bg="#ecf0f1", padx=10, pady=5) bm.pack(side=tk.TOP, fill=tk.X) ModernButton(bm, "Words", lambda: self.load_url_str("https://justinjackson.ca/words.html"), 80, 28, "#e74c3c", "#c0392b").pack(side=tk.LEFT, padx=5) ModernButton(bm, "Forum", lambda: self.load_url_str("http://162.208.9.114:8081/"), 80, 28, "#9b59b6", "#8e44ad").pack(side=tk.LEFT, padx=5) ModernButton(bm, "HN", lambda: self.load_url_str("https://news.ycombinator.com"), 60, 28, "#f39c12", "#d35400").pack(side=tk.LEFT, padx=5) ModernButton(bm, "Example", lambda: self.load_url_str("http://example.com"), 80, 28, "#1abc9c", "#16a085").pack(side=tk.LEFT, padx=5) frame = tk.Frame(self.root, bg=bg) frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10) canvas_frame = tk.Frame(frame, bg="white", bd=1, relief=tk.RAISED) canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.canvas = tk.Canvas(canvas_frame, bg="white", scrollregion=(0,0,3000,10000), highlightthickness=0) self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.scroll = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview) self.scroll.pack(side=tk.RIGHT, fill=tk.Y) self.canvas.config(yscrollcommand=self.scroll.set) self.canvas.bind("", lambda e: self.canvas.yview_scroll(int(-1*(e.delta/120)), "units")) self.canvas.bind("", lambda e: self.canvas.yview_scroll(-3, "units")) self.canvas.bind("", lambda e: self.canvas.yview_scroll(3, "units")) # Store initial browser event handlers self.browser_click_handler = self.on_click self.browser_motion_handler = self.on_motion # Bind browser handlers initially self.canvas.bind("", self.browser_click_handler) self.canvas.bind("", self.browser_motion_handler) self.root.bind("", lambda e: self.refresh()) self.root.bind("", lambda e: self.refresh()) self.status = tk.Label(self.root, text="Ready", bd=1, relief=tk.SUNKEN, anchor=tk.W, bg="#34495e", fg="white", font=("Arial", 9)) self.status.pack(side=tk.BOTTOM, fill=tk.X) self.images_cache = [] self.image_url_cache = {} self.current_dom = None self.layout_root = None self.current_url_obj = None self.form_widgets = [] self.link_areas = [] self.css_rules = [] self.css_index = None self.js_engine = None self.loading = False self.pentrix_game = None self.image_executor = ThreadPoolExecutor(max_workers=8) self.prefetch_queue = queue.Queue() def launch_pentrix(self): self.cleanup_current_page() self.canvas.delete("all") self.canvas.configure(scrollregion=(0, 0, 500, 600)) self.url_bar.delete(0, tk.END) self.url_bar.insert(0, "pentrix://game") self.pentrix_game = PentrixGame(self.canvas, self.root, lambda msg: self.status.config(text=msg)) music_info = "with music! Press M to toggle." if self.pentrix_game.music_player.music_enabled else "(install numpy + simpleaudio for music)" self.status.config(text=f"Pentrix loaded {music_info}") def cleanup_current_page(self): """Clean up the current page and restore browser state""" # Clean up JavaScript engine if self.js_engine: self.js_engine.cleanup() self.js_engine = None # Clean up Pentrix game if self.pentrix_game: self.pentrix_game.cleanup() self.pentrix_game = None # Reset canvas to white background self.canvas.configure(bg='white') # Rebind browser event handlers self.canvas.unbind('') self.canvas.unbind('') self.canvas.bind("", self.browser_click_handler) self.canvas.bind("", self.browser_motion_handler) def go_home(self): self.load_url_str("https://news.ycombinator.com") def refresh(self): if self.current_url_obj: global RESPONSE_CACHE, DNS_CACHE RESPONSE_CACHE.clear() DNS_CACHE.clear() self.image_url_cache.clear() self.images_cache.clear() self.load_url(self.current_url_obj) def go_back(self): if self.hist_pos > 0: self.hist_pos -= 1 self.load_url_str(self.history[self.hist_pos], False) def go_fwd(self): if self.hist_pos < len(self.history) - 1: self.hist_pos += 1 self.load_url_str(self.history[self.hist_pos], False) def on_go(self): raw = self.url_bar.get().strip() if not raw: return if raw == "pentrix://game" or raw.lower() == "pentrix": self.launch_pentrix() return if not raw.startswith(("http://", "https://")): raw = "https://" + raw if "." in raw else f"https://{raw}.com" self.load_url_str(raw, True) def load_url_str(self, url_s, push=True): if url_s == "pentrix://game" or url_s.lower() == "pentrix": self.launch_pentrix() return try: purl = parse_url(url_s) except Exception as e: self.show_error(str(e)) return self.load_url(purl) if push: self.history = self.history[:self.hist_pos+1] self.history.append(url_s) self.hist_pos += 1 def load_url(self, url_obj, method="GET", body="", headers=None): self.cleanup_current_page() global MEASURE_CACHE, LINEHEIGHT_CACHE, _SELECTOR_CACHE MEASURE_CACHE.clear() LINEHEIGHT_CACHE.clear() _SELECTOR_CACHE.clear() if headers is None: headers = {} self.loading = True start = time.time() try: self.status.config(text=f"Connecting to {url_obj.host}...") self.root.update_idletasks() rh, rb, fu = http_request(url_obj, method, headers, body) except Exception as e: self.loading = False self.show_error(str(e)) return self.url_bar.delete(0, tk.END) self.url_bar.insert(0, str(fu)) self.current_url_obj = fu ctype = rh.get("content-type", "").lower() encoding = "utf-8" if "charset=" in ctype: encoding = ctype.split("charset=")[-1].split(";")[0].strip() try: text = rb.decode(encoding, "replace") except: text = rb.decode("utf-8", "replace") if 'PENTOMINOES' in text or 'Pentrix' in text or ('React' in text and 'useState' in text): self.launch_pentrix() self.loading = False return if "text/html" in ctype or " target_w: scale = target_w / final_w final_w = int(final_w * scale) final_h = int(final_h * scale) if final_h > target_h: scale = target_h / final_h final_w = int(final_w * scale) final_h = int(final_h * scale) final_w = max(1, final_w) final_h = max(1, final_h) if im.width != final_w or im.height != final_h: im = im.resize((final_w, final_h), Image.Resampling.LANCZOS) tkimg = ImageTk.PhotoImage(im) def draw(): self.images_cache.append(tkimg) self.image_url_cache[cache_key] = tkimg canvas.create_image(x, y, anchor="nw", image=tkimg) self.root.after(0, draw) except Exception: def draw_err(): canvas.create_rectangle(x, y, x+(w or 20), y+(h or 20), fill="#f8f8f8", outline="#ddd") self.root.after(0, draw_err) self.image_executor.submit(worker) def on_click(self, event): cx, cy = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y) for la in self.link_areas: if la.x1 <= cx <= la.x2 and la.y1 <= cy <= la.y2: if not la.href.startswith("#"): self.load_url_str(str(resolve_url(self.current_url_obj, la.href)) if self.current_url_obj else la.href) break def on_motion(self, event): cx, cy = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y) for la in self.link_areas: if la.x1 <= cx <= la.x2 and la.y1 <= cy <= la.y2: self.canvas.config(cursor="hand2") self.status.config(text=la.href) return self.canvas.config(cursor="") if not self.loading: self.status.config(text="Ready") def show_error(self, msg): self.canvas.delete("all") self.link_areas.clear() self.images_cache.clear() self.canvas.create_rectangle(50, 50, 650, 250, fill="#fff0f0", outline="#c00") self.canvas.create_text(60, 70, text="Error Loading Page", font=("Arial", 16, "bold"), fill="#c00", anchor="nw") self.canvas.create_text(60, 110, text=msg, font=("Arial", 11), fill="#800", anchor="nw", width=570) self.status.config(text=f"Error: {msg[:60]}...") def run(self): self.root.mainloop() if __name__ == "__main__": sys.setrecursionlimit(10**3) ToyBrowser().run()