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 atpath(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:
- Rejects
nilpaths. - Rejects paths containing null bytes (
\0). - Rejects paths containing
..(literal double-dot segment check). - Expands the path with
File.expand_path(path, Rails.root)and verifies it starts with"#{Rails.root}/"(boundary check). - Checks membership in
whitelisted_filesSet.
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:
{ 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
@contentand@language, and the view renders syntax-highlighted source viahighlight_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, rendersrender_markdown(@content)inside.markdown-body:source— whenparams[: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:
- Click handler — immediately marks the clicked file
.activebefore Turbo navigation completes. turbo:loadsync — after each navigation, re-derives the active path fromwindow.location.pathname, corrects.activeclasses, 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_pathboundary check + whitelist membership — three independent layers. format: falseon the wildcard route is essential: without it Rails would treat.js/.rbfile extensions in the URL as the response format and skip the layout.- The request format is also forced to
:htmlin abefore_actionto handle edge cases where Rails still infers a non-HTML format. data-turbo-permanentkeeps the tree state (open directories, scroll position) across Turbo navigations.- Binary files bypass Turbo (
data-turbo="false") becausesend_filesends 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 triggeringturbo:loadforsyncActive.
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 — extends the whitelist to expose specs and Claude command files.
- Source Browser — Markdown Links — rewrites relative links in rendered Markdown files to Source Browser URLs.
- Styling and CSS —
_source.scssprovides the two-column layout, sticky tree sidebar, and active file styles. - Markdown Code Block Syntax Highlighting — 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.