bowser/src/browser/tab.py
Benedikt Willi d3119f0b10 Add automatic HTTPS protocol assumption for URLs
Feature: Smart protocol handling
- If no protocol is provided, assume https://
- URLs like 'example.com' become 'https://example.com'
- 'example.com/path' becomes 'https://example.com/path'
- Special 'about:' URLs are preserved as-is

Implementation:
- Added _normalize_url() method to Browser class
- Checks for '://' in URL to detect existing protocol
- Strips whitespace from URLs
- Applied in both new_tab() and navigate_to() methods
- Supports all URL formats (subdomains, ports, paths, queries)

URL Normalization Logic:
1. Strip leading/trailing whitespace
2. Check if URL already has a protocol ('://')
3. Check for special about: URLs
4. Otherwise prepend 'https://'

Examples:
- 'example.com' → 'https://example.com'
- 'https://example.com' → 'https://example.com' (unchanged)
- 'about:startpage' → 'about:startpage' (unchanged)
- 'www.example.com:8080' → 'https://www.example.com:8080'
- 'localhost:3000' → 'https://localhost:3000'

Tests added (10 test cases):
- test_normalize_url_with_https
- test_normalize_url_with_http
- test_normalize_url_without_protocol
- test_normalize_url_with_path
- test_normalize_url_with_about
- test_normalize_url_strips_whitespace
- test_normalize_url_with_query_string
- test_normalize_url_with_subdomain
- test_normalize_url_with_port
- test_normalize_url_localhost

Existing tests still passing (15/15)
2026-01-09 14:52:05 +01:00

101 lines
3.4 KiB
Python

"""Tab and frame orchestration stubs."""
from typing import Optional
import logging
from ..network.url import URL
from ..network import http
from ..parser.html import parse_html, Element
from ..templates import render_startpage, render_error_page
class Frame:
def __init__(self, tab: "Tab", parent_frame=None, frame_element=None):
self.tab = tab
self.parent_frame = parent_frame
self.frame_element = frame_element
self.document: Optional[Element] = None
def load(self, url: URL, payload: Optional[bytes] = None):
"""Fetch and parse the URL content."""
logger = logging.getLogger("bowser.frame")
# Handle special about: URLs
url_str = str(url)
if url_str.startswith("about:startpage"):
html = render_startpage()
self.document = parse_html(html)
self.tab.current_url = url
return
try:
status, content_type, body = http.request(url, payload)
if status == 200:
# Decode response
text = body.decode('utf-8', errors='replace')
# Parse HTML
self.document = parse_html(text)
self.tab.current_url = url
else:
# Error handling - show error page
html = render_error_page(status, str(url))
self.document = parse_html(html)
except Exception as e:
# Network error - show error page
html = render_error_page(0, str(url), str(e))
self.document = parse_html(html)
logger.error(f"Failed to load {url}: {e}")
class Tab:
def __init__(self, browser: "Browser", tab_height: int = 40):
self.browser = browser
self.tab_height = tab_height
self.current_url: Optional[URL] = None
self.main_frame = Frame(self)
self.history: list[URL] = []
self.history_index: int = -1
def load(self, url: URL, payload: Optional[bytes] = None):
# push into history (truncate forward)
if self.history_index < len(self.history) - 1:
self.history = self.history[: self.history_index + 1]
self.history.append(url)
self.history_index += 1
self.browser._log(f"Tab load: {url}", logging.INFO)
self.main_frame.load(url, payload)
def go_back(self) -> bool:
if self.history_index > 0:
self.history_index -= 1
url = self.history[self.history_index]
self.browser._log("Tab go_back", logging.DEBUG)
self.main_frame.load(url)
return True
return False
def go_forward(self) -> bool:
if self.history_index < len(self.history) - 1:
self.history_index += 1
url = self.history[self.history_index]
self.browser._log("Tab go_forward", logging.DEBUG)
self.main_frame.load(url)
return True
return False
def reload(self) -> bool:
if 0 <= self.history_index < len(self.history):
url = self.history[self.history_index]
self.browser._log("Tab reload", logging.DEBUG)
self.main_frame.load(url)
return True
return False
@property
def title(self) -> str:
if self.current_url is None:
return "New Tab"
return str(self.current_url)