Files
rust_browser/_bmad-output/implementation-artifacts/2-9-base-url-resolution-and-image-rendering-completeness.md
Zachary D. Rowitsch 70ad1244d8 Implement base URL resolution and image rendering completeness with code review fixes (§4.2.3)
Add <base href> support with resolve_base_url() wired before all resource loading,
document.baseURI JS API (falling back to "about:blank" per spec), image format
verification golden tests (PNG/JPEG/GIF/WebP/SVG for both <img> and CSS
background-image), and aspect ratio preservation tests. Code review fixes:
baseURI spec compliance, restyle path no longer overwrites <base href> URL,
added CSS background-image format tests and base URL integration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 01:36:35 -04:00

20 KiB

Story 2.9: Base URL Resolution & Image Rendering Completeness

Status: done

Story

As a web user, I want all relative URLs to resolve correctly and all image formats to render, So that pages with relative links and various image types display properly.

Acceptance Criteria

  1. <base href> sets the document base URL: A document with <base href="https://example.com/path/"> resolves all relative URLs (in links, images, scripts, stylesheets) against that base URL instead of the page URL. (WHATWG HTML §4.2.3)

  2. First <base href> wins: When multiple <base> elements exist, only the first one with an href attribute is used.

  3. All image formats render correctly: Images in PNG, JPEG, GIF, WebP, and SVG formats referenced as <img> or CSS background-image decode and render correctly at specified dimensions.

  4. Image aspect ratio preservation: An <img> with width/height attributes and/or CSS sizing scales to the specified dimensions. When only one dimension is specified and the other is auto, the intrinsic aspect ratio is preserved.

  5. Golden tests cover base URL resolution and each image format, checklists are updated, and just ci passes.

