Prohlížeč zdrojového kódu
docs/specs/source-browser.md
# Spec: Source Browser
- **Status:** active
- **Created:** 2026-03-05
- **Related code:** `app/services/source_browser.rb`, `app/controllers/source_controller.rb`, `app/views/source/`, `app/javascript/controllers/source_tree_controller.js`, `config/initializers/source_browser.rb`
## Overview
The Source Browser lets visitors browse and read the application's own source code directly in the UI. It exposes a curated, whitelisted subset of project files as a navigable file tree with syntax-highlighted content. The feature exists to make the portfolio transparent — showing exactly how the app itself is built.
## Behaviour
### Routes
- `GET /source` — index page (no file selected, placeholder shown)
- `GET /source/*path` — show file at `path` (format-free wildcard, `trailing_slash: true`)
Both routes map to `SourceController#show`. The `format: false` constraint prevents Rails from inferring the response format from the file extension in the URL.
### File Whitelist
`config/initializers/source_browser.rb` declares `Rails.application.config.source_browser_whitelist` — an array of glob patterns. At boot, `SourceBrowser` resolves these globs against `Rails.root`, collecting a `Set` of relative paths. Only files matching the whitelist can be read or served.
The whitelist covers: app code (`controllers`, `models`, `services`, `views`, `helpers`, `javascript`, `assets`), config files, specs, examples, `db/`, selected root dotfiles, and `*.md` files.
Sensitive files such as `config/database.yml`, `.env`, and `config/credentials.yml.enc` are **not** included.
### Security
`SourceBrowser` applies layered path validation before any read or path resolution:
1. Rejects `nil` paths.
2. Rejects paths containing null bytes (`\0`).
3. Rejects paths containing `..` (literal double-dot segment check).
4. Expands the path with `File.expand_path(path, Rails.root)` and verifies it starts with `"#{Rails.root}/"` (boundary check).
5. Checks membership in `whitelisted_files` Set.
Any violation raises `SourceBrowser::NotAllowed`. The controller catches this and raises `ActionController::RoutingError` (404), never a redirect.
### Tree Structure
`SourceBrowser.tree` returns a nested hash:
```ruby
{ dirs: { "app" => { name:, path:, dirs: {}, files: [] }, ... }, files: [] }
```
Directories are keyed by name within their parent's `:dirs` hash. Files are arrays of `{ name:, path:, binary: }` hashes. The tree is built from sorted whitelisted paths.
### Text Files
For whitelisted, non-binary files:
- `SourceBrowser.read(path)` returns the file content as a string.
- `SourceBrowser.language_for(path)` detects the language from extension or basename (special-cased: `Gemfile`, `Rakefile`, `Guardfile` → `"ruby"`; `Dockerfile` → `"docker"`; unknown extensions → `"plaintext"`).
- The controller assigns `@content` and `@language`, and the view renders syntax-highlighted source via `highlight_code(@content, language: @language)`.
### Binary Files
Files with extensions in `BINARY_EXTENSIONS` (`.png`, `.jpg`, `.jpeg`, `.gif`, `.ico`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.pdf`) are served via `send_file` with `disposition: :attachment` (browser download). The tree marks them `binary: true`; their links use `data-turbo="false"` to bypass Turbo navigation.
### Markdown View Toggle
When `@language == "markdown"`, the controller sets `@view_mode`:
- `:formatted` — default, renders `render_markdown(@content)` inside `.markdown-body`
- `:source` — when `params[:view] == "source"`, renders syntax-highlighted source
The view shows a toggle between "Náhled" (preview) and "Zdrojový kód" (source code).
### UI / Navigation
The source tree is rendered as a recursive `_tree` partial with `<details>/<summary>` elements for directories. The current file is marked `.active` on its `.tree-file` div.
The `SourceTreeController` Stimulus controller (on `[data-turbo-permanent]`) handles:
1. **Click handler** — immediately marks the clicked file `.active` before Turbo navigation completes.
2. **`turbo:load` sync** — after each navigation, re-derives the active path from `window.location.pathname`, corrects `.active` classes, expands all ancestor `<details>`, and scrolls the active item into view.
The tree element is marked `data-turbo-permanent` so it is not re-rendered on Turbo navigations; only the `<div id="source-content">` area updates.
### Whitelist Cache
`whitelisted_files` is memoized per class (`@whitelisted_files`). `SourceBrowser.reload!` clears it. This is used in specs (via `before { SourceBrowser.reload! }`) to ensure test isolation.
## Implementation Notes
- Path security is defence-in-depth: `..` string check + `expand_path` boundary check + whitelist membership — three independent layers.
- `format: false` on the wildcard route is essential: without it Rails would treat `.js`/`.rb` file extensions in the URL as the response format and skip the layout.
- The request format is also forced to `:html` in a `before_action` to handle edge cases where Rails still infers a non-HTML format.
- `data-turbo-permanent` keeps the tree state (open directories, scroll position) across Turbo navigations.
- Binary files bypass Turbo (`data-turbo="false"`) because `send_file` sends a binary response that Turbo cannot process.
- The content area is a plain `<div id="source-content">` so all links (including those in rendered Markdown) trigger full Turbo Drive navigations — updating the URL, pushing history, and triggering `turbo:load` for `syncActive`.
## Tests
- `spec/services/source_browser_spec.rb` — unit tests for `.read`, `.file?`, `.binary?`, `.full_path`, `.tree`, `.language_for`, and security (path traversal, null bytes, non-whitelisted paths)
- `spec/system/source_browser_spec.rb` — browser integration tests covering:
- Turbo Drive navigation (no full reload) with placeholder restored on back
- Markdown formatted/source view toggle
- Active file and ancestor `<details>` restoration after back navigation
- Scroll-into-view of the active tree item after back and forward navigation
## See Also
- [Source Browser — Claude Workflow Files](source-browser-claude.md) — extends the whitelist to expose specs and Claude command files.
- [Source Browser — Markdown Links](source-browser-md-links.md) — rewrites relative links in rendered Markdown files to Source Browser URLs.
- [Styling and CSS](styling.md) — `_source.scss` provides the two-column layout, sticky tree sidebar, and active file styles.
- [Markdown Code Block Syntax Highlighting](markdown-code-highlighting.md) — Markdown files in the source browser are rendered via `render_markdown`, which now highlights fenced code blocks.
## Change Log
- 2026-03-05: Initial retrospective spec written.