Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import socket
- import tkinter as tk
- from tkinter import ttk
- import struct
- import re
- import json
- 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 math
- import threading
- ###############################################################################
- # 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 idx < len(data) and data[idx] != 0:
- idx += 1
- idx += 1
- idx += 4
- ip_addr = None
- while idx < len(data):
- if idx >= len(data):
- break
- 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, 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 + CSS + JS
- ###############################################################################
- 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 = {}
- self.inline_css = ""
- self.script_code = ""
- self.is_form = (self.tag_name == "form")
- self.method = "get"
- self.action = ""
- self.form_fields = {}
- inline_set = {
- "text","span","a","b","strong","i","em","u","small","code","mark","img",
- "sub","sup","del","ins","abbr","kbd","q","var","s","cite","time"
- }
- self.is_inline = (self.tag_name in inline_set)
- self.id = ""
- self.classes = []
- self.display = "block"
- 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 = {}
- def __repr__(self):
- return f"<DOMNode tag='{self.tag_name}' text='{self.text[:20]}...' children={len(self.children)}>"
- def parse_html(ht):
- i = 0
- root = DOMNode("document")
- current = root
- tb = []
- def flush_text_buffer():
- nonlocal tb, current
- if not tb:
- return
- leftover = "".join(tb)
- tb = []
- leftover = leftover.replace("\r", " ").replace("\n", " ")
- leftover = leftover.replace(" ", " ")
- leftover = leftover.replace(""", "\"")
- if leftover != "":
- n = DOMNode(parent=current)
- n.tag_name = "text"
- n.text = leftover
- current.children.append(n)
- while i < len(ht):
- if ht[i] == "<":
- flush_text_buffer()
- close_i = ht.find(">", i)
- if close_i == -1:
- break
- tag_c = ht[i+1:close_i].strip()
- if tag_c.startswith("/"):
- close_tag = tag_c[1:].lower()
- if current.tag_name == close_tag and current.parent:
- current = current.parent
- else:
- up = current
- while up and up.tag_name != close_tag:
- up = up.parent
- if up and up.parent:
- current = up.parent
- i = close_i + 1
- continue
- parts = tag_c.split()
- if not parts:
- i = close_i + 1
- continue
- tname = parts[0].lower()
- nd = DOMNode(tname, parent=current)
- # attributes (simple)
- for ap in parts[1:]:
- eq = ap.find("=")
- if eq != -1:
- an = ap[:eq].lower()
- av = ap[eq+1:].strip("\"' ")
- nd.attributes[an] = av
- if an == "id":
- nd.id = av
- elif an == "class":
- nd.classes = av.split()
- elif an == "style":
- for style_part in av.split(";"):
- if ":" in style_part:
- prop, val = style_part.split(":", 1)
- nd.styles[prop.strip()] = val.strip()
- elif an.startswith("on"):
- event_type = an[2:]
- nd.event_handlers[event_type] = av
- elif an=="bgcolor":
- nd.styles["background-color"] = av
- elif an=="color" and tname=="font":
- nd.styles["color"] = av
- elif an=="align":
- if av in ("left","right","center","justify"):
- nd.styles["text-align"] = av
- elif an=="valign":
- if av in ("top","middle","bottom","baseline"):
- nd.styles["vertical-align"] = 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)
- # Self-closing or special
- if tname in ["br","hr","meta","link","img","input"]:
- 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
- if tname=="title":
- close_t = ht.find("</title>", close_i+1)
- if close_t == -1:
- i = len(ht)
- continue
- cont = ht[close_i+1 : close_t]
- cont = cont.replace("\r", " ").replace("\n", " ")
- cont = cont.replace(" "," ").replace(""","\"")
- nd.text = cont
- i = close_t + len("</title>")
- continue
- elif tname=="textarea":
- close_t = ht.find("</textarea>", close_i+1)
- if close_t == -1:
- i = len(ht)
- continue
- cont = ht[close_i+1 : close_t]
- cont = cont.replace("\r", " ").replace("\n", " ")
- cont = cont.replace(" "," ").replace(""","\"")
- 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 = close_t + len("</textarea>")
- continue
- elif tname=="style":
- close_t = ht.find("</style>", close_i+1)
- if close_t == -1:
- i = len(ht)
- continue
- st = ht[close_i+1 : close_t]
- nd.inline_css = st
- i = close_t + len("</style>")
- continue
- elif tname=="script":
- close_t = ht.find("</script>", close_i+1)
- if close_t == -1:
- i = len(ht)
- continue
- sc = ht[close_i+1 : close_t]
- nd.script_code = sc
- i = close_t + len("</script>")
- continue
- else:
- current = nd
- i = close_i + 1
- else:
- tb.append(ht[i])
- i += 1
- if tb:
- leftover = "".join(tb)
- leftover = leftover.replace("\r", " ").replace("\n", " ")
- leftover = leftover.replace(" ", " ").replace(""","\"")
- n = DOMNode(parent=current)
- n.tag_name = "text"
- n.text = leftover
- current.children.append(n)
- return root
- class CSSRule:
- def __init__(self, selector, properties):
- self.selector = selector
- self.properties = properties
- self.specificity = self._calc_spec(selector)
- def _calc_spec(self, sel):
- idc = sel.count("#")
- clc = sel.count(".")
- stripped = re.sub(r'[#\.]', ' ', sel)
- words = re.findall(r'[a-zA-Z]+', stripped)
- elc = len(words)
- return (idc, clc, elc)
- def parse_css(css_text):
- rules = []
- i = 0
- while i < len(css_text):
- bo = css_text.find("{", i)
- if bo == -1:
- break
- sel_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
- props = {}
- for line in block.split(";"):
- line = line.strip()
- if ":" in line:
- p, v = line.split(":", 1)
- props[p.strip()] = v.strip()
- for s in sel_text.split(","):
- s = s.strip()
- if s:
- rules.append(CSSRule(s, props))
- return rules
- def selector_matches(sel, node):
- if sel.lower() == node.tag_name.lower():
- return True
- if sel.startswith("#") and node.id == sel[1:]:
- return True
- if sel.startswith(".") and sel[1:] in node.classes:
- return True
- if " " in sel:
- parent_sel, child_sel = sel.rsplit(" ", 1)
- if selector_matches(child_sel, node):
- p = node.parent
- while p:
- if selector_matches(parent_sel, p):
- return True
- p = p.parent
- return False
- def apply_css_rules(node, rules):
- matched = []
- for r in rules:
- if selector_matches(r.selector, node):
- matched.append(r)
- matched.sort(key=lambda x: x.specificity)
- for m in matched:
- for k, v in m.properties.items():
- node.styles[k] = v
- for c in node.children:
- apply_css_rules(c, rules)
- def _px_to_int(v, default=0, base_for_percent=None):
- try:
- s = str(v).strip()
- if s.endswith("%") and base_for_percent is not None:
- return int(float(s[:-1]) * 0.01 * base_for_percent)
- if s.endswith("px"):
- return int(float(s[:-2]))
- if s.endswith("pt"):
- return int(float(s[:-2]) * 1.33)
- return int(float(s))
- except:
- return default
- def compute_styles(node):
- defaults = {
- "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",
- "text-align": "left",
- "vertical-align": "top"
- }
- if node.parent and hasattr(node.parent, "computed_styles"):
- for p in ["color", "font-size", "font-family"]:
- if p in node.parent.computed_styles:
- defaults[p] = node.parent.computed_styles[p]
- for k, v in node.styles.items():
- defaults[k] = v
- t = node.tag_name
- if t in ["h1", "h2", "h3", "h4", "h5", "h6"]:
- defaults["font-weight"] = "bold"
- defaults["font-size"] = {
- "h1":"24px","h2":"20px","h3":"18px","h4":"16px","h5":"14px","h6":"13px"
- }[t]
- if t in ["b", "strong"]:
- defaults["font-weight"] = "bold"
- if t in ["i", "em"]:
- defaults["font-style"] = "italic"
- if t == "u":
- defaults["text-decoration"] = "underline"
- if t == "a" and defaults["color"] == "black":
- defaults["text-decoration"] = "underline"
- defaults["color"] = "blue"
- if t == "th":
- defaults["font-weight"] = "bold"
- if t == "p":
- if _px_to_int(defaults["margin-bottom"]) < 15:
- defaults["margin-bottom"] = "15px"
- node.computed_styles = defaults
- for c in node.children:
- compute_styles(c)
- class JSEngine:
- def __init__(self, dom_root):
- self.dom_root = dom_root
- self.global_vars = {}
- def execute_scripts(self):
- scripts = self._collect_scripts(self.dom_root)
- for sc in scripts:
- try:
- self._exec(sc)
- except Exception as e:
- print("JS 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):
- for line in sc.split(";"):
- line=line.strip()
- if not line:
- continue
- if "=" in line and "==" not in line:
- var_name, value = line.split("=",1)
- var_name = var_name.strip()
- value = value.strip()
- if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
- value = value[1:-1]
- elif value.isdigit():
- value = int(value)
- elif value.replace('.', '', 1).isdigit():
- value = float(value)
- self.global_vars[var_name] = value
- elif line.startswith("alert(") and line.endswith(")"):
- c=line[6:-1].strip()
- if (c.startswith('"') and c.endswith('"')) or (c.startswith("'") and c.endswith("'")):
- c=c[1:-1]
- print("ALERT:", c)
- elif line.startswith("console.log(") and line.endswith(")"):
- c=line[12:-1].strip()
- if (c.startswith('"') and c.endswith('"')) or (c.startswith("'") and c.endswith("'")):
- c=c[1:-1]
- print("CONSOLE:", c)
- def handle_event(self, node, event_type):
- if event_type in node.event_handlers:
- code=node.event_handlers[event_type]
- try:
- self._exec(code)
- except Exception as e:
- print("JS Event Error:", e)
- ###############################################################################
- # 4) LAYOUT ENGINE (HN-focused upgrades)
- ###############################################################################
- GLOBAL_MEASURE_CANVAS = None
- class LayoutBox:
- def __init__(self, node):
- self.dom_node = node
- self.x = 0
- self.y = 0
- self.width = 0
- self.height = 0
- self.children = []
- 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",
- "text_align": "left",
- "valign": "top"
- }
- self.widget = None
- self.is_image = False
- self.is_input = False
- self.is_button = False
- self.is_textarea = False
- self.is_inline = node.is_inline
- def measure_text(txt, style):
- global GLOBAL_MEASURE_CANVAS
- if not GLOBAL_MEASURE_CANVAS:
- base = style["size"] * 0.6
- return max(0, len(txt)) * base
- weight = "bold" if style["bold"] else "normal"
- slant = "italic" if style["italic"] else "roman"
- underline = style["underline"]
- size = style["size"]
- f = Font(family="Arial", size=size, weight=weight, slant=slant, underline=underline)
- return f.measure(txt)
- def measure_lineheight(style):
- global GLOBAL_MEASURE_CANVAS
- if not GLOBAL_MEASURE_CANVAS:
- return style["size"] + 6
- weight = "bold" if style["bold"] else "normal"
- slant = "italic" if style["italic"] else "roman"
- underline = style["underline"]
- size = style["size"]
- f = Font(family="Arial", size=size, weight=weight, slant=slant, underline=underline)
- return f.metrics("linespace")
- def combine_styles(parent_style, node):
- s = dict(parent_style)
- cs = node.computed_styles
- if cs:
- if "color" in cs:
- s["color"] = cs["color"]
- fw = cs.get("font-weight","normal")
- if fw in ["bold","bolder"] or (str(fw).isdigit() and int(fw) >= 700):
- s["bold"] = True
- else:
- s["bold"] = (True if parent_style.get("bold") else False)
- if cs.get("font-style","normal") == "italic":
- s["italic"] = True
- else:
- s["italic"] = (True if parent_style.get("italic") else False)
- if "underline" in cs.get("text-decoration",""):
- s["underline"] = True
- else:
- s["underline"] = True if parent_style.get("underline") else False
- fs = cs.get("font-size","12px")
- val = 12
- if str(fs).endswith("px"):
- try:
- val = int(float(fs[:-2]))
- except:
- pass
- elif str(fs).endswith("pt"):
- try:
- val = int(float(fs[:-2]) * 1.33)
- except:
- pass
- s["size"] = val
- bc = cs.get("background-color","transparent")
- s["background_color"] = bc
- for side in ["top","right","bottom","left"]:
- s["margin"][side] = _px_to_int(cs.get(f"margin-{side}","0px"), 0)
- s["padding"][side] = _px_to_int(cs.get(f"padding-{side}","0px"), 0)
- s["text_align"] = cs.get("text-align", s.get("text_align", "left"))
- s["valign"] = cs.get("vertical-align", s.get("valign", "top"))
- t = node.tag_name
- if t in ["b","strong"]:
- s["bold"] = True
- if t in ["i","em"]:
- s["italic"] = True
- if t=="u":
- s["underline"] = True
- if t=="p" and s["margin"]["bottom"]<15:
- s["margin"]["bottom"] = 15
- return s
- def _node_text_content(node):
- if node.tag_name == "text":
- return node.text
- out = ""
- for c in node.children:
- out += _node_text_content(c)
- return out
- def layout_tree(dom_node, container_width=800, offset_x=0, offset_y=0):
- root_box = LayoutBox(dom_node)
- 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 = st["margin"]["top"]
- ml = st["margin"]["left"]
- mr = st["margin"]["right"]
- mb = st["margin"]["bottom"]
- pt = st["padding"]["top"]
- pl = st["padding"]["left"]
- pr = st["padding"]["right"]
- pb = st["padding"]["bottom"]
- # special: table width attribute (px or %)
- desired_w = None
- if node.tag_name == "table":
- if "width" in node.attributes:
- desired_w = _px_to_int(node.attributes.get("width",""), base_for_percent=avail_w)
- # default desired width
- if desired_w is None:
- desired_w = avail_w
- content_x = x + ml + pl
- content_y = y + mt + pt
- content_w = min(avail_w, desired_w) - (pl + pr)
- if content_w < 10:
- content_w = 10
- box.x = x
- box.y = y
- # Centering container
- if node.tag_name == "center":
- # layout children, then center them horizontally
- current_y = content_y
- max_w = 0
- for child in node.children:
- cb = layout_block(child, box, content_x, current_y, avail_w, st)
- max_w = max(max_w, cb.width)
- current_y = cb.y + cb.height
- # shift children to center based on widest child
- dx = (avail_w - max_w)//2
- if dx > 0:
- def shift(lb, dx):
- lb.x += dx
- for ch in lb.children:
- shift(ch, dx)
- for ch in box.children:
- shift(ch, dx)
- box.width = avail_w
- box.height = (current_y - y) + mb + pb
- return box
- # Table layout
- if node.tag_name=="table":
- layout_table(node, box, content_x, content_y, min(avail_w, desired_w), st)
- bh = 0
- for c in box.children:
- end = c.y + c.height
- if end > bh:
- bh = end
- # If the table is narrower than available, keep its own width and center within the content area
- table_inner_w = min(avail_w, desired_w)
- box.width = table_inner_w + ml + mr + pl + pr
- box.height = (bh - y) + mb + pb
- # If align center via parent <center>, we already handled. Otherwise keep left.
- return box
- # Horizontal rule
- if node.tag_name=="hr":
- box.x = content_x
- box.y = content_y
- box.width = content_w
- box.height = 2
- return box
- # Image
- if node.tag_name=="img":
- box.is_image = True
- box.x = content_x
- box.y = content_y
- wv = node.attributes.get("width","200")
- hv = node.attributes.get("height","150")
- try:
- wv = int(wv)
- except:
- wv = 200
- try:
- hv = int(hv)
- except:
- hv = 150
- wv = min(wv, content_w)
- box.width = wv
- box.height = hv
- return box
- # Input
- if node.tag_name=="input":
- box.is_input = True
- t = node.attributes.get("type","text").lower()
- if t=="submit":
- box.is_button = True
- box.is_input = False
- box.x = content_x
- box.y = content_y
- if t in ["text","password","email","search","url"]:
- box.width = min(300, content_w)
- else:
- box.width = min(150, content_w)
- box.height = 30 if t!="checkbox" else 25
- return box
- # Textarea
- if node.tag_name=="textarea":
- box.is_textarea = True
- box.x = content_x
- box.y = content_y
- box.width = min(400, content_w)
- box.height = 70
- return box
- # Button
- if node.tag_name=="button":
- box.is_button = True
- box.x = content_x
- box.y = content_y
- box.width = min(200, content_w)
- box.height = 30
- return box
- # <br> - line break
- if node.tag_name=="br":
- box.x = content_x
- box.y = content_y
- box.width = 1
- box.height = measure_lineheight(st) // 2
- return box
- # Generic block content with word wrapping
- 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
- # horizontal alignment for block text
- if line_items and st.get("text_align") in ("center","right"):
- line_width = sum(it.width for it in line_items)
- extra = content_w - line_width
- if extra > 0:
- shift = 0
- if st["text_align"] == "center":
- shift = extra//2
- elif st["text_align"] == "right":
- shift = extra
- for it in line_items:
- it.x += shift
- for it in line_items:
- it.y = current_y
- if line_items:
- current_y += line_h
- else:
- current_y += measure_lineheight(st)
- line_items.clear()
- line_x = content_x
- line_h = 0
- for child in node.children:
- if (not child.is_inline) and child.tag_name != "text":
- if line_items:
- flush_line()
- cb = layout_block(child, box, content_x, current_y, content_w, st)
- cb.height = max(cb.height, 1)
- current_y = cb.y + cb.height
- else:
- if child.tag_name == "text":
- raw = child.text
- if not raw:
- continue
- tokens = re.findall(r'\S+|\s+', raw)
- for tok in tokens:
- if tok.isspace() and not line_items:
- continue
- tbox = LayoutBox(DOMNode("text"))
- tbox.style = combine_styles(st, child)
- tbox.x = line_x
- tbox.y = current_y
- tw = measure_text(tok, tbox.style)
- th = measure_lineheight(tbox.style)
- if (line_x + tw) > (content_x + content_w) and not tok.isspace():
- flush_line()
- if tok.isspace():
- continue
- tbox.x = line_x
- tbox.y = current_y
- tw = measure_text(tok, tbox.style)
- th = measure_lineheight(tbox.style)
- tbox.width = tw
- tbox.height = th
- tbox.dom_node.text = tok
- box.children.append(tbox)
- line_items.append(tbox)
- line_x += tw
- if th > line_h:
- line_h = th
- else:
- cbox = layout_block(child, box, line_x, current_y, content_w - (line_x - content_x), st)
- if cbox.width == 0 and cbox.dom_node.tag_name != "br":
- txt = _node_text_content(cbox.dom_node)
- cbox.width = measure_text(txt, cbox.style) if txt else 0
- cbox.height = measure_lineheight(cbox.style)
- if line_x + cbox.width > (content_x + content_w) and line_items:
- flush_line()
- cbox.x = line_x
- cbox.y = current_y
- line_items.append(cbox)
- line_x += cbox.width
- if cbox.height > line_h:
- 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
- box.height = (current_y - y) + mb + pb
- return box
- def collect_table_rows(table_node):
- rows = []
- rows.extend([c for c in table_node.children if c.tag_name == "tr"])
- for c in table_node.children:
- if c.tag_name in ("tbody","thead","tfoot"):
- rows.extend([r for r in c.children if r.tag_name == "tr"])
- direct_tds = [c for c in table_node.children if c.tag_name in ("td","th")]
- if direct_tds:
- fake_tr = DOMNode("tr", parent=table_node)
- fake_tr.children = direct_tds
- rows.insert(0, fake_tr)
- return rows
- def _explicit_cell_width_px(cell, total_w):
- # width attribute or style width on td
- if "width" in cell.attributes:
- return _px_to_int(cell.attributes["width"], base_for_percent=total_w)
- if "width" in cell.styles:
- return _px_to_int(cell.styles["width"], base_for_percent=total_w)
- # image inside cell (common in HN header: 18x18 logo)
- for ch in cell.children:
- if ch.tag_name == "img":
- w = ch.attributes.get("width", "")
- if w:
- return _px_to_int(w, base_for_percent=total_w)
- return None
- def _cell_min_width_heuristic(cell, total_w, cell_index):
- # Heuristics tailored for HN:
- # - rank column (first .title cell): width of text like "123."
- # - votelinks column: tiny arrow area (~16-18px)
- classes = set(cell.classes)
- if "votelinks" in classes:
- return 18
- if "title" in classes and cell_index == 0:
- txt = _node_text_content(cell).strip() or "99."
- return max(18, _px_to_int(measure_text(txt, {"size":12,"bold":False,"italic":False,"underline":False}), 18))
- return None
- def _colspan(cell):
- try:
- return int(cell.attributes.get("colspan","1"))
- except:
- return 1
- def layout_table(node, pbox, x, y, w, st):
- # table attributes
- cp = _px_to_int(node.attributes.get("cellpadding","0"))
- cs = _px_to_int(node.attributes.get("cellspacing","0"))
- row_nodes = collect_table_rows(node)
- # Determine column count by max colspan sum
- ncols = 0
- for rn in row_nodes:
- s = 0
- for c in rn.children:
- if c.tag_name in ("td","th"):
- s += _colspan(c)
- ncols = max(ncols, s)
- if ncols == 0:
- pbox.width = w
- pbox.height = 0
- return
- # First pass: gather fixed/min widths
- col_fixed = [0]*ncols
- col_min = [0]*ncols
- for rn in row_nodes:
- ci = 0
- for c in rn.children:
- if c.tag_name not in ("td","th"):
- continue
- span = _colspan(c)
- exp = _explicit_cell_width_px(c, w)
- heur = _cell_min_width_heuristic(c, w, ci)
- want = exp if exp is not None else heur
- if want is not None:
- # apply to the first column in the span if span==1, otherwise distribute minimally
- if span == 1:
- col_fixed[ci] = max(col_fixed[ci], want)
- else:
- share = want // span
- for k in range(ci, ci+span):
- col_fixed[k] = max(col_fixed[k], share)
- else:
- # estimate minimal width from a short content sample
- txt = _node_text_content(c).strip()
- if txt:
- tokens = re.findall(r'\S+', txt)
- longest = max(tokens, key=len) if tokens else ""
- mw = int(measure_text(longest, {"size":12,"bold":False,"italic":False,"underline":False})) + 2*cp
- # spread across span
- if span == 1:
- col_min[ci] = max(col_min[ci], mw)
- else:
- share = mw // span
- for k in range(ci, ci+span):
- col_min[k] = max(col_min[k], share)
- ci += span
- # Build initial widths
- col_w = [max(col_fixed[i], col_min[i]) for i in range(ncols)]
- total_spacing = (ncols+1) * cs # simple outer spacing model
- remaining = max(0, w - total_spacing - sum(col_w))
- # Give all remaining width to the last column (HN titles)
- if remaining > 0:
- col_w[-1] += remaining
- # Row layout
- row_y = y
- used_h = 0
- for rn in row_nodes:
- rbox = LayoutBox(rn)
- pbox.children.append(rbox)
- rbox.style = combine_styles(st, rn)
- rbox.x = x
- rbox.y = row_y
- ci = 0
- cx = x + cs
- maxh = 0
- for cnode in rn.children:
- if cnode.tag_name not in ("td","th"):
- continue
- span = _colspan(cnode)
- width_span = sum(col_w[ci:ci+span]) + (span-1)*cs
- # cell background from styles
- cbox = LayoutBox(cnode)
- rbox.children.append(cbox)
- cbox.style = combine_styles(rbox.style, cnode)
- cbox.x = cx
- cbox.y = row_y + cs
- inner_x = cbox.x + cp
- inner_y = cbox.y + cp
- inner_w = max(1, width_span - 2*cp)
- # layout children inside the cell
- # (use generic block layout for each child)
- cy = inner_y
- temp_parent = LayoutBox(DOMNode("cell-content"))
- cbox.children.append(temp_parent)
- # background color draw uses cbox.style in renderer
- for cc in cnode.children:
- cb = layout_block(cc, temp_parent, inner_x, cy, inner_w, cbox.style)
- cy = cb.y + cb.height
- cell_h = max(cy - (row_y + cs) + cp, measure_lineheight(cbox.style) + 2*cp)
- cbox.width = width_span
- cbox.height = cell_h
- # advance column
- cx += width_span + cs
- ci += span
- if cbox.height > maxh:
- maxh = cbox.height
- if ci == 0:
- # empty row (e.g., spacer with style height)
- rh = _px_to_int(rn.styles.get("height","0px"))
- maxh = max(maxh, rh, measure_lineheight(rbox.style)//2)
- rbox.width = w
- rbox.height = maxh + cs
- row_y += rbox.height
- used_h += rbox.height
- pbox.width = w
- pbox.height = used_h
- 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:
- end = find_box_bottom(c)
- if end > my:
- my = end
- return my
- ###############################################################################
- # 5) find_form_ancestor
- ###############################################################################
- def find_form_ancestor(node):
- p = node.parent
- while p and not p.is_form:
- p = p.parent
- return p
- ###############################################################################
- # 6) RENDER
- ###############################################################################
- 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 render_layout_box(browser, lb, canvas, widget_list, link_areas):
- st = lb.style
- x = lb.x
- y = lb.y
- w = lb.width
- h = lb.height
- node = lb.dom_node
- bc = st["background_color"]
- if bc and bc != "transparent":
- canvas.create_rectangle(x, y, x+w, y+h, fill=bc, outline="")
- if node.tag_name=="hr":
- canvas.create_line(x, y+1, x+w, y+1, fill="#aaa")
- return
- if lb.is_image:
- src = node.attributes.get("src","")
- if src:
- browser.draw_image(canvas, src, x, y)
- return
- if lb.is_button:
- if node.tag_name=="button":
- label = node.text if node.text else "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)
- lb.widget = b
- widget_list.append(lb)
- return
- if lb.is_input:
- t = node.attributes.get("type","text").lower()
- if 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)
- lb.widget = cb
- widget_list.append(lb)
- 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)
- lb.widget = e
- widget_list.append(lb)
- return
- if lb.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 and not old_val:
- old_val = node.text
- txt.insert("1.0", old_val)
- canvas.create_window(x+5, y+5, anchor="nw", window=txt, width=w-10, height=h-10)
- lb.widget = txt
- widget_list.append(lb)
- return
- # text
- wght = "bold" if st["bold"] else "normal"
- slnt = "italic" if st["italic"] else "roman"
- und = 1 if st["underline"] else 0
- color = st["color"]
- f = Font(family="Arial", size=st["size"], weight=wght, slant=slnt, underline=und)
- if node.tag_name=="a":
- href = node.attributes.get("href","")
- child_coords = []
- for c in lb.children:
- render_layout_box(browser, c, canvas, widget_list, link_areas)
- cx1 = c.x
- cy1 = c.y
- cx2 = c.x + c.width
- cy2 = c.y + c.height
- child_coords.append((cx1, cy1, cx2, cy2))
- if child_coords and href:
- minx = min(coord[0] for coord in child_coords)
- miny = min(coord[1] for coord in child_coords)
- maxx = max(coord[2] for coord in child_coords)
- maxy = max(coord[3] for coord in child_coords)
- link_areas.append(LinkArea(minx, miny, maxx, maxy, href))
- else:
- if node.tag_name == "text" and node.text:
- # horizontal alignment inside a cell line is already handled in layout
- canvas.create_text(x+5, y+5, anchor="nw", text=node.text, fill=color, font=f)
- for c in lb.children:
- render_layout_box(browser, c, canvas, widget_list, link_areas)
- ###############################################################################
- # 7) Modern Button
- ###############################################################################
- 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
- self._draw_button(bg_color)
- self.bind("<Enter>", self._on_enter)
- self.bind("<Leave>", self._on_leave)
- self.bind("<Button-1>", self._on_click)
- def _draw_button(self, color):
- self.delete("all")
- for i in range(self.height):
- 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}"
- self.create_line(0, i, self.width, i, fill=gradient_color)
- self.create_rectangle(2, 2, self.width-2, self.height-2,
- outline=self._lighten_color(color), width=1)
- self.create_text(self.width//2, self.height//2,
- text=self.text, fill=self.text_color, font=self.font)
- def _hex_to_rgb(self, hc):
- hc = hc.lstrip("#")
- return tuple(int(hc[i:i+2],16) for i in (0,2,4))
- def _darken_color(self, hc, factor=0.8):
- r,g,b = self._hex_to_rgb(hc)
- 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, hc, factor=1.2):
- r,g,b = self._hex_to_rgb(hc)
- 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()
- ###############################################################################
- # 8) BROWSER
- ###############################################################################
- class ToyBrowser:
- def __init__(self):
- self.root = tk.Tk()
- self.root.title("Refactored Web Browser - Clickable Links Preserved")
- self.root.geometry("1000x800")
- 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_frame = tk.Frame(self.root, bg="#2c3e50", pady=5, padx=10)
- top_frame.pack(side=tk.TOP, fill=tk.X)
- 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)
- 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("<Return>", 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)
- bm_frame = tk.Frame(self.root, bg="#ecf0f1", padx=10, pady=5)
- bm_frame.pack(side=tk.TOP, fill=tk.X)
- 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)
- self.frame = tk.Frame(self.root, bg=bg)
- self.frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
- 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)
- 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)
- self.canvas.bind("<MouseWheel>", self.on_mousewheel_win)
- self.canvas.bind("<Button-4>", self.on_mousewheel_lin)
- self.canvas.bind("<Button-5>", self.on_mousewheel_lin)
- self.canvas.bind("<Button-1>", self.on_canvas_click)
- # Extra mouse buttons for back & forward (Linux style).
- self.canvas.bind("<Button-4>", self.on_mouse_back_forward)
- self.canvas.bind("<Button-5>", self.on_mouse_back_forward)
- 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_mouse_back_forward(self, event):
- if event.num == 4:
- self.go_back()
- elif event.num == 5:
- self.go_fwd()
- 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 < len(self.history) - 1:
- self.hist_pos += 1
- url_s = self.history[self.hist_pos]
- self.load_url_str(url_s, push_hist=False)
- def on_go_click(self):
- raw = self.url_bar.get().strip()
- self.load_url_str(raw, True)
- def load_url_str(self, url_s, push_hist=True):
- try:
- self.status_bar.config(text=f"Loading {url_s}...")
- self.root.update()
- purl = parse_url(url_s)
- except Exception as e:
- self.show_error(str(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="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()
- self.css_rules = []
- self.status_bar.config(text="Processing CSS...")
- self.root.update()
- cst = self.collect_css(dom, fu)
- cst += self.collect_inline(dom)
- self.css_rules = parse_css(cst)
- apply_css_rules(dom, self.css_rules)
- compute_styles(dom)
- self.status_bar.config(text="Building layout...")
- self.root.update()
- self.layout_root = layout_tree(dom, 800, 0, 0)
- final_y = find_box_bottom(self.layout_root)
- self.canvas.config(scrollregion=(0,0,900,final_y+50))
- self.status_bar.config(text="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)
- t = self.find_title(dom)
- if t and t.text.strip():
- self.root.title(t.text.strip() + " - Browser")
- self.status_bar.config(text="Running JS...")
- self.root.update()
- self.js_engine = JSEngine(dom)
- self.js_engine.execute_scripts()
- self.status_bar.config(text="Page loaded successfully")
- except Exception as e:
- self.show_error(f"Error rendering page: {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="Plain text content displayed")
- def collect_css(self, node, baseurl):
- s = ""
- if node.tag_name=="link":
- if node.attributes.get("rel","").lower()=="stylesheet":
- href = node.attributes.get("href","").strip()
- if href:
- try:
- if not href.startswith(("http://","https://")):
- if href.startswith("/"):
- href = f"{baseurl.scheme}://{baseurl.host}:{baseurl.port}{href}"
- else:
- p = baseurl.path
- slash = p.rfind("/")
- if slash>0:
- base_dir = p[:slash+1]
- else:
- base_dir = "/"
- href = f"{baseurl.scheme}://{baseurl.host}:{baseurl.port}{base_dir}{href}"
- self.status_bar.config(text=f"Loading CSS => {href}")
- self.root.update()
- cu = parse_url(href)
- hh, bb, _ = http_request(cu, "GET")
- cst = bb.decode("utf-8", "replace")
- s += cst + "\n"
- except Exception as e:
- print("CSS load error:", e)
- for c in node.children:
- s += self.collect_css(c, baseurl)
- return s
- def collect_inline(self, node):
- c = node.inline_css
- for ch in node.children:
- c += "\n" + self.collect_inline(ch)
- return c
- def find_title(self, node):
- if node.tag_name=="title":
- return node
- for c in node.children:
- ans = self.find_title(c)
- if ans:
- return ans
- return None
- def on_button_click(self, node):
- if self.js_engine and "click" in node.event_handlers:
- self.js_engine.handle_event(node, "click")
- fa = find_form_ancestor(node)
- if fa:
- self.submit_form(fa)
- def submit_form(self, form_node):
- m = form_node.method.upper()
- act = form_node.action.strip()
- if act.startswith("/"):
- if self.current_url_obj:
- s = self.current_url_obj.scheme
- h = self.current_url_obj.host
- p = self.current_url_obj.port
- act = f"{s}://{h}:{p}{act}"
- if not act:
- 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
- act = f"{s}://{h}:{p}{path}"
- else:
- act = "http://127.0.0.1/"
- arr = []
- for nm, (ov, nref) in form_node.form_fields.items():
- lb = self.find_layout_box_for_node(self.layout_root, nref)
- tv = ov
- if lb and lb.widget:
- t = nref.attributes.get("type","").lower()
- if t=="checkbox":
- c = lb.widget.var.get()
- if c:
- vv = nref.attributes.get("value","on")
- tv = vv
- else:
- continue
- else:
- import tkinter
- if isinstance(lb.widget, tkinter.Entry):
- tv = lb.widget.get()
- elif isinstance(lb.widget, tkinter.Text):
- tv = lb.widget.get("1.0","end-1c")
- encn = urllib.parse.quote_plus(nm)
- encv = urllib.parse.quote_plus(tv)
- arr.append(f"{encn}={encv}")
- qstr = "&".join(arr)
- if m=="GET":
- if "?" in act:
- finalurl = act + "&" + qstr
- else:
- finalurl = act + "?" + qstr
- self.load_url_str(finalurl)
- else:
- p = parse_url(act)
- hh = {"Content-Type":"application/x-www-form-urlencoded"}
- self.load_url(p, "POST", qstr, hh)
- def find_layout_box_for_node(self, lb, node):
- if lb.dom_node is node:
- return lb
- for c in lb.children:
- ans = self.find_layout_box_for_node(c, node)
- if ans:
- return ans
- return None
- def draw_image(self, canvas, src, x, y):
- """
- Threaded image fetch so the UI doesn't freeze.
- SVG fallback: draw a tiny placeholder (PIL can't parse SVG).
- """
- if not src:
- return
- def announce_download():
- self.status_bar.config(text=f"Loading image => {src}")
- self.root.after(0, announce_download)
- def worker():
- absu = self.make_absolute_url(src)
- try:
- # simple SVG check
- if absu.path.lower().endswith(".svg"):
- raise Exception("SVG not supported by PIL")
- hh, bb, _ = http_request(absu, "GET")
- import io
- im = Image.open(io.BytesIO(bb))
- tkimg = ImageTk.PhotoImage(im)
- def do_main():
- self.images_cache.append(tkimg)
- canvas.create_image(x+5, y+5, anchor="nw", image=tkimg)
- self.status_bar.config(text="Done loading image.")
- self.root.after(0, do_main)
- except Exception:
- def do_err():
- # placeholder keeps layout stable (e.g., HN logo 18x18)
- canvas.create_rectangle(x+5, y+5, x+23, y+23,
- fill="#ffffff", outline="#ffffff")
- self.root.after(0, do_err)
- t = threading.Thread(target=worker, daemon=True)
- t.start()
- def make_absolute_url(self, raw):
- if raw.startswith("http://") or raw.startswith("https://"):
- return parse_url(raw)
- if not self.current_url_obj:
- return parse_url(raw)
- if raw.startswith("//"):
- return ParsedURL(self.current_url_obj.scheme,
- raw.split("//",1)[1].split("/")[0],
- 443 if self.current_url_obj.scheme=="https" else 80,
- "/" + raw.split("//",1)[1].split("/",1)[1] if "/" in raw.split("//",1)[1] else "/")
- if raw.startswith("/"):
- return ParsedURL(self.current_url_obj.scheme,
- self.current_url_obj.host,
- self.current_url_obj.port,
- raw)
- bp = self.current_url_obj.path
- slash = bp.rfind("/")
- base_dir = bp[:slash] if slash != -1 else ""
- newp = base_dir + "/" + raw
- return ParsedURL(self.current_url_obj.scheme,
- self.current_url_obj.host,
- self.current_url_obj.port,
- newp)
- def on_canvas_click(self, event):
- 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:
- absu = self.make_absolute_url(la.href)
- final_s = self.url_to_string(absu)
- self.load_url_str(final_s)
- break
- def url_to_string(self, p):
- if (p.scheme=="http" and p.port==80) or (p.scheme=="https" and p.port==443):
- return f"{p.scheme}://{p.host}{p.path}"
- else:
- return f"{p.scheme}://{p.host}:{p.port}{p.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()
- ewidth = 600
- eheight = 300
- x0 = (int(self.canvas.cget("width")) - ewidth) // 2
- y0 = 100
- for i in range(eheight):
- ratio = i / eheight
- r1,g1,b1 = 255,240,240
- r2,g2,b2 = 220,53,69
- r = int(r1 + (r2-r1)*ratio)
- g = int(g1 + (g2-g1)*ratio)
- b = int(b1 + (b2-b1)*ratio)
- c = f"#{r:02x}{g:02x}{b:02x}"
- self.canvas.create_line(x0, y0+i, x0+ewidth, y0+i, fill=c)
- self.canvas.create_rectangle(x0, y0, x0+ewidth, y0+eheight,
- outline="#721c24", width=2)
- icon = 40
- self.canvas.create_oval(x0+30, y0+30, x0+30+icon, y0+30+icon,
- fill="#dc3545", outline="#721c24")
- self.canvas.create_text(x0+30+icon//2, y0+30+icon//2, text="!",
- font=("Arial",24,"bold"), fill="white")
- self.canvas.create_text(x0+100, y0+40, text="Error Loading Page",
- font=("Arial",16,"bold"), fill="#721c24",
- anchor="nw")
- self.canvas.create_text(x0+30, y0+100, text=msg,
- font=("Arial",12), fill="#721c24",
- anchor="nw", width=ewidth-60)
- self.status_bar.config(text=f"Error: {msg}")
- def run(self):
- self.root.mainloop()
- ###############################################################################
- # main
- ###############################################################################
- if __name__=="__main__":
- sys.setrecursionlimit(10**6)
- app = ToyBrowser()
- app.run()
Advertisement
Add Comment
Please, Sign In to add comment