Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# TODO

Concrete, code-level roadmap for theBrowser, ordered by priority: rendering correctness first (make real pages and Acid1 render better), long-term fronts (JS) last. High-level status lives in [README.md](README.md); this list tracks the actual gaps found in the code.

## 1. Quick fixes / known bugs

High impact, small effort — mostly straight-up bugs.

- [x] `computed_style` percentage font-size with a parent falls through and returns nothing — the `%` branch only handles the no-parent path (`web/css/utils.py:19-34`)
- [x] `raise NotImplemented` uses the non-exception constant, so it never actually raises (`web/html/parser/utils.py:136`)
- [x] rem/em handling assigns a `str` to `font_size` and then multiplies with it (`browser/layouts/Layout.py:151-212`, `browser/layouts/ImageLayout.py:74,112`)
- [x] `convert_absolute_size_to_pixels` is a stub that always returns 16 — keyword font sizes (`small`/`medium`/`large`/...) are ignored (`browser/styling/font/utils.py:7`)
- [x] `scroll_down` uses window *width* instead of height for max scroll (`browser/Browser.py:294`)
- [x] Border and padding both write `internal_padding` and clobber each other (`browser/layouts/Layout.py:293,308`)
- [x] `DrawBorder.calculate_offset` body is commented out (always returns `(0, 0)`), so per-side borders with differing widths misalign at corners (`browser/elements/elements.py:143-165`)
- [x] `raster()` writes debug files `rules.txt` and `document.html` to CWD on every page load (`browser/Browser.py:262-267`); stray debug prints (`browser/layouts/ImageLayout.py:102`, `browser/styling/color/utils.py:185`)
- [x] Bare `except: pass` hides all page-load errors (`browser/Browser.py:98`)
- [x] `EventInit` trailing commas turn `bubbles`/`cancelable` into tuples (`web/dom/events/Event.py:10-11`)
- [x] `DomException` subclasses `BaseException` — should be `Exception`, and should actually be raised from DOM mutation code (`web/dom/exceptions/DomException.py`)

## 2. CSS correctness

The biggest rendering-correctness lever: cascade and selector matching.

- [ ] Correct specificity — `(id, class, tag)` tuple instead of hardcoded `priority = 1` (`web/css/TagSelector.py:6`, `web/css/DescendantSelector.py:9`, `cascade_priority` in `web/css/utils.py`)
- [ ] Honor `!important` — it's parsed but the flag is discarded (`web/css/CSSParser.py:66-69`)
- [ ] Shorthand property expansion: `margin`, `padding`, `border`, `font`, `background`
- [ ] More selectors: universal `*`, child `>`, sibling `+`/`~`, attribute `[attr]`, basic pseudo-classes
- [ ] Broaden inherited properties beyond the current 4 (`web/css/utils.py:7-12`) and unit resolution beyond font-size px/% (proper `em`/`rem`/`pt`)
- [ ] `@media` blocks are currently swallowed and their rules discarded — parse and evaluate (`web/css/CSSParser.py:122-136`)
- [ ] Colors: `#RRGGBBAA` hex, `hsl()`/`hsla()`, `currentColor`; `initial` shouldn't hardcode black (`browser/styling/color/utils.py:161,189`)

## 3. Layout

Acid1 lives or dies here.

- [ ] Proper float support: `clear`, a shared float context instead of copy-pasted left/right branches across BlockLayout/InlineLayout/TableLayout/dl (`browser/layouts/Layout.py:121`)
- [ ] Margins: `auto` centering, margin collapsing, negative margins (`browser/layouts/Layout.py:337-383`)
- [ ] `min-width`/`max-width`/`min-height`/`max-height` (commented out in `browser/layouts/InputLayout.py:48-53`), `calc()` (currently dropped, `browser/layouts/Layout.py:163`), `max-content` (`browser/layouts/Layout.py:222`)
- [ ] Units: `vh`, proper `em` vs `rem` distinction, `ch`/`ex`/`cm`/`mm`/`in`
- [ ] `display: inline-block`; `position: relative/absolute/fixed` and `z-index` — nothing reads `position` today
- [ ] Table: colspan/rowspan, `thead`/`tfoot`/`caption`, `border-collapse`, a real width algorithm instead of the current heuristic (`browser/layouts/table/TableLayout.py:196-218,261-271`)
- [ ] `dl`: distinguish `dt` vs `dd` (indentation) (`browser/layouts/dl/`)
- [ ] Remove BlockLayout hacks: default `height = 10` and the body re-layout loop (`browser/layouts/BlockLayout.py:13,45-48`)

## 4. Painting / rendering

