Repositories / gitweb2.git

gitweb2.git

Clone (read-only): git clone http://git.guha-anderson.com/git/gitweb2.git

Branch

Use ppx_format and Printf.bprintf for HTML assembly

Wire ppx_format into the gitweb2 library and rewrite route and view
builders to append markup with Printf.bprintf and [%i {| ... |}]
interpolations. Keep the stylesheet outside the format string so CSS
percent widths stay literal; interpolate only the document title after
escape.

Made-with: Cursor
Author
Arjun Guha <a.guha@northeastern.edu>
Date
2026-04-30 05:11:43 -0400
Commit
652c226e8ccae0b6b4f1e3d8164163f7aa7f07c1
dune-project
index dd3c04c..b78cf77 100644
--- a/dune-project
+++ b/dune-project
@@ -8,6 +8,7 @@
  (depends
   (ocaml (>= 5.4))
   dune
+  ppx_format
   ocaml-git
   cohttp-eio
   eio_main
gitweb2.opam
index 1d3fcf8..94fc82d 100644
--- a/gitweb2.opam
+++ b/gitweb2.opam
@@ -4,6 +4,7 @@ synopsis: "Read-only local Git web viewer"
 depends: [
   "ocaml" {>= "5.4"}
   "dune" {>= "3.22"}
+  "ppx_format"
   "ocaml-git"
   "cohttp-eio"
   "eio_main"
src/dune
index d967ae7..c115b2f 100644
--- a/src/dune
+++ b/src/dune
@@ -2,7 +2,9 @@
  (name gitweb2)
  (public_name gitweb2)
  (modules gitweb2)
- (libraries ocaml-git unix uri cohttp-eio eio_main))
+ (libraries ocaml-git unix uri cohttp-eio eio_main)
+ (preprocess
+  (pps ppx_format)))
 
 (executable
  (name main)
src/gitweb2.ml
index 23f550d..e6b768b 100644
--- a/src/gitweb2.ml
+++ b/src/gitweb2.ml
@@ -111,11 +111,12 @@ let file_render ?(pygments_command : string = default_pygments_command) ~(file_p
 let is_readme (name : string) : bool = name = "README.md" || name = "README.txt" || name = "README"
 
 let page (title : string) (body : string) : string =
-  "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n\
-   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>"
-  ^ html title
-  ^ " &middot; gitweb2</title>\n<style>\n\
-     :root { color-scheme: light; --ink: #1f2428; --muted: #586069; --line: #d0d7de; --wash: #f6f8fa; --link: #0969da; --accent: #1a7f37; }\n\
+  let buffer = Buffer.create (String.length body + 4096) in
+  Buffer.add_string buffer "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n\
+   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>";
+  Printf.bprintf buffer [%i "{%s html title} &middot; gitweb2</title>\n<style>\n"];
+  Buffer.add_string buffer
+    ":root { color-scheme: light; --ink: #1f2428; --muted: #586069; --line: #d0d7de; --wash: #f6f8fa; --link: #0969da; --accent: #1a7f37; }\n\
      * { box-sizing: border-box; }\n\
      body { margin: 0; font: 15px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; color: var(--ink); background: #fff; }\n\
      a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; }\n\
@@ -140,8 +141,10 @@ let page (title : string) (body : string) : string =
      .readme { margin-top: 24px; } .file-meta { padding: 10px 12px; border-bottom: 1px solid var(--line); background: #fff; }\n\
      .notice { padding: 16px; border: 1px solid var(--line); border-radius: 8px; background: var(--wash); }\n\
      @media (max-width: 640px) { main { padding: 20px 12px 40px; } h1 { font-size: 23px; } .kind { width: 78px; } }\n\
-     </style>\n</head>\n<body><main>"
-  ^ body ^ "</main></body>\n</html>"
+     </style>\n</head>\n<body><main>";
+  Buffer.add_string buffer body;
+  Buffer.add_string buffer "</main></body>\n</html>";
+  Buffer.contents buffer
 
 let normalize_root (root : string) : string =
   let expanded =
@@ -216,22 +219,23 @@ let entries_for_display (entries : Ocaml_git.tree_entry list) : Ocaml_git.tree_e
 
 let repo_header (repo : repo_info) (branch : string) (path : string) (branches : Ocaml_git.branch list) (selected : string) : string =
   let buffer = Buffer.create 512 in
-  Buffer.add_string buffer ("<section class=\"repo-head\"><p><a href=\"/\">Repositories</a> / " ^ html repo.name ^ "</p>");
-  Buffer.add_string buffer ("<h1>" ^ html (if path = "" then repo.name else path) ^ "</h1>");
-  Buffer.add_string buffer "<div class=\"toolbar\"><span>Branch</span><nav>";
+  let heading_title = if String.length path = 0 then repo.name else path in
+  Printf.bprintf buffer
+    [%i
+      {|<section class="repo-head"><p><a href="/">Repositories</a> / {%s html repo.name}</p><h1>{%s html heading_title}</h1><div class="toolbar"><span>Branch</span><nav>|} ];
   List.iter
     (fun (item : Ocaml_git.branch) ->
       let active = if item.name = branch then " class=\"active\"" else "" in
-      Buffer.add_string buffer ("<a" ^ active ^ " href=\"" ^ repo_url repo item.name path ^ "\">" ^ html item.name ^ "</a>"))
+      Printf.bprintf buffer [%i {|<a{%s active} href="{%s repo_url repo item.name path}">{%s html item.name}</a>|} ])
     branches;
-  Buffer.add_string buffer "</nav></div><div class=\"tabs\">";
+  Printf.bprintf buffer [%i {|</nav></div><div class="tabs">|} ];
   let tab id label href =
     let active = if id = selected then " class=\"active\"" else "" in
-    Buffer.add_string buffer ("<a" ^ active ^ " href=\"" ^ href ^ "\">" ^ label ^ "</a>")
+    Printf.bprintf buffer [%i {|<a{%s active} href="{%s href}">{%s label}</a>|} ]
   in
   tab "code" "Code" (repo_url repo branch path);
   tab "history" "History" ("/repo/" ^ url_encode repo.key ^ "/" ^ url_encode branch ^ "/-/commits");
-  Buffer.add_string buffer "</div></section>";
+  Printf.bprintf buffer [%i {|</div></section>|} ];
   Buffer.contents buffer
 
 let tree_view ~(pygments_command : string) (repo : repo_info) (git : Ocaml_git.t) (branch : string) (path : string)
@@ -240,31 +244,29 @@ let tree_view ~(pygments_command : string) (repo : repo_info) (git : Ocaml_git.t
   let listing = Ocaml_git.tree ~commit ~path git () in
   let buffer = Buffer.create 4096 in
   Buffer.add_string buffer (repo_header repo branch path branches "code");
-  Buffer.add_string buffer "<div class=\"panel\"><table><tbody>";
-  if path <> "" then (
+  Printf.bprintf buffer [%i {|<div class="panel"><table><tbody>|} ];
+  if String.length path <> 0 then (
     let parent = try Filename.dirname path with _ -> "" in
     let parent = if parent = "." then "" else parent in
-    Buffer.add_string buffer ("<tr><td><a href=\"" ^ repo_url repo branch parent ^ "\">..</a></td><td class=\"kind\">tree</td></tr>")
-  ) else ();
+    Printf.bprintf buffer [%i {|<tr><td><a href="{%s repo_url repo branch parent}">..</a></td><td class="kind">tree</td></tr>|} ])
+  else ();
   let display_entries = entries_for_display listing.entries in
   List.iter
     (fun (entry : Ocaml_git.tree_entry) ->
       let child = join_path path entry.name in
-      Buffer.add_string buffer ("<tr><td><a href=\"" ^ repo_url repo branch child ^ "\">" ^ html entry.name ^ "</a></td>");
       let kind =
         match entry.kind with Blob -> "blob" | Tree -> "tree" | Commit -> "commit" | Tag -> "tag" | Other -> "other"
       in
-      Buffer.add_string buffer ("<td class=\"kind\">" ^ kind ^ "</td></tr>"))
+      Printf.bprintf buffer [%i {|<tr><td><a href="{%s repo_url repo branch child}">{%s html entry.name}</a></td><td class="kind">{%s kind}</td></tr>|} ])
     display_entries;
-  Buffer.add_string buffer "</tbody></table></div>";
+  Printf.bprintf buffer [%i {|</tbody></table></div>|} ];
   (match List.find_opt (fun (entry : Ocaml_git.tree_entry) -> is_readme entry.name && entry.kind = Blob) display_entries with
   | None -> ()
   | Some readme ->
       let readme_path = join_path path readme.name in
       let blob = Ocaml_git.blob ~commit git readme_path in
       if not (Ocaml_git.blob_is_binary blob) then (
-        Buffer.add_string buffer "<section class=\"readme\">";
-        Buffer.add_string buffer ("<h2>" ^ html readme.name ^ "</h2><div class=\"panel file\">");
+        Printf.bprintf buffer [%i {|<section class="readme"><h2>{%s html readme.name}</h2><div class="panel file">|} ];
         Buffer.add_string buffer (file_render ~pygments_command ~file_path:(join_path repo.path readme_path) (Ocaml_git.blob_text blob));
         Buffer.add_string buffer "</div></section>")
       else ());
@@ -274,10 +276,7 @@ let blob_view ~(pygments_command : string) (repo : repo_info) (blob : Ocaml_git.
     (branches : Ocaml_git.branch list) : string =
   let buffer = Buffer.create 4096 in
   Buffer.add_string buffer (repo_header repo branch path branches "code");
-  Buffer.add_string buffer "<div class=\"panel file\">";
-  Buffer.add_string buffer
-    ("<div class=\"file-meta\">" ^ string_of_int (String.length blob.Ocaml_git.bytes) ^ " bytes &middot; "
-   ^ html (String.sub blob.id 0 (min 12 (String.length blob.id))) ^ "</div>");
+  Printf.bprintf buffer [%i {|<div class="panel file"><div class="file-meta">{%d String.length blob.Ocaml_git.bytes} bytes &middot; {%s html (String.sub blob.id 0 (min 12 (String.length blob.id)))}</div>|} ];
   if Ocaml_git.blob_is_binary blob then Buffer.add_string buffer "<p class=\"notice\">Binary file</p>"
   else Buffer.add_string buffer (file_render ~pygments_command ~file_path:(join_path repo.path path) (Ocaml_git.blob_text blob));
   Buffer.add_string buffer "</div>";
@@ -287,15 +286,12 @@ let commits_page (repo : repo_info) (branch : string) : string =
   Ocaml_git.with_repo repo.path @@ fun git ->
   let buffer = Buffer.create 4096 in
   Buffer.add_string buffer (repo_header repo branch "" (local_branches git) "history");
-  Buffer.add_string buffer "<ol class=\"commits\">";
+  Printf.bprintf buffer [%i {|<ol class="commits">|} ];
   List.iter
     (fun (commit : Ocaml_git.commit) ->
-      Buffer.add_string buffer
-        ("<li><strong>" ^ html commit.summary ^ "</strong><span>"
-       ^ html (String.sub commit.id 0 (min 12 (String.length commit.id)))
-       ^ " &middot; " ^ html commit.author.name ^ "</span></li>"))
+      Printf.bprintf buffer [%i {|<li><strong>{%s html commit.summary}</strong><span>{%s html (String.sub commit.id 0 (min 12 (String.length commit.id)))} &middot; {%s html commit.author.name}</span></li>|} ])
     (Ocaml_git.commits ~limit:100 git branch);
-  Buffer.add_string buffer "</ol>";
+  Printf.bprintf buffer [%i {|</ol>|} ];
   page (branch ^ " history") (Buffer.contents buffer)
 
 let repo_page ~(pygments_command : string) (repo : repo_info) (branch : string) (path : string) : string =
@@ -322,17 +318,16 @@ let route ?(pygments_command : string = default_pygments_command) ~(root : strin
     match parts with
     | [] ->
         let buffer = Buffer.create 4096 in
-        Buffer.add_string buffer ("<section class=\"hero\"><p class=\"eyebrow\">" ^ html root ^ "</p><h1>Repositories</h1></section>");
+        Printf.bprintf buffer [%i {|<section class="hero"><p class="eyebrow">{%s html root}</p><h1>Repositories</h1></section>|} ];
         if repos = [] then Buffer.add_string buffer "<p class=\"notice\">No Git repositories were found under this path.</p>"
         else (
           Buffer.add_string buffer "<ol class=\"repo-list\">";
           List.iter
             (fun repo ->
               let summary = repo_summary repo in
-              Buffer.add_string buffer ("<li><a href=\"" ^ repo_url repo summary.branch "" ^ "\"><strong>" ^ html repo.name ^ "</strong>");
-              Buffer.add_string buffer ("<span>" ^ html summary.branch);
-              if summary.short_id <> "" then Buffer.add_string buffer (" &middot; " ^ html summary.short_id);
-              Buffer.add_string buffer ("</span><small>" ^ html summary.summary ^ "</small></a></li>"))
+              Printf.bprintf buffer [%i {|<li><a href="{%s repo_url repo summary.branch ""}"><strong>{%s html repo.name}</strong><span>{%s html summary.branch}|} ];
+              if String.length summary.short_id <> 0 then Printf.bprintf buffer [%i {| &middot; {%s html summary.short_id}|} ];
+              Printf.bprintf buffer [%i {|</span><small>{%s html summary.summary}</small></a></li>|} ])
             repos;
           Buffer.add_string buffer "</ol>");
         { status = 200; body = page "Repositories" (Buffer.contents buffer) }