import socket import tkinter as tk from tkinter import ttk import struct import re import json import warnings from urllib3.exceptions import InsecureRequestWarning # Suppress "InsecureRequestWarning" for unverified HTTPS requests 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 math ############################################################################### # 1) DNS + URL PARSING ############################################################################### def resolve_hostname_dns(hostname, dns_server="8.8.8.8", port=53, timeout=5): """ If 'hostname' is numeric, skip DNS. Otherwise do a naive DNS A-record lookup. """ hostname = hostname.strip() try: socket.inet_aton(hostname) # numeric => skip DNS return hostname except OSError: pass tid = random.randint(0,65535) header = struct.pack(">HHHHHH", tid, 0x0100, 1, 0, 0, 0) qname = b"" for part in hostname.split("."): qname += bytes([len(part)]) + part.encode("ascii") 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() resp_tid, flags, qdcount, ancount, nscount, arcount = struct.unpack(">HHHHHH", data[:12]) if resp_tid != tid: return None idx = 12 # skip question while data[idx] != 0: idx += 1 idx += 1 idx += 4 ip_addr = None for _ in range(ancount): if data[idx] & 0xC0 == 0xC0: idx += 2 else: while data[idx] != 0: idx += 1 idx += 1 rtype, rclass, rttl, rdlength = struct.unpack(">HHIH", data[idx:idx+10]) idx += 10 if rtype == 1 and rclass == 1 and rdlength == 4: ip_bytes = data[idx:idx+4] ip_addr = ".".join(map(str, ip_bytes)) break idx += rdlength return ip_addr class ParsedURL: def __init__(self, scheme="http", host="", port=80, path="/"): self.scheme = scheme self.host = host self.port = port self.path = path def parse_url(url): """ Minimal parse => scheme://host[:port]/path scheme= http => default port=80, https => default=443 """ url = url.strip() scheme = "http" if url.startswith("http://"): after = url[7:] scheme = "http" elif url.startswith("https://"): after = url[8:] scheme = "https" else: after = url slash = after.find("/") if slash == -1: host_port = after path = "/" else: host_port = after[:slash] path = after[slash:] or "/" if ":" in host_port: h,p = host_port.split(":",1) port = int(p) host = h else: host = host_port port = 443 if scheme=="https" else 80 return ParsedURL(scheme, host.strip(), port, path) ############################################################################### # 2) HTTP with chunked decode + manual 3xx ############################################################################### def http_request(url_obj, method="GET", headers=None, body="", max_redirects=10): if headers is None: headers={} cur_url = url_obj cur_method = method cur_body = 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 new_url = parse_url(location) if status_code in (302,303): cur_method="GET" cur_body="" cur_url=new_url else: return r_headers, r_body, r_url return r_headers, r_body, r_url def _single_http_request(url_obj, method="GET", headers=None, body=""): if url_obj.scheme=="https": return _requests_https(url_obj, method, headers, body) else: return _raw_http(url_obj, method, headers, body) def _requests_https(url_obj, method="GET", headers=None, body=""): import requests if headers is None: headers={} final_h={} for k,v in headers.items(): if k.lower() not in ["host","content-length"]: final_h[k]=v if url_obj.port != 443: full_url = f"https://{url_obj.host}:{url_obj.port}{url_obj.path}" else: full_url = f"https://{url_obj.host}{url_obj.path}" resp = requests.request( method=method, url=full_url, headers=final_h, data=body.encode("utf-8") if body else None, allow_redirects=False, verify=False ) r_h = {":status_code": str(resp.status_code)} for k,v in resp.headers.items(): r_h[k.lower()] = v return r_h, resp.content, url_obj def _raw_http(url_obj, method="GET", headers=None, body=""): if headers is None: headers={} ip_addr = resolve_hostname_dns(url_obj.host) if not ip_addr: raise Exception(f"DNS fail => {url_obj.host}") import socket sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((ip_addr, url_obj.port)) lines = [ f"{method} {url_obj.path} HTTP/1.1", f"Host: {url_obj.host}" ] for k,v in headers.items(): if k.lower()!="host": lines.append(f"{k}: {v}") lines.append("Connection: close") lines.append(f"Content-Length: {len(body)}") lines.append("") req_str="\r\n".join(lines)+"\r\n"+body sock.sendall(req_str.encode("utf-8")) response=b"" while True: chunk=sock.recv(4096) if not chunk: break response+=chunk 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") st_line=lines[0] parts = st_line.split(" ",2) headers_dict={} if len(parts)>=2: headers_dict[":status_code"] = parts[1] 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) return headers_dict, raw_body, url_obj def decode_chunked_body(rb): i=0 decoded=b"" while True: newline=rb.find(b"\r\n", i) if newline==-1: break chunk_size_hex=rb[i:newline].decode("utf-8","replace").strip() i=newline+2 try: chunk_size=int(chunk_size_hex,16) except: chunk_size=0 if chunk_size==0: break chunk_data=rb[i:i+chunk_size] decoded+=chunk_data i+=chunk_size if rb[i:i+2]==b"\r\n": i+=2 return decoded ############################################################################### # 3) DOM Node with CSS & JS support ############################################################################### class DOMNode: def __init__(self, tag_name="document", parent=None): self.tag_name=tag_name.lower() self.attributes={} self.children=[] self.parent=parent self.text="" self.styles={} self.computed_styles={} # Final 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 ["span", "a", "b", "strong", "i", "em", "u", "small", "code", "mark"] self.id = "" # For CSS selectors self.classes = [] # For CSS class selectors self.display = "block" # Default display self.margin = {"top": 0, "right": 0, "bottom": 0, "left": 0} self.padding = {"top": 0, "right": 0, "bottom": 0, "left": 0} self.color = "" self.background_color = "" self.font_weight = "" self.font_style = "" self.text_decoration = "" self.font_size = "" self.event_handlers = {} # For JavaScript def __repr__(self): return f"" def parse_html(ht): i=0 root=DOMNode("document") current=root tb=[] while i",i) if close_i==-1: break tag_c=ht[i+1:close_i].strip() # Check if it's a close tag if tag_c.startswith("/"): close_tag=tag_c[1:].lower() if current.tag_name==close_tag and current.parent: current=current.parent i=close_i+1 continue parts=tag_c.split() tname=parts[0].lower() nd=DOMNode(tname,parent=current) for ap in parts[1:]: eq=ap.find("=") if eq!=-1: an=ap[:eq].lower() av=ap[eq+1:].strip("\"' ") nd.attributes[an]=av # Extract important attributes for CSS if an == "id": nd.id = av elif an == "class": nd.classes = av.split() elif an == "style": # Parse inline style for style_part in av.split(";"): if ":" in style_part: prop, val = style_part.split(":", 1) nd.styles[prop.strip()] = val.strip() # Parse event handlers elif an.startswith("on"): event_type = an[2:] # Remove "on" prefix nd.event_handlers[event_type] = av if tname=="form": nd.is_form=True nd.method=nd.attributes.get("method","get").lower() nd.action=nd.attributes.get("action","") current.children.append(nd) # handle self-closing or special tags: if tname in ["br","hr","meta","link","img","input"]: # 'img' => no children if tname=="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] i=close_i+1 continue # handle same as <script>, <style>, <textarea> => read until closing tag elif tname=="title": title_close=ht.find("",close_i+1) if title_close==-1: i=len(ht) continue cont=ht[close_i+1:title_close] nd.text=cont i=title_close+len("") continue elif tname=="textarea": textarea_close=ht.find("",close_i+1) if textarea_close==-1: i=len(ht) continue cont=ht[close_i+1:textarea_close] nd.text=cont fa=current while fa and not fa.is_form: fa=fa.parent nm=nd.attributes.get("name","") if fa and nm: fa.form_fields[nm]=[cont,nd] i=textarea_close+len("") continue elif tname=="style": style_close=ht.find("",close_i+1) if style_close==-1: i=len(ht) continue st=ht[close_i+1:style_close] nd.inline_css=st i=style_close+len("") continue elif tname=="script": script_close=ht.find("",close_i+1) if script_close==-1: i=len(ht) continue sc=ht[close_i+1:script_close] nd.script_code=sc i=script_close+len("") continue else: # normal open tag => descend current=nd i=close_i+1 else: tb.append(ht[i]) i+=1 if tb: leftover="".join(tb) if leftover.strip(): tn=DOMNode(parent=current) tn.text=leftover current.children.append(tn) return root ############################################################################### # 4) Enhanced CSS Parser and Processor ############################################################################### class CSSRule: def __init__(self, selector, properties): self.selector = selector self.properties = properties self.specificity = self._calculate_specificity(selector) def _calculate_specificity(self, selector): """Calculate CSS specificity: (id, class, element)""" id_count = selector.count('#') class_count = selector.count('.') element_count = len(re.findall(r'[a-zA-Z]+', selector)) - id_count - class_count return (id_count, class_count, element_count) def __repr__(self): return f"" def parse_css(css_text): """Enhanced CSS parser that returns a list of CSSRule objects""" rules = [] i = 0 while i < len(css_text): bo = css_text.find("{", i) if bo == -1: break selector_text = css_text[i:bo].strip() bc = css_text.find("}", bo) if bc == -1: break block = css_text[bo+1:bc].strip() i = bc + 1 # Parse properties properties = {} for line in block.split(";"): line = line.strip() if ":" in line: prop, val = line.split(":", 1) properties[prop.strip()] = val.strip() # Create rules for each selector in a group for selector in selector_text.split(","): selector = selector.strip() if selector: rules.append(CSSRule(selector, properties)) return rules def selector_matches(selector, node): """Check if a CSS selector matches a DOM node""" # Simple element selector if selector.lower() == node.tag_name.lower(): return True # ID selector if selector.startswith('#') and node.id == selector[1:]: return True # Class selector if selector.startswith('.') and selector[1:] in node.classes: return True # Compound selectors (very basic implementation) if ' ' in selector: # Descendant selector parent_sel, child_sel = selector.rsplit(' ', 1) if selector_matches(child_sel, node): parent = node.parent while parent: if selector_matches(parent_sel, parent): return True parent = parent.parent return False def apply_css_rules(node, css_rules): """Apply CSS rules to a node and its children""" matching_rules = [] # Find all matching rules for this node for rule in css_rules: if selector_matches(rule.selector, node): matching_rules.append(rule) # Sort by specificity matching_rules.sort(key=lambda r: r.specificity) # Apply properties from matching rules for rule in matching_rules: for prop, value in rule.properties.items(): node.styles[prop] = value # Apply CSS to children for child in node.children: apply_css_rules(child, css_rules) def compute_styles(node): """Compute final styles for a node based on inheritance and cascading""" # Start with defaults computed = { "color": "black", "background-color": "transparent", "font-size": "12px", "font-weight": "normal", "font-style": "normal", "text-decoration": "none", "display": "inline" if node.is_inline else "block", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" } # Inherit from parent if applicable if node.parent and hasattr(node.parent, "computed_styles"): for prop in ["color", "font-size", "font-family"]: if prop in node.parent.computed_styles: computed[prop] = node.parent.computed_styles[prop] # Apply node's styles (overriding inherited ones) for prop, value in node.styles.items(): computed[prop] = value # Save computed styles node.computed_styles = computed # Propagate to children for child in node.children: compute_styles(child) ############################################################################### # 5) Basic JavaScript Engine ############################################################################### class JSEngine: def __init__(self, dom_root): self.dom_root = dom_root self.global_vars = {} def execute_scripts(self): """Execute all scripts in the document""" scripts = self._collect_scripts(self.dom_root) for script in scripts: try: self._execute_script(script) except Exception as e: print(f"JavaScript Error: {str(e)}") def _collect_scripts(self, node): """Collect all script nodes""" scripts = [] if node.tag_name == "script" and node.script_code: scripts.append(node.script_code) for child in node.children: scripts.extend(self._collect_scripts(child)) return scripts def _execute_script(self, script_code): """Execute a single script""" # Very basic JS implementation - just handle variable assignments and alerts for line in script_code.split(';'): line = line.strip() if not line: continue # Handle variable assignments if '=' in line and not '==' in line: var_name, value = line.split('=', 1) var_name = var_name.strip() value = value.strip() # Handle string values if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): value = value[1:-1] # Handle numeric values elif value.isdigit(): value = int(value) elif value.replace('.', '', 1).isdigit(): value = float(value) self.global_vars[var_name] = value # Handle alert() elif line.startswith('alert(') and line.endswith(')'): content = line[6:-1].strip() if content.startswith('"') and content.endswith('"'): content = content[1:-1] print(f"ALERT: {content}") # Handle console.log() elif line.startswith('console.log(') and line.endswith(')'): content = line[12:-1].strip() if content.startswith('"') and content.endswith('"'): content = content[1:-1] print(f"CONSOLE LOG: {content}") def handle_event(self, node, event_type): """Handle a DOM event""" if event_type in node.event_handlers: handler_code = node.event_handlers[event_type] try: self._execute_script(handler_code) except Exception as e: print(f"Event Handler Error: {str(e)}") ############################################################################### # 6) LAYOUT ############################################################################### # Define inline elements INLINE_ELEMENTS = [ "span", "a", "b", "strong", "i", "em", "u", "small", "code", "mark", "img", "sub", "sup" ] class LayoutBox: def __init__(self,dom_node): self.dom_node=dom_node self.x=0 self.y=0 self.width=0 self.height=0 self.widget=None self.is_input=False self.is_textarea=False self.is_button=False self.is_image=False self.is_inline = dom_node.tag_name in INLINE_ELEMENTS # Apply display property from computed styles if available if hasattr(dom_node, 'computed_styles') and 'display' in dom_node.computed_styles: self.is_inline = dom_node.computed_styles['display'] == 'inline' self.style={ "bold": False, "italic": False, "underline": False, "color": "black", "size": 12, "margin": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "background_color": "transparent" } self.children=[] self.text_width = 0 # For measuring inline text self.line_height = 0 # For tracking line height def combine_styles(parent_style, node): s = dict(parent_style) # Apply styles from CSS computed styles if available if hasattr(node, 'computed_styles'): # Text styling if 'color' in node.computed_styles: s["color"] = node.computed_styles["color"] # Font weight if 'font-weight' in node.computed_styles: fw = node.computed_styles["font-weight"] s["bold"] = fw == "bold" or fw == "bolder" or (fw.isdigit() and int(fw) >= 700) # Font style if 'font-style' in node.computed_styles: s["italic"] = node.computed_styles["font-style"] == "italic" # Text decoration if 'text-decoration' in node.computed_styles: s["underline"] = "underline" in node.computed_styles["text-decoration"] # Font size if 'font-size' in node.computed_styles: fs = node.computed_styles["font-size"] if fs.endswith('px'): try: s["size"] = int(fs[:-2]) except ValueError: pass elif fs.endswith('pt'): try: s["size"] = int(float(fs[:-2]) * 1.33) # Approximate pt to px except ValueError: pass # Background color if 'background-color' in node.computed_styles: s["background_color"] = node.computed_styles["background-color"] # Margins for side in ["top", "right", "bottom", "left"]: margin_prop = f"margin-{side}" if margin_prop in node.computed_styles: margin_val = node.computed_styles[margin_prop] if margin_val.endswith('px'): try: s["margin"][side] = int(margin_val[:-2]) except ValueError: pass # Padding for side in ["top", "right", "bottom", "left"]: padding_prop = f"padding-{side}" if padding_prop in node.computed_styles: padding_val = node.computed_styles[padding_prop] if padding_val.endswith('px'): try: s["padding"][side] = int(padding_val[:-2]) except ValueError: pass # Apply default HTML tag styling if no CSS overrides them tn = node.tag_name if tn == "h1" and not s["bold"]: s["bold"] = True s["size"] = max(s["size"], 24) elif tn == "h2" and not s["bold"]: s["bold"] = True s["size"] = max(s["size"], 20) elif tn == "h3": s["size"] = max(s["size"], 18) elif tn == "h4": s["size"] = max(s["size"], 16) elif tn == "h5": s["size"] = max(s["size"], 14) elif tn == "h6": s["size"] = max(s["size"], 13) if tn in ["b", "strong"] and not s["bold"]: s["bold"] = True if tn in ["i", "em"] and not s["italic"]: s["italic"] = True if tn == "u" and not s["underline"]: s["underline"] = True if tn == "a" and s["color"] == "black": s["underline"] = True s["color"] = "blue" # Add proper paragraph spacing if tn == "p" and s["margin"]["bottom"] == 0: s["margin"]["bottom"] = 15 return s def estimate_text_width(text, font_size, is_bold=False): """Improved text width estimation based on font properties""" if not text: return 0 # Adjust width multiplier based on font properties if is_bold: avg_char_width = font_size * 0.65 # Bold characters are wider else: avg_char_width = font_size * 0.55 # Regular characters # Calculate more accurate width based on character types total_width = 0 for char in text: if char.isspace(): total_width += avg_char_width * 0.6 # Spaces are narrower elif char.isupper(): total_width += avg_char_width * 1.2 # Uppercase letters are wider elif char in 'ijlt': total_width += avg_char_width * 0.5 # Narrow characters elif char in 'mw': total_width += avg_char_width * 1.5 # Wide characters else: total_width += avg_char_width return total_width def layout_tree(dom_node, container_width=800, offset_x=0, offset_y=0, line_spacing=25, parent_style=None): """ Simplified layout engine with vertical positioning that preserves advanced styling """ if parent_style is None: parent_style = { "bold": False, "italic": False, "underline": False, "color": "black", "size": 12, "margin": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}, "background_color": "transparent" } root_box = LayoutBox(dom_node) root_box.style = dict(parent_style) # Simple function to calculate line height def determine_line_height(font_size): return font_size + 4 # Simple line height calculation # Recursive layout function with simpler vertical positioning def rec(node, pbox, x, y, w, st): box = LayoutBox(node) pbox.children.append(box) # Apply styles based on node type and inheritance local_st = combine_styles(st, node) box.style = local_st # Apply margins from style (we still want to keep margin handling) margin_top = local_st["margin"]["top"] margin_left = local_st["margin"]["left"] # Adjusted starting position with margins adj_x = x + margin_left adj_y = y + margin_top tag_name = node.tag_name # Handle special elements - keep the advanced code for these if tag_name == "br": box.x = adj_x box.y = adj_y box.width = 5 box.height = 5 return box if tag_name == "img": box.is_image = True box.x = adj_x box.y = adj_y img_width = 200 img_height = 150 box.width = img_width box.height = img_height return box if tag_name == "input": box.is_input = True inp_t = node.attributes.get("type", "text").lower() if inp_t == "submit": box.is_button = True box.is_input = False box.x = adj_x box.y = adj_y # Size based on input type if inp_t in ["text", "password", "email"]: input_width = min(300, w) else: input_width = 150 box.width = input_width box.height = 30 if inp_t != "checkbox" else 25 return box if tag_name == "textarea": box.is_textarea = True box.x = adj_x box.y = adj_y box.width = min(400, w) box.height = 70 return box if tag_name == "button": box.is_button = True box.x = adj_x box.y = adj_y button_width = min(200, w) box.width = button_width box.height = 30 return box # Process all child nodes with a simple vertical layout approach child_y = adj_y for c in node.children: c_box = rec(c, box, adj_x, child_y, w - margin_left, local_st) child_y = c_box.y + c_box.height # Handle text content txt = node.text.strip() if txt: line_ht = determine_line_height(local_st["size"]) lines = txt.split("\n") txt_h = line_ht * len(lines) else: txt_h = 0 # Set box dimensions box.x = adj_x box.y = adj_y box.width = w - margin_left - local_st["margin"]["right"] box.height = max(child_y - adj_y, txt_h, 25) # Ensure minimum height # Add bottom margin to final height calculation bottom_margin = local_st["margin"]["bottom"] if tag_name == "p": bottom_margin = max(bottom_margin, 15) # Extra space after paragraphs return box # Start layout process with simpler parameters rec(dom_node, root_box, offset_x, offset_y, container_width, parent_style) return root_box ############################################################################### # 7) find_form_ancestor ############################################################################### def find_form_ancestor(node): f=node.parent while f and not f.is_form: f=f.parent return f ############################################################################### # 8) RENDER => improved text positioning and background colors ############################################################################### class LinkArea: def __init__(self,x1,y1,x2,y2,href): self.x1=x1 self.y1=y1 self.x2=x2 self.y2=y2 self.href=href def gather_text_for_a(node): """ Recursively gather child text if has child nodes. If total is empty, fallback to href or '(link)'. """ if not node.children: return node.text pieces=[node.text] for c in node.children: pieces.append(gather_text_for_a(c)) return "".join(pieces) def render_layout_box(browser, layout_box, canvas, widget_list, link_areas): """ Simplified rendering function that maintains advanced styling features """ node = layout_box.dom_node st = layout_box.style x = layout_box.x y = layout_box.y w = layout_box.width h = layout_box.height # Render background color if specified (keep this advanced feature) if st.get("background_color") and st["background_color"] != "transparent": canvas.create_rectangle(x, y, x+w, y+h, fill=st["background_color"], outline="") # For debugging layout - uncomment to see box borders # canvas.create_rectangle(x, y, x+w, y+h, outline="lightgray", width=1) if layout_box.is_image: src=node.attributes.get("src","") if src: browser.draw_image(canvas, src, x, y) return if layout_box.is_button: if node.tag_name=="button": label=node.text.strip() or "Submit" else: label=node.attributes.get("value","Submit") b=tk.Button(canvas, text=label, command=lambda:browser.on_button_click(node)) canvas.create_window(x+5, y+5, anchor="nw", window=b, width=w-10, height=h-10) layout_box.widget=b widget_list.append(layout_box) return if layout_box.is_input: inp_t=node.attributes.get("type","text").lower() if inp_t=="checkbox": var=tk.BooleanVar(value=False) if "checked" in node.attributes: var.set(True) nm=node.attributes.get("name","") cb=tk.Checkbutton(canvas, text=nm, variable=var) cb.var=var canvas.create_window(x+5, y+5, anchor="nw", window=cb, width=w-10, height=h-10) layout_box.widget=cb widget_list.append(layout_box) return else: e_var=tk.StringVar() nm=node.attributes.get("name","") fa=find_form_ancestor(node) if fa and nm in fa.form_fields: old_val,_=fa.form_fields[nm] e_var.set(old_val) e=tk.Entry(canvas, textvariable=e_var) canvas.create_window(x+5, y+5, anchor="nw", window=e, width=w-10, height=h-10) layout_box.widget=e widget_list.append(layout_box) return if layout_box.is_textarea: txt=tk.Text(canvas, width=40, height=4) nm=node.attributes.get("name","") fa=find_form_ancestor(node) old_val="" if fa and nm in fa.form_fields: old_val,_=fa.form_fields[nm] if node.text.strip() and not old_val: old_val=node.text.strip() txt.insert("1.0", old_val) canvas.create_window(x+5, y+5, anchor="nw", window=txt, width=w-10, height=h-10) layout_box.widget=txt widget_list.append(layout_box) return from tkinter.font import Font col=st["color"] wght="normal" slnt="roman" und=0 if st["bold"]: wght="bold" if st["italic"]: slnt="italic" if st["underline"]: und=1 f=Font(family="Arial", size=st["size"], weight=wght, slant=slnt, underline=und) if node.tag_name=="a": link_text = gather_text_for_a(node).strip() if not link_text: link_text=node.attributes.get("href","(link)") if link_text: tid=canvas.create_text(x+5, y+5, anchor="nw", text=link_text, fill=col, font=f) canvas.update_idletasks() bbox=canvas.bbox(tid) if bbox: x1,y1,x2,y2=bbox href=node.attributes.get("href","") link_areas.append(LinkArea(x1,y1,x2,y2,href)) # Render any nested children inside for c in layout_box.children: render_layout_box(browser, c, canvas, widget_list, link_areas) else: # Normal text txt=node.text.strip() if txt: canvas.create_text(x+5, y+5, anchor="nw", text=txt, fill=col, font=f) # Render children for c in layout_box.children: render_layout_box(browser, c, canvas, widget_list, link_areas) ############################################################################### # 9) Create modern-looking buttons with gradients and hover effects ############################################################################### 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"), corner_radius=10, **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.corner_radius = corner_radius self.width = width self.height = height self.text = text self.font = font # Draw initial button self._draw_button(bg_color) # Bind events self.bind("", self._on_enter) self.bind("", self._on_leave) self.bind("", self._on_click) def _draw_button(self, color): self.delete("all") # Create gradient background for i in range(self.height): # Calculate gradient color ratio = i / self.height r1, g1, b1 = self._hex_to_rgb(color) r2, g2, b2 = self._hex_to_rgb(self._darken_color(color)) r = int(r1 + (r2 - r1) * ratio) g = int(g1 + (g2 - g1) * ratio) b = int(b1 + (b2 - b1) * ratio) gradient_color = f"#{r:02x}{g:02x}{b:02x}" # Draw gradient line self.create_line(0, i, self.width, i, fill=gradient_color) # Draw rounded rectangle border self.create_rectangle(2, 2, self.width-2, self.height-2, outline=self._lighten_color(color), width=1) # Draw text self.create_text(self.width//2, self.height//2, text=self.text, fill=self.text_color, font=self.font) def _hex_to_rgb(self, hex_color): h = hex_color.lstrip('#') return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) def _darken_color(self, hex_color, factor=0.8): r, g, b = self._hex_to_rgb(hex_color) r = max(0, int(r * factor)) g = max(0, int(g * factor)) b = max(0, int(b * factor)) return f"#{r:02x}{g:02x}{b:02x}" def _lighten_color(self, hex_color, factor=1.2): r, g, b = self._hex_to_rgb(hex_color) r = min(255, int(r * factor)) g = min(255, int(g * factor)) b = min(255, int(b * factor)) return f"#{r:02x}{g:02x}{b:02x}" def _on_enter(self, event): self._draw_button(self.hover_color) def _on_leave(self, event): self._draw_button(self.bg_color) def _on_click(self, event): if self.command: self.command() ############################################################################### # 10) BROWSER => Modern browser with JS and CSS support ############################################################################### class ToyBrowser: def __init__(self): self.root=tk.Tk() self.root.title("Modern Web Browser") self.root.geometry("1000x800") # Make default window bigger # Set a nice background color bg_color = "#f0f2f5" self.root.configure(bg=bg_color) self.history=[] self.hist_pos=-1 # Style the top navigation bar top_frame = tk.Frame(self.root, bg="#2c3e50", pady=5, padx=10) top_frame.pack(side=tk.TOP, fill=tk.X) # Create modern buttons self.back_btn = ModernButton(top_frame, "◀", self.go_back, 40, 30, bg_color="#34495e", hover_color="#3498db") self.back_btn.pack(side=tk.LEFT, padx=5) self.fwd_btn = ModernButton(top_frame, "▶", self.go_fwd, 40, 30, bg_color="#34495e", hover_color="#3498db") self.fwd_btn.pack(side=tk.LEFT, padx=5) # Create a modern URL bar url_frame = tk.Frame(top_frame, 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, insertbackground="#2c3e50") self.url_bar.pack(fill=tk.BOTH, expand=True, ipady=3) self.url_bar.bind("", self.on_url_enter) self.go_btn = ModernButton(top_frame, "Go", self.on_go_click, 50, 30, bg_color="#2ecc71", hover_color="#27ae60") self.go_btn.pack(side=tk.LEFT, padx=5) # Create a fancy bookmark bar bm_frame = tk.Frame(self.root, bg="#ecf0f1", padx=10, pady=5) bm_frame.pack(side=tk.TOP, fill=tk.X) # Create beautiful bookmark buttons self.bm1 = ModernButton(bm_frame, "This is a web page", self.bookmark_page1, 200, 30, bg_color="#e74c3c", hover_color="#c0392b") self.bm1.pack(side=tk.LEFT, padx=10) self.bm2 = ModernButton(bm_frame, "Founder's forum", self.bookmark_page2, 200, 30, bg_color="#9b59b6", hover_color="#8e44ad") self.bm2.pack(side=tk.LEFT, padx=10) self.bm3 = ModernButton(bm_frame, "Hacker News", self.bookmark_page3, 200, 30, bg_color="#f39c12", hover_color="#d35400") self.bm3.pack(side=tk.LEFT, padx=10) # Create a frame for the browser content self.frame = tk.Frame(self.root, bg=bg_color) self.frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10) # Create a canvas with a border and shadow effect canvas_frame = tk.Frame(self.frame, bg="white", bd=1, relief=tk.RAISED, highlightthickness=1, highlightbackground="#bdc3c7") canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.canvas = tk.Canvas(canvas_frame, bg="white", scrollregion=(0,0,3000,5000), highlightthickness=0) self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Modern scrollbar style = ttk.Style() style.theme_use("default") style.configure("Vertical.TScrollbar", background="#3498db", arrowcolor="white", bordercolor="#2980b9", troughcolor="#ecf0f1") self.scroll = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview, style="Vertical.TScrollbar") self.scroll.pack(side=tk.RIGHT, fill=tk.Y) self.canvas.config(yscrollcommand=self.scroll.set) # Mouse wheel binding self.canvas.bind("", self.on_mousewheel_win) self.canvas.bind("", self.on_mousewheel_lin) self.canvas.bind("", self.on_mousewheel_lin) self.canvas.bind("", self.on_canvas_click) # Status bar self.status_bar = tk.Label(self.root, text="Ready", bd=1, relief=tk.SUNKEN, anchor=tk.W, bg="#34495e", fg="white", font=("Arial", 9)) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) self.images_cache=[] self.current_dom=None self.layout_root=None self.current_url_obj=None self.form_widgets=[] self.link_areas=[] self.css_rules=[] self.js_engine=None def on_url_enter(self,evt): self.on_go_click() def bookmark_page1(self): self.url_bar.delete(0,tk.END) self.url_bar.insert(0,"https://justinjackson.ca/words.html") self.on_go_click() def bookmark_page2(self): self.url_bar.delete(0,tk.END) self.url_bar.insert(0,"http://162.208.9.114:8081/") self.on_go_click() def bookmark_page3(self): self.url_bar.delete(0,tk.END) self.url_bar.insert(0,"https://news.ycombinator.com") self.on_go_click() def go_back(self): if self.hist_pos>0: self.hist_pos-=1 url_s=self.history[self.hist_pos] self.load_url_str(url_s, push_hist=False) def go_fwd(self): if self.hist_pos {e}") return self.load_url(purl,"GET","",{}) if push_hist: 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="", extra_headers=None): if extra_headers is None: extra_headers={} try: rh, rb, fu = http_request(url_obj, method, extra_headers, body) except Exception as e: self.show_error(str(e)) return if (fu.scheme=="http" and fu.port==80) or (fu.scheme=="https" and fu.port==443): final_url = f"{fu.scheme}://{fu.host}{fu.path}" else: final_url = f"{fu.scheme}://{fu.host}:{fu.port}{fu.path}" self.url_bar.delete(0,tk.END) self.url_bar.insert(0,final_url) self.current_url_obj = fu ctype=rh.get("content-type","").lower() encoding="utf-8" if "charset=" in ctype: encp=ctype.split("charset=")[-1].split(";")[0].strip() encoding=encp try: text_data=rb.decode(encoding,"replace") except: text_data=rb.decode("utf-8","replace") if "text/html" in ctype: try: self.status_bar.config(text=f"Parsing HTML...") self.root.update() dom=parse_html(text_data) self.current_dom=dom self.canvas.delete("all") self.form_widgets.clear() self.link_areas.clear() self.images_cache.clear() # Clear image cache self.css_rules = [] # Clear CSS rules self.status_bar.config(text=f"Processing CSS...") self.root.update() # Collect and process CSS css_text = self.collect_css(dom, fu) css_text += self.collect_inline(dom) self.css_rules = parse_css(css_text) apply_css_rules(dom, self.css_rules) compute_styles(dom) self.status_bar.config(text=f"Building layout...") self.root.update() # Use a wider container width for better layout container_width = 900 self.layout_root=layout_tree(dom, container_width, 0, 0, 25) # Update canvas scroll region based on layout height max_height = self.find_max_layout_height(self.layout_root) self.canvas.config(scrollregion=(0, 0, container_width + 100, max_height + 100)) self.status_bar.config(text=f"Rendering page...") self.root.update() render_layout_box(self, self.layout_root, self.canvas, self.form_widgets, self.link_areas) self.canvas.yview_moveto(0.0) # Now, let's see if we found title_node = self.find_title(dom) if title_node and title_node.text.strip(): new_t = title_node.text.strip() self.root.title(f"{new_t} - Modern Browser") # Process JavaScript self.status_bar.config(text=f"Processing JavaScript...") self.root.update() self.js_engine = JSEngine(dom) self.js_engine.execute_scripts() self.status_bar.config(text=f"Page loaded successfully") except Exception as e: self.show_error(f"Error rendering page: {str(e)}\n{traceback.format_exc()}") else: self.canvas.delete("all") self.canvas.create_text(10,10,anchor="nw",text=text_data,fill="black",font=("Arial",12)) self.status_bar.config(text=f"Plain text content displayed") def find_max_layout_height(self, layout_box): """Find the maximum height of the layout tree to set proper scroll region""" max_height = layout_box.y + layout_box.height for child in layout_box.children: child_max = self.find_max_layout_height(child) max_height = max(max_height, child_max) return max_height def collect_css(self,dom_node, base_url_obj): s="" if dom_node.tag_name=="link": if dom_node.attributes.get("rel","").lower()=="stylesheet": href=dom_node.attributes.get("href","") if href: try: # Handle relative URLs if not href.startswith(("http://", "https://")): if href.startswith("/"): # Absolute path href = f"{base_url_obj.scheme}://{base_url_obj.host}:{base_url_obj.port}{href}" else: # Relative path path = base_url_obj.path last_slash = path.rfind("/") if last_slash > 0: base_path = path[:last_slash+1] else: base_path = "/" href = f"{base_url_obj.scheme}://{base_url_obj.host}:{base_url_obj.port}{base_path}{href}" self.status_bar.config(text=f"Loading CSS: {href}") self.root.update() css_url = parse_url(href) headers, body, _ = http_request(css_url, "GET") css_text = body.decode("utf-8", "replace") s += css_text + "\n" except Exception as e: print(f"Error loading CSS: {e}") for c in dom_node.children: s += self.collect_css(c, base_url_obj) return s def collect_inline(self,dom_node): c=dom_node.inline_css for ch in dom_node.children: c+="\n"+self.collect_inline(ch) return c def on_button_click(self,node): # Handle JavaScript event if self.js_engine and "click" in node.event_handlers: self.js_engine.handle_event(node, "click") # Handle form submission fa=find_form_ancestor(node) if fa: self.submit_form(fa) def submit_form(self,form_node): method=form_node.method.upper() action=form_node.action.strip() if action.startswith("/"): if self.current_url_obj: s=self.current_url_obj.scheme h=self.current_url_obj.host p=self.current_url_obj.port action=f"{s}://{h}:{p}{action}" if not action: if self.current_url_obj: s=self.current_url_obj.scheme h=self.current_url_obj.host p=self.current_url_obj.port path=self.current_url_obj.path action=f"{s}://{h}:{p}{path}" else: action="http://127.0.0.1/" form_data=[] for nm,(old_val,nref) in form_node.form_fields.items(): lb=self.find_layout_box_for_node(self.layout_root,nref) typed_val=old_val if lb and lb.widget: t=nref.attributes.get("type","").lower() if t=="checkbox": c=lb.widget.var.get() if c: va=nref.attributes.get("value","on") typed_val=va else: continue else: import tkinter if isinstance(lb.widget,tkinter.Entry): typed_val=lb.widget.get() elif isinstance(lb.widget,tkinter.Text): typed_val=lb.widget.get("1.0","end-1c") encn=urllib.parse.quote_plus(nm) encv=urllib.parse.quote_plus(typed_val) form_data.append(f"{encn}={encv}") q_str="&".join(form_data) if method=="GET": if "?" in action: new_url=action+"&"+q_str else: new_url=action+"?"+q_str self.load_url_str(new_url) else: newu=parse_url(action) hh={"Content-Type":"application/x-www-form-urlencoded"} self.load_url(newu,"POST",q_str,hh) def find_layout_box_for_node(self, layout_box,node): if layout_box.dom_node is node: return layout_box for c in layout_box.children: f=self.find_layout_box_for_node(c,node) if f: return f return None def draw_image(self,canvas, src, x, y): if not src: return absu=self.make_absolute_url(src) try: self.status_bar.config(text=f"Loading image: {src}...") self.root.update() hh,bb,_=http_request(absu,"GET") import io im=Image.open(io.BytesIO(bb)) tkimg=ImageTk.PhotoImage(im) self.images_cache.append(tkimg) self.canvas.create_image(x+5,y+5,anchor="nw",image=tkimg) self.status_bar.config(text=f"Page loaded successfully") except Exception as e: # Create a placeholder for failed images canvas.create_rectangle(x+5, y+5, x+105, y+65, fill="#f8f9fa", outline="#dee2e6") canvas.create_text(x+55, y+35, text="Image Load Failed", fill="#dc3545", font=("Arial", 9, "bold")) def make_absolute_url(self, raw_url): """ If link doesn't start with http:// or https://, interpret relative to self.current_url_obj. So relative links & images also work. """ if raw_url.startswith("http://") or raw_url.startswith("https://"): return parse_url(raw_url) if not self.current_url_obj: return parse_url(raw_url) if raw_url.startswith("/"): return ParsedURL(self.current_url_obj.scheme, self.current_url_obj.host, self.current_url_obj.port, raw_url) basep=self.current_url_obj.path slash=basep.rfind("/") if slash==-1: base_dir="" else: base_dir=basep[:slash] new_path=base_dir+"/"+raw_url return ParsedURL(self.current_url_obj.scheme, self.current_url_obj.host, self.current_url_obj.port, new_path) def on_canvas_click(self,event): """ On link click => bounding box => interpret relative => open link """ cx=self.canvas.canvasx(event.x) cy=self.canvas.canvasy(event.y) for la in self.link_areas: if la.x1<=cx<=la.x2 and la.y1<=cy<=la.y2: abs_link=self.make_absolute_url(la.href) final_str=self.url_to_string(abs_link) self.load_url_str(final_str) break def url_to_string(self,purl): if (purl.scheme=="http" and purl.port==80) or (purl.scheme=="https" and purl.port==443): return f"{purl.scheme}://{purl.host}{purl.path}" else: return f"{purl.scheme}://{purl.host}:{purl.port}{purl.path}" def on_mousewheel_win(self,event): self.canvas.yview_scroll(int(-1*(event.delta/120)), "units") def on_mousewheel_lin(self,event): if event.num==4: self.canvas.yview_scroll(-1, "units") else: self.canvas.yview_scroll(1, "units") def show_error(self,msg): self.canvas.delete("all") self.link_areas.clear() self.images_cache.clear() # Create an error box with modern styling error_box_width = 600 error_box_height = 300 x0 = (int(self.canvas.cget("width")) - error_box_width) // 2 y0 = 100 # Error box with gradient for i in range(error_box_height): # Create gradient effect for error box ratio = i / error_box_height r1, g1, b1 = 255, 240, 240 # Light red at top r2, g2, b2 = 220, 53, 69 # Darker red at bottom r = int(r1 + (r2 - r1) * ratio) g = int(g1 + (g2 - g1) * ratio) b = int(b1 + (b2 - b1) * ratio) gradient_color = f"#{r:02x}{g:02x}{b:02x}" self.canvas.create_line(x0, y0 + i, x0 + error_box_width, y0 + i, fill=gradient_color) # Error box border self.canvas.create_rectangle(x0, y0, x0 + error_box_width, y0 + error_box_height, outline="#721c24", width=2) # Error icon icon_size = 40 self.canvas.create_oval(x0 + 30, y0 + 30, x0 + 30 + icon_size, y0 + 30 + icon_size, fill="#dc3545", outline="#721c24") self.canvas.create_text(x0 + 30 + icon_size//2, y0 + 30 + icon_size//2, text="!", font=("Arial", 24, "bold"), fill="white") # Error title self.canvas.create_text(x0 + 100, y0 + 40, text="Error Loading Page", font=("Arial", 16, "bold"), fill="#721c24", anchor="nw") # Error message self.canvas.create_text(x0 + 30, y0 + 100, text=f"{msg}", font=("Arial", 12), fill="#721c24", anchor="nw", width=error_box_width - 60) # Update status bar self.status_bar.config(text=f"Error: {msg}") def find_title(self,node): """ Check node's tag => if it's 'title', return that node. Otherwise recursively check children. """ if node.tag_name=="title": return node for ch in node.children: found=self.find_title(ch) if found: return found return None def run(self): self.root.mainloop() ############################################################################### # main ############################################################################### if __name__=="__main__": sys.setrecursionlimit(10**6) app=ToyBrowser() app.run()