- [ ] `border-style` (only solid supported) and `border-radius` (`browser/layouts/Layout.py:289`)
- [ ] `text-decoration` (underline/line-through), `text-align`, `white-space` handling
- [ ] `font-family` — currently ignored, tkinter default is always used (`browser/layouts/utils.py`)
- [ ] Re-enable emoji image rendering — the code path is commented out, emojis render as plain text (`browser/Browser.py:335-350`, `browser/elements/elements.py:42-54`)
- [ ] `background-image`; proper rgba rendering instead of the per-rect PIL image workaround (`browser/elements/elements.py:106,232`)

## 5. HTML parsing (spec completeness)

- [ ] Adoption-agency algorithm, reconstruct-active-formatting-elements, generate-implied-end-tags (`web/html/parser/HTMLDocumentParser.py:201,534-694,743`)
- [ ] Table insertion modes — InTable/InTableText/InCaption/InColumnGroup/InTableBody/InRow/InCell/InSelect are empty stubs (`web/html/parser/HTMLDocumentParser.py:875-900`)
- [ ] Frameset / after-body / after-after modes (`web/html/parser/HTMLDocumentParser.py:939-949`)
- [ ] Missing tokenizer states: DOCTYPE public/system identifiers, CDATA sections, PLAINTEXT, script-data-double-escaped, after-attribute-name, comment-end-bang (`web/html/parser/HTMLTokenizerRefactored.py:277,719-731,776,1012,1041-1086`)
- [ ] Named character reference state-switch TODO (`web/html/parser/HTMLTokenizerRefactored.py:1122`) and numeric noncharacter fix-up (~1211)
- [ ] Quirks-mode detection (`web/html/parser/HTMLDocumentParser.py:311`) and charset handling (`372,568`)
- [ ] Delete dead code: old `web/html/parser/HTMLTokenizer.py` (imported nowhere — parser uses `HTMLTokenizerRefactored`) and the `libs/JSlib/JavaScript.py` stub

## 6. Networking & forms

- [ ] Real POST form submission — `submit_form` always builds a GET query string, ignoring `method`/`enctype` (`browser/Browser.py:197-214`)
- [ ] Serialize `select`/`textarea`/checkbox semantics in form data; radio buttons don't deselect their group siblings (`browser/Browser.py:177-182`)
- [ ] `data:` URLs; handle `request()` returning `None` for unsupported schemes without crashing (`browser/utils/networking.py:37`)
- [ ] Replace hand-rolled `resolve_url` with `urllib.parse.urljoin` (`browser/utils/networking.py:16-35`)
- [ ] Cookies, explicit redirect handling, request timeouts; `REQUEST_CACHE` never invalidates and ignores Cache-Control (`browser/utils/networking.py:10,40,54`)

## 7. Browser chrome

- [ ] History with back/forward buttons
- [ ] Tabs
- [ ] Bookmarks
- [ ] Text input: caret positioning, selection, clipboard, tab between fields; `textarea` and `select` support
- [ ] Inspector: computed-styles panel; network view appends without clearing so rows duplicate (`browser/Inspector.py:100-102`); scrollbar created twice and wired to the wrong tree (`browser/Inspector.py:88-93`)

## 8. JavaScript (long-term)

- [ ] Lexer/parser for JS source — none exists, the AST can't be built from text
- [ ] Interpreter scope handling — `__enter_scope`/`__exit_scope` raise `NotImplementedError`, so `run()` always throws (`web/js/Interpreter.py:26-29`)
- [ ] Fix `ASTNode.execute` signature mismatches and implement the missing `execute` methods (`web/js/ASTNode.py`)
- [ ] DOM events: listener storage, `dispatchEvent`, propagation, `preventDefault`/`stopPropagation` — `EventTarget.add_event_listener` raises today (`web/dom/events/EventTarget.py:14`)
- [ ] Wire `<script>` execution into the parser (`web/html/parser/HTMLDocumentParser.py:395,578`) and expose DOM bindings to the interpreter

## 9. Testing & infra

- [ ] `test.sh` only discovers `tests/` — include `web/js/tests` (currently never run in CI)
- [ ] Fill the empty test stub (`tests/test_HTMLTokenizer.py:9`)
- [ ] Add tests for the `browser/` side — layout classes, color/font utils, networking, CSS cascade have zero coverage
- [ ] Automated Acid1 regression (render + screenshot compare, or layout-tree assertions) instead of the manual screenshot in the README; consider adding an acid2 page to `acid_tests/`
20 changes: 8 additions & 12 deletions browser/Browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,15 @@ def __init__(self) -> None:
with open("./browser/styling/defaults/browser.css") as file:
self.default_style_sheet = CSSParser(file.read()).parse()

def check_key(self, event):
def check_key(self, event: tkinter.Event) -> Optional[str]:
# Ignore the 'Return' key
if event.keysym == "Return":
try:
self.load_webpage()
except:
pass
except Exception as e:
logging.log("Failed to load page:", e)
return "break"
return None

