Spec: Rendered README and web code-reading view
Status: implemented on feature/rendered-read-view.
Scope owner: sigit-si (Rails web app).
This is a reverse-engineered specification of the repository read experience: rendered Markdown, the syntax-highlighted blob view, permalinks and line selection, the raw endpoint, and the rendering security boundary. It documents the contract the code fulfills so the behavior can be re-implemented or verified independently. Requirement IDs (RM-, BV-, PL-, RAW-, SEC-, PERF-) are referenced by the test map at the end.
1. Goal and non-goals
Goal: make the web repo-browsing experience render code and Markdown instead of serving them as plain text, with reading affordances comparable to a standard code host (rendered READMEs, highlighting, line numbers, permalinks, raw bytes).
Non-goals (not built here): pull-request review UI, issues, blame, code search, diff view. This spec covers README rendering and the blob view only.
2. Surfaces and routes
Existing routes (unchanged by this work, listed for context):
| Route | Controller action | Purpose |
|---|---|---|
GET /:username/:repository |
repositories#show |
repo landing page, renders README |
GET /:username/:repository/blob/:branch/*path |
blobs#show |
file view |
GET /:username/:repository/raw/:branch/*path |
blobs#raw |
exact bytes |
:branch accepts any ref string, including a full commit SHA (used by
permalinks). *path is the repository-relative file path.
3. Components
| Component | File | Responsibility |
|---|---|---|
MarkdownRenderer |
app/services/markdown_renderer.rb |
GFM to sanitized HTML |
SyntaxHighlighter |
app/services/syntax_highlighter.rb |
per-line highlighted HTML |
GitRepositoryService (extended) |
app/services/git_repository_service.rb |
blob_sha, blob_size, commit_sha |
BlobsController |
app/controllers/blobs_controller.rb |
blob view, raw, large-file and binary handling, caching |
RepositoriesController |
app/controllers/repositories_controller.rb |
README render, clone URLs |
line_selection_controller.js |
app/javascript/controllers/ |
line anchors, range select, SHA permalink |
clipboard_controller.js |
app/javascript/controllers/ |
copy code and clone URLs |
code_copy_controller.js |
app/javascript/controllers/ |
copy buttons on rendered Markdown code blocks |
4. Markdown rendering (RM)
MarkdownRenderer.render(text, context: nil) returns a sanitized HTML string.
context is a MarkdownRenderer::Context carrying username, repository,
ref, and dir (the directory of the file being rendered, "" for repo root).
- RM-1: Blank or nil input returns an empty string.
- RM-2: GitHub-Flavored Markdown is supported: tables, task lists, strikethrough, autolinks, fenced code, footnotes, headings.
- RM-3: Fenced code blocks are syntax-highlighted server-side with Rouge and
wrapped in
<pre class="highlight code-block">. An unknown or absent language falls back to plain text without raising. - RM-4: Headings get a stable id slug and an in-page anchor link
(
<a class="heading-anchor" href="#slug">). Repeated heading text yields unique slugs (name,name-1,name-2). - RM-5: Task list items (
- [ ],- [x]) render as disabled checkbox inputs onli.task-list-item. - RM-6: When
contextis present, relative links resolve to the blob view and relative images resolve to the raw endpoint, both against the repo atref:- link
docs/guide.mdfrom root to/:user/:repo/blob/:ref/docs/guide.md - image
assets/logo.pngfrom root to/:user/:repo/raw/:ref/assets/logo.png
- link
- RM-7: Relative paths resolve against
context.dir, collapsing.and..without escaping the repository root. A traversal that would escape the root is left unresolved. - RM-8: Absolute URLs (any scheme), root-relative paths (
/...),mailto:, and in-page anchors (#...) are passed through unchanged.
README discovery on the repo landing page is unchanged
(GitRepositoryService.readme_content): the first match of README.md,
README.txt, README, readme.md. Its rendered HTML is cached (see PERF-2).
5. Blob view (BV)
BlobsController#show pins the response and view formats to HTML so a file
extension in the path does not trigger Rails format negotiation, then classifies
the blob.
- BV-1: A missing blob (
blob_shaorblob_sizenil) renders the 404 page. - BV-2: A previewable image (
png jpg jpeg gif webp avif bmp ico svg) at or belowMAX_LOAD_BYTESis shown inline as a base64data:URI. SVG is safe here because it loads through an<img>tag. - BV-3: A file larger than
MAX_LOAD_BYTES(5 MB) is not read into memory. The view shows a size notice and a "View raw" link only (@too_large). - BV-4: A binary file (NUL byte in the first 8192 bytes, or invalid UTF-8) shows a "Binary file" notice with size and a "View raw" link.
- BV-5: A Markdown blob (
md markdown mdown mkd) at or belowMAX_MARKDOWN_BYTES(512 KB) renders as Markdown by default.?plain=1forces the highlighted source view, and a toggle link switches between the two. - BV-6: Any other text file is shown with per-line Rouge highlighting, line
numbers, and a per-line anchor target (
tr#L<n>). - BV-7: A text file larger than
MAX_HIGHLIGHT_BYTES(512 KB) is shown as escaped plain text without highlighting, with a notice (@highlight_skipped). - BV-8: A file with more than
MAX_DISPLAY_LINES(5000) lines is truncated to the first 5000 displayed lines, with a notice and a "View raw" link (@truncated). - BV-9:
SyntaxHighlighter.lines(content, filename:)returns one HTML fragment per source line. It tokenizes the whole file in one pass so multi-line constructs highlight correctly, does not emit a trailing blank line when the file ends in a newline, counts a final line with no trailing newline, and on any lexer error falls back to HTML-escaped plain lines.
6. Permalinks and line selection (PL)
Implemented in line_selection_controller.js, driven by data attributes on the
blob table and line-number anchors.
- PL-1: Clicking a line number sets the URL hash to
#L<n>and highlights that line (.line-selected). - PL-2: Shift-clicking a second line selects the inclusive range and sets the
hash to
#L<lo>-L<hi>, regardless of click order. - PL-3: On load (and on
hashchange), a#L<n>or#L<lo>-L<hi>hash highlights the matching lines and scrolls the first into view. - PL-4: "Copy permalink" copies an absolute URL built from
data-line-selection-permalink-base-valueplus the current line selection. The base path isrepository_blob_path(owner, repo, @commit_sha, file_path), where@commit_sha = GitRepositoryService.commit_sha(disk_path, branch). The link is pinned to the commit the ref points at, so it keeps resolving to the same content after the branch moves.
7. Raw endpoint (RAW)
BlobsController#raw serves the file bytes for install scripts, badges, and CI.
- RAW-1: Returns the exact, unmodified bytes of the blob.
- RAW-2: Sets
X-Content-Type-Options: nosniff. - RAW-3: Raster image extensions are served with their real image content type;
every other file is served as
text/plain; charset=utf-8. SVG is served as text/plain (notimage/svg+xml) so it cannot execute as active content on direct navigation. - RAW-4: A missing file returns 404 with no body.
8. Security (SEC)
- SEC-1: All rendered Markdown HTML passes through an allow-list sanitizer
(
Rails::HTML5::SafeListSanitizer, falling back to HTML4) before reaching a view. Only the tags and attributes inMarkdownRenderer::ALLOWED_TAGSandALLOWED_ATTRIBUTESsurvive. - SEC-2:
<script>,<style>,<noscript>,<template>,<svg>,<math>,<head>,<title>,<object>, and<embed>blocks are pruned with their contents before sanitization, so disallowed code never survives even as inert text. - SEC-3: Inline event handlers (
on*) and dangerous-scheme URLs (for examplejavascript:) are removed. Redcarpet runs withsafe_links_only; the sanitizer is the authoritative gate. - SEC-4: Raw user content cannot run as active content on the app origin (RAW-2, RAW-3).
- SEC-5: Result: malicious HTML or JS embedded in a README does not execute and its payload text does not appear in the rendered output.
9. Performance (PERF)
- PERF-1: Highlighting and Markdown rendering run server-side.
- PERF-2: Rendered output is cached in
Rails.cachekeyed by the blob SHA (content-addressed, so the cache is shared across refs and never goes stale). Markdown cache keys also includerefanddirbecause relative-link resolution depends on them. - PERF-3: Blob size is read with
git cat-file -sbefore the content is loaded, so the large-file thresholds (BV-3, BV-7, BV-8) avoid loading or tokenizing oversized files.
10. Convenience controls
- CV-1: The blob view has a "Copy" button that copies the file text (joined from
the per-line code cells via
clipboard_controller.js). - CV-2: Rendered Markdown code blocks get a hover "Copy" button injected by
code_copy_controller.js. - CV-3: The repo page has a Clone control exposing HTTPS and SSH URLs, each with
a copy button. URLs come from
request.base_url/request.hostso they match the serving host.
11. Constants
Defined on BlobsController:
| Constant | Value | Requirement |
|---|---|---|
MAX_HIGHLIGHT_BYTES |
512 KB | BV-7 |
MAX_MARKDOWN_BYTES |
512 KB | BV-5 |
MAX_LOAD_BYTES |
5 MB | BV-3 |
MAX_DISPLAY_LINES |
5000 | BV-8 |
MARKDOWN_EXTENSIONS |
md markdown mdown mkd |
BV-5 |
PREVIEW_IMAGE_TYPES |
raster set plus svg |
BV-2 |
RAW_IMAGE_TYPES |
raster set, no svg |
RAW-3 |
12. Test map
RSpec (spec/) is the project test framework. Tests assume the Rails
environment boots and a Postgres test database is available
(bin/rails db:test:prepare).
| Requirement | Test |
|---|---|
| RM-1, RM-2, RM-3, RM-4, RM-5, RM-6, RM-7, RM-8 | spec/services/markdown_renderer_spec.rb |
| BV-9 | spec/services/syntax_highlighter_spec.rb |
| BV-5, BV-6, PL-4, SEC-5 | spec/requests/blob_view_spec.rb |
| RAW-1, RAW-2, RAW-3, RAW-4 | spec/requests/raw_blob_spec.rb |
| SEC-1, SEC-2, SEC-3 | spec/services/markdown_renderer_spec.rb (sanitization group) |
Gaps not yet covered by automated tests (manual or future work): BV-3, BV-4, BV-7, BV-8 (the large-file and binary branches), PL-1 to PL-3 (client-side line selection), CV-1 to CV-3 (clipboard controllers).
13. Verification
bundle exec rspec spec/services spec/requests # renderer, highlighter, raw, blob view
bundle exec rails server # then open /:user/:repo with a README
Manual checks on a running server: a README renders as HTML with working code
copy buttons; a source file shows highlighting and line numbers; #L10
highlights and scrolls; click and shift-click select lines and update the URL;
"Copy permalink" yields a SHA-pinned URL; /raw/... returns exact bytes with
X-Content-Type-Options: nosniff.