Prohlížeč zdrojového kódu

docs/specs/source-browser.md

Náhled Zdrojový kód

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:

{ 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

Change Log

  • 2026-03-05: Initial retrospective spec written.