def init_emojis(self) -> List[str]:
from os import listdir
Expand Down Expand Up @@ -259,12 +260,7 @@ def raster(self, dom: DocumentType):
child = cast(CharacterData, child)
rules.extend(CSSParser(child.data).parse())

with open("rules.txt", "w") as f:
for rule in rules:
f.write(str(rule.__dict__) + "\n")
style(dom, sorted(rules, key=cascade_priority))
with open("document.html", "w") as f:
f.write(str(dom))
self.document = DocumentLayout(dom)
[inspector.update_dom(dom) for inspector in BrowserState.get_inspectors()]
self.document.height = BrowserState.get_window_size()[1]
Expand All @@ -291,13 +287,13 @@ def handle_scroll(self, direction: tkinter.Event):

def scroll_down(self, delta: int):
delta = delta * -1
max_y = (self.document.content_height - BrowserState.get_window_size()[0]) + 15
max_y = (self.document.content_height - BrowserState.get_window_size()[1]) + 15
scroll = min(self.scroll + (delta * SCROLL_STEP), max_y)
if scroll <= 0:
self.scroll = 0
else:
self.scroll = scroll
self.scrollbar.set((self.scroll/self.document.content_height), ((self.scroll + BrowserState.get_window_size()[0])/self.document.content_height))
self.scrollbar.set((self.scroll/self.document.content_height), ((self.scroll + BrowserState.get_window_size()[1])/self.document.content_height))
self.draw()

def scroll_up(self, delta):
Expand All @@ -310,7 +306,7 @@ def scroll_up(self, delta):
self.scroll = 0
else:
self.scroll -= (delta * SCROLL_STEP)
self.scrollbar.set((self.scroll/self.document.content_height), ((self.scroll + BrowserState.get_window_size()[0])/self.document.content_height))
self.scrollbar.set((self.scroll/self.document.content_height), ((self.scroll + BrowserState.get_window_size()[1])/self.document.content_height))
self.draw()

def scrollbar_scroll(self, action: Literal["moveto"], position: str):
Expand All @@ -320,7 +316,7 @@ def scrollbar_scroll(self, action: Literal["moveto"], position: str):
if not 0 <= position_float <= max_position:
return
self.scroll = self.document.content_height * position_float
self.scrollbar.set((self.scroll/self.document.content_height), ((self.scroll + BrowserState.get_window_size()[0])/self.document.content_height))
self.scrollbar.set((self.scroll/self.document.content_height), ((self.scroll + BrowserState.get_window_size()[1])/self.document.content_height))
self.draw()

def is_emoji(self, unicode) -> bool:
Expand Down
32 changes: 7 additions & 25 deletions browser/elements/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,31 +139,13 @@ def __init__(self, x1, y1, x2, y2, border: Border = Border()):
self.border = border

def calculate_offset(self, side: Literal["top", "left", "bottom", "right"]) -> Tuple[int, int]:
offset: Tuple[int, int] = (0, 0)
"""
if side == "top":
if self.border.get_border("left"):
offset = (int(self.border.get_border("left").width/2), offset[1])
if self.border.get_border("right"):
offset = (offset[0], int(self.border.get_border("right").width/2))
elif side == "left":
if self.border.get_border("top"):
offset = (offset[0], int(self.border.get_border("top").width/2))
if self.border.get_border("bottom"):
offset = (int(self.border.get_border("bottom").width/2), offset[1])
elif side == "bottom":
if self.border.get_border("left"):
offset = (int(self.border.get_border("left").width/2), offset[1])
if self.border.get_border("right"):
offset = (offset[0], int(self.border.get_border("right").width/2))
elif side == "right":
if self.border.get_border("top"):
offset = (offset[0], int(self.border.get_border("top").width/2))
if self.border.get_border("bottom"):
offset = (int(self.border.get_border("bottom").width/2), offset[1])

"""
return offset
# Extend each border line by half the width of the perpendicular
# borders so lines of differing widths meet at the corners.
if side in ("top", "bottom"):
return (self.border.get_border("left").width // 2,
self.border.get_border("right").width // 2)
return (self.border.get_border("bottom").width // 2,
self.border.get_border("top").width // 2)

def execute(self, scroll: int, canvas: Canvas, supported_emojis: List[str]):
widths = [border.width for border in self.border.get_borders().values()]
Expand Down
1 change: 0 additions & 1 deletion browser/layouts/ImageLayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ def calculate_height(self) -> int:
attr_height = attr_height[:-2]
return int(attr_height)
elif style_height == "auto":
print("width", self.width)
if self.width == None:
return 100
style_height = str(self.width)
Expand Down
Loading
Loading