Tasks / Subtasks

  • Task 1: Implement <base href> element support (AC: #1, #2)

    • 1.1 After HTML parsing, scan the Document for the first <base> element with an href attribute:
      • Walk <head> children for <base> elements
      • Take the href value from the first <base> that has one
      • Parse it as an absolute URL, or resolve against the page URL if relative
      • Call document.set_base_url(Some(resolved_url))
    • 1.2 The best place for this logic is in the pipeline integration, after parsing and before any resource loading. Add a resolve_base_url() function in crates/app_browser/src/pipeline/ (new file or in existing module):
      pub fn resolve_base_url(doc: &Document, page_url: &BrowserUrl) -> BrowserUrl {
          // Find first <base href="..."> in <head>
          // Return resolved URL or page_url as fallback
      }
      
    • 1.3 In event_handler.rs, after parsing HTML and before script extraction / resource loading:
      • Call resolve_base_url(&document, &state.current_url)
      • Use the resolved base URL for ALL subsequent resource loading (scripts, stylesheets, images)
      • Pass base URL to fetch_script_sources(), fetch_external_stylesheets(), fetch_images(), fetch_background_images()
    • 1.4 Verify that existing BrowserUrl::join() in crates/shared/src/url.rs correctly handles joining against a base URL (it should — it wraps url::Url::join())
    • 1.5 Edge cases to handle:
      • <base href=""> — empty href, use page URL
      • <base> without href — skip, look for next <base>
      • <base href> with invalid URL — fall back to page URL
      • <base> in <body> — per spec, only <base> in <head> is effective (but browsers are lenient; match browser behavior)
      • Multiple <base> elements — only first with href wins
    • 1.6 Unit tests for resolve_base_url(): all edge cases above
  • Task 2: Expose document.baseURI to JavaScript (AC: #1)

    • 2.1 In DomHost::get_property() for "Document" type, add "baseURI" property:
      • Return JsValue::String(doc.base_url().map(|u| u.to_string()).unwrap_or_default())
    • 2.2 If base_url is None on Document, return the page URL (baseURI always has a value per spec)
    • 2.3 Unit test: document.baseURI returns the effective base URL
  • Task 3: Verify and fix image format rendering completeness (AC: #3)

    • 3.1 Create golden test fixtures for each image format:
      • <img src="test.png"> — PNG with transparency
      • <img src="test.jpg"> — JPEG
      • <img src="test.gif"> — GIF (static frame)
      • <img src="test.webp"> — WebP
      • <img src="test.svg"> — SVG
    • 3.2 Create golden test fixtures for CSS background-image with each format:
      • background-image: url(test.png) etc.
    • 3.3 Verify each format decodes and renders without errors
    • 3.4 If any format fails, fix the decoding path in crates/image/src/lib.rs
    • 3.5 Note: All five formats (PNG, JPEG, GIF, WebP, SVG) are already supported by the image crate (0.25) and resvg (0.47). This task is primarily verification via golden tests, not new implementation.
  • Task 4: Verify and fix image aspect ratio preservation (AC: #4)

    • 4.1 Review existing aspect ratio logic in:
      • crates/layout/src/engine/block/width.rs (lines 24-68) — proportional width from height
      • crates/layout/src/engine/block/layout.rs (lines 278-300) — proportional height from width
    • 4.2 Create golden tests for image sizing scenarios:
      • <img src="..." width="200" height="100"> — explicit both dimensions
      • <img src="..." width="200"> — width only, height auto → preserve aspect ratio
      • <img src="..." height="100"> — height only, width auto → preserve aspect ratio
      • <img src="..." style="width:200px"> — CSS width, height auto
      • <img src="..." style="max-width:100px"> — constrained by max-width
      • <img src="..."> — no dimensions, use intrinsic size
    • 4.3 Verify that CSS width/height override HTML attributes when both are present
    • 4.4 Verify min-width/max-width/min-height/max-height constraints work with images
    • 4.5 Fix any issues found in the aspect ratio calculation
  • Task 5: Ensure base URL flows through all resource loading paths (AC: #1)

    • 5.1 Audit all places where URLs are resolved to ensure they use the effective base URL:
      • fetch_script_sources() in crates/app_browser/src/pipeline/scripts.rs — uses base_url parameter
      • fetch_external_stylesheets() in crates/app_browser/src/pipeline/stylesheets.rs — uses base_url
      • @import resolution in resolve_imports_recursive() — uses stylesheet URL, which should already be absolute
      • fetch_images() in crates/app_browser/src/pipeline/images.rs — uses base_url parameter
      • fetch_background_images() — uses base_url parameter
      • Link href resolution for navigation — uses base_url
    • 5.2 If any path is using state.current_url directly instead of the resolved base URL, fix it to use the base URL
    • 5.3 Integration test: <base href> with a relative <img src> resolves correctly
    • 5.4 Integration test: <base href> with a relative <link href> for stylesheet resolves correctly
  • Task 6: Tests and documentation (AC: #5)

    • 6.1 Golden test: base URL resolution with images
      • HTML with <base href="..."> and <img src="relative-path.png">
      • Verify image loads from the base-URL-resolved path
    • 6.2 Golden test: each image format renders at correct size
    • 6.3 Golden test: aspect ratio preservation with various dimension combinations
    • 6.4 Integration test: document.baseURI returns correct value with and without <base> element
    • 6.5 Integration test: multiple <base> elements — first href wins
    • 6.6 Update docs/HTML5_Implementation_Checklist.md:
      • Check off <base href> under Phase 3 Base URL resolution
      • Check off document.baseURI
      • Verify img is checked (already is)
    • 6.7 Run just ci and ensure all tests pass

Dev Notes

Current URL Resolution Architecture

All resource URLs are resolved using BrowserUrl::join(&relative_url) from crates/shared/src/url.rs. The base URL is passed as a parameter through the pipeline:

event_handler.rs
  → fetch_script_sources(sources, &base_url, network)
  → fetch_external_stylesheets(doc, &base_url, network)
  → fetch_images(doc, &base_url, network, decoder)
  → fetch_background_images(styles, &base_url, network, decoder)

Currently, base_url is always state.current_url (the page load URL). The fix is straightforward: resolve the effective base URL from <base href> BEFORE passing it to these functions.

What Already Works

  • Image format decoding: PNG, JPEG, GIF, WebP via image crate; SVG via resvg/usvg
  • Image intrinsic sizing: LayoutBox::intrinsic_width/height set from decoded image dimensions
  • Aspect ratio preservation: Width/height proportional scaling in block/width.rs and block/layout.rs
  • Background image URL resolution: Via fetch_background_images() with base_url parameter
  • CSS width/height on images: Images coerced to InlineBlock, CSS sizing applied

What NOT to Implement

  • Do NOT implement <base target> — only <base href> is in scope
  • Do NOT implement srcset / <picture> responsive images — future work
  • Do NOT implement animated GIF support — static first frame only is fine
  • Do NOT implement image lazy loading (loading="lazy") — future optimization
  • Do NOT implement CORS for images — future security feature
  • Do NOT implement <img> error/load events — related to Document Lifecycle
  • Do NOT modify the image decoding pipeline in crates/image/ unless a format actually fails to render

Architecture Constraints

  • Layer 0 (shared): BrowserUrl::join() — no changes expected
  • Layer 1 (dom): Document::base_url already exists as a field — just ensure it's set from <base href>
  • Layer 1 (web_api): Expose document.baseURI as JS property
  • Layer 3 (app_browser): Add resolve_base_url() and wire into pipeline
  • No unsafe — enforced by CI

Key Design: Base URL Resolution Timing

The base URL must be resolved AFTER HTML parsing but BEFORE any resource loading:

1. Parse HTML → Document
2. Resolve base URL from <base href> ← NEW
3. Extract and fetch scripts (using base URL)
4. Fetch stylesheets (using base URL)
5. Execute scripts
6. Fetch images (using base URL)
7. Render

Key Files to Modify

File Change
crates/app_browser/src/pipeline/ New resolve_base_url() function
crates/app_browser/src/event_handler.rs Wire base URL resolution before resource loading
crates/web_api/src/dom_host/host_environment.rs Add document.baseURI JS property
docs/HTML5_Implementation_Checklist.md Check off base URL items

Key Files to Read (Reference)

File Why
crates/shared/src/url.rs BrowserUrl::join() implementation
crates/dom/src/document.rs Document::base_url field, set_base_url()
crates/app_browser/src/pipeline/images.rs fetch_images(), fetch_background_images() — URL resolution pattern
crates/app_browser/src/pipeline/stylesheets.rs fetch_external_stylesheets() — URL resolution
crates/app_browser/src/pipeline/scripts.rs fetch_script_sources() — URL resolution
crates/app_browser/src/event_handler.rs Pipeline integration (lines 143-200)
crates/image/src/lib.rs ImagePipeline: format detection and decoding
crates/layout/src/engine/block/width.rs Image width proportional scaling (lines 24-68)
crates/layout/src/engine/block/layout.rs Image height proportional scaling (lines 278-300)
crates/layout/src/engine/box_tree.rs Image intrinsic dimension setup (lines 98-115)
crates/layout/src/tests/image_sizing_tests.rs Existing image sizing tests

Previous Story Intelligence

From Story 2.8 (iframe Support):

  • Pipeline orchestration pattern in event_handler.rs is well-understood
  • Image-like replaced element pattern applies here too (iframes follow same sizing model)
  • Resource loading follows the same base_url.join() pattern throughout

From Epic 1 (CSS stories):

  • Background-image URL resolution was implemented in Story 1.7 (Backgrounds)
  • Golden tests are the standard for visual verification

Testing Strategy

  • Golden tests for image format rendering — create small test images in each format
  • Golden tests for aspect ratio with various dimension combinations
  • Integration tests for base URL resolution with different resource types
  • Unit tests for resolve_base_url() edge cases
  • Note: Golden tests for base URL may require data: URIs or local file references since the test infrastructure may not support external URLs. Consider using <base href="data:..."> or testing with relative paths within the golden test fixture directory.

References

  • WHATWG HTML §4.2.3 — The base element
  • WHATWG HTML §3.1 — Document base URL
  • [Source: crates/shared/src/url.rs] — BrowserUrl with join()
  • [Source: crates/dom/src/document.rs] — Document::base_url field
  • [Source: crates/app_browser/src/pipeline/images.rs] — fetch_images(), fetch_background_images()
  • [Source: crates/app_browser/src/pipeline/stylesheets.rs] — stylesheet URL resolution
  • [Source: crates/app_browser/src/pipeline/scripts.rs] — script URL resolution
  • [Source: crates/image/src/lib.rs] — ImagePipeline with format detection
  • [Source: crates/layout/src/engine/block/width.rs] — Proportional image width (lines 24-68)
  • [Source: crates/layout/src/engine/block/layout.rs] — Proportional image height (lines 278-300)
  • [Source: docs/HTML5_Implementation_Checklist.md] — Phase 3: Base URL resolution, §6.6 img

Dev Agent Record

Agent Model Used

Claude Opus 4.6 (1M context)

Debug Log References

  • All 8 resolve_base_url() unit tests pass
  • All 11 new golden tests pass (284-294)
  • JS262 baseURI DOM test passes
  • just ci passes with 0 failures

Completion Notes List

  • Task 1: Created resolve_base_url() in crates/app_browser/src/pipeline/base_url.rs with 8 unit tests covering all edge cases (no base, absolute href, relative href, first-wins, skip without href, skip empty href, skip whitespace href, multiple elements). Wired into event_handler.rs Phase 1b before all resource loading. Updated link click handler to use document base URL.
  • Task 2: Added document.baseURI property to DomHost::get_property() for Document type. Returns base URL string, falling back to "about:blank" per spec when no page URL is set. JS262 test added (dom-baseuri.js).
  • Task 3: Created test images in PNG, JPEG, GIF, WebP, SVG formats (20x10 red rectangles). Golden tests 284-288 verify each format renders correctly at intrinsic dimensions. All formats decode and render without errors — no fixes needed.
  • Task 4: Golden tests 289-294 verify aspect ratio preservation: width-only (100px→50px height), height-only (50px→100px width), both attributes, CSS width, max-width constraint, intrinsic size. All tests pass — existing proportional scaling in block/width.rs and block/layout.rs works correctly.
  • Task 5: Audited all resource loading paths. effective_base_url now flows through: fetch_and_schedule_scripts, run_from_document (→ fetch_stylesheets, fetch_images, fetch_background_images, load_font_faces, fetch_and_render_iframes). Link click handler updated to use document.base_url() instead of state.current_url.
  • Task 6: 16 golden tests (11 original + 5 CSS background-image format tests 295-299), 11 unit tests (8 original + 3 integration-style base URL resolution tests), 1 JS262 test added. HTML5 checklist updated. just ci passes cleanly.

Change Log

  • 2026-03-15: Story 2.9 implementation complete — base URL resolution from <base href>, document.baseURI JS API, image format verification via golden tests, aspect ratio preservation verification.
  • 2026-03-15: Code review fixes — (H1) document.baseURI now returns "about:blank" instead of "" when no page URL set (spec compliance). (H2) Fixed restyle middle-path overwriting <base href> resolved URL with page URL. (M1) Added 5 CSS background-image format golden tests (295-299). (M2) Added 3 integration-style tests for base URL resource resolution.

File List

New files:

  • crates/app_browser/src/pipeline/base_url.rsresolve_base_url() function with unit tests
  • tests/goldens/fixtures/284-img-png-format.html — PNG format golden test
  • tests/goldens/fixtures/285-img-jpeg-format.html — JPEG format golden test
  • tests/goldens/fixtures/286-img-gif-format.html — GIF format golden test
  • tests/goldens/fixtures/287-img-webp-format.html — WebP format golden test
  • tests/goldens/fixtures/288-img-svg-format.html — SVG format golden test
  • tests/goldens/fixtures/289-img-aspect-ratio-width-only.html — Aspect ratio width-only test
  • tests/goldens/fixtures/290-img-aspect-ratio-height-only.html — Aspect ratio height-only test
  • tests/goldens/fixtures/291-img-aspect-ratio-both-attrs.html — Aspect ratio both attrs test
  • tests/goldens/fixtures/292-img-aspect-ratio-css-width.html — Aspect ratio CSS width test
  • tests/goldens/fixtures/293-img-aspect-ratio-max-width.html — Aspect ratio max-width test
  • tests/goldens/fixtures/294-img-intrinsic-size.html — Intrinsic size test
  • tests/goldens/expected/284-img-png-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/285-img-jpeg-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/286-img-gif-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/287-img-webp-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/288-img-svg-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/289-img-aspect-ratio-width-only.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/290-img-aspect-ratio-height-only.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/291-img-aspect-ratio-both-attrs.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/292-img-aspect-ratio-css-width.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/293-img-aspect-ratio-max-width.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/294-img-intrinsic-size.{layout,dl}.txt — Expected outputs
  • tests/goldens/fixtures/images/test_20x10.{png,jpg,gif,webp,svg} — Test images (20x10 red)
  • tests/external/js262/fixtures/dom-baseuri.js — JS262 baseURI test
  • tests/external/js262/expected/dom-baseuri.txt — Expected output
  • tests/goldens/fixtures/295-bg-img-png-format.html — CSS background-image PNG test
  • tests/goldens/fixtures/296-bg-img-jpeg-format.html — CSS background-image JPEG test
  • tests/goldens/fixtures/297-bg-img-gif-format.html — CSS background-image GIF test
  • tests/goldens/fixtures/298-bg-img-webp-format.html — CSS background-image WebP test
  • tests/goldens/fixtures/299-bg-img-svg-format.html — CSS background-image SVG test
  • tests/goldens/expected/295-bg-img-png-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/296-bg-img-jpeg-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/297-bg-img-gif-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/298-bg-img-webp-format.{layout,dl}.txt — Expected outputs
  • tests/goldens/expected/299-bg-img-svg-format.{layout,dl}.txt — Expected outputs

Modified files:

  • crates/app_browser/src/pipeline/mod.rs — Added base_url module and re-export
  • crates/app_browser/src/event_handler.rs — Wired resolve_base_url() before resource loading; updated link click to use document base URL
  • crates/web_api/src/dom_host/host_environment.rs — Added document.baseURI property
  • tests/goldens.rs — Added 16 new golden test functions (11 img + 5 CSS background-image)
  • tests/external/js262/js262_manifest.toml — Added baseURI test case
  • docs/HTML5_Implementation_Checklist.md — Checked off base URL items
  • _bmad-output/implementation-artifacts/sprint-status.yaml — Status updated