Replace inline HTML generation with ERB templates
Moves HTML out of Ruby string interpolation into proper templates.
Rake task prereqs replace manual `invoke` calls. Capture now owns
the Puma lifecycle.

Assisted-by: Claude Opus 4.6 via Claude Code
change rqrqslrpmuklsrwrtnrnlznzowoqyovt
commit d5817886a84ce20ca825bad3a1531739c4bea5a7
author Alpha Chen <alpha@kejadlen.dev>
date
parent tkxqxltz
diff --git a/Rakefile b/Rakefile
index fcf09ce..6b474d1 100644
--- a/Rakefile
+++ b/Rakefile
@@ -102,26 +102,9 @@ namespace :snapshots do
     ENV["DATABASE_URL"] = ":memory:"
     require "ketchup/web"
     require "ketchup/snapshots"
-    require "puma"
-    require "puma/configuration"
 
     output_dir = File.join(Ketchup::Snapshots.cache_dir, "current")
-
-    config = Puma::Configuration.new do |c|
-      c.app Web.freeze.app
-      c.bind "tcp://127.0.0.1:0"
-      c.log_requests false
-      c.quiet
-    end
-
-    launcher = Puma::Launcher.new(config)
-    thread = Thread.new { launcher.run }
-    sleep 1 until launcher.connected_ports.any?
-
-    port = launcher.connected_ports.first
-    Ketchup::Snapshots::Capture.new(output_dir: output_dir, port: port).call
-    launcher.stop
-    thread.join
+    Ketchup::Snapshots::Capture.new(output_dir: output_dir).call
 
     if ENV["CI"]
       require "json"
@@ -132,8 +115,8 @@ namespace :snapshots do
   end
 
   desc "Compare current screenshots against baseline from latest release"
-  task :diff do
-    require "ketchup/snapshots"
+  task diff: :capture do
+    require "erb"
 
     base_dir = Ketchup::Snapshots.cache_dir
     baseline_dir = File.join(base_dir, "baseline")
@@ -153,30 +136,50 @@ namespace :snapshots do
       puts "No baseline found — showing current screenshots only"
     end
 
-    Rake::Task["snapshots:capture"].invoke
+    baseline_images = Dir.glob(File.join(baseline_dir, "*.png")).map { |f| File.basename(f, ".png") }.sort
+    current_images = Dir.glob(File.join(current_dir, "*.png")).map { |f| File.basename(f, ".png") }.sort
+
+    snapshots = (baseline_images | current_images).sort.map do |name|
+      has_baseline = baseline_images.include?(name)
+      has_current = current_images.include?(name)
+
+      status = if !has_baseline then :new
+               elsif !has_current then :removed
+               end
 
+      {
+        name: name,
+        status: status,
+        baseline: has_baseline ? File.join("baseline", "#{name}.png") : nil,
+        current: has_current ? File.join("current", "#{name}.png") : nil,
+      }
+    end
+
+    template = File.read(File.expand_path("snapshot_diff.erb", __dir__))
     output_path = File.join(base_dir, "diff.html")
-    Ketchup::Snapshots.generate_diff_html(
-      baseline_dir: baseline_dir,
-      current_dir: current_dir,
-      output_path: output_path
-    )
+    File.write(output_path, ERB.new(template, trim_mode: "-").result(binding))
     puts "Diff viewer: #{output_path}"
   end
 
   desc "Capture, diff, and open the viewer"
-  task :review do
-    Rake::Task["snapshots:diff"].invoke
+  task review: :diff do
     system("open", File.join(Ketchup::Snapshots.cache_dir, "diff.html"))
   end
 
   desc "Generate gallery HTML from images in a directory"
   task :gallery, [:images_dir, :output_path] do |_t, args|
-    require "ketchup/snapshots"
+    require "erb"
 
     images_dir = args.fetch(:images_dir)
     output_path = args.fetch(:output_path)
-    Ketchup::Snapshots.generate_gallery_html(images_dir: images_dir, output_path: output_path)
+
+    title = "Ketchup Snapshots"
+    images = Dir.glob(File.join(images_dir, "*.png")).sort.map do |f|
+      { name: File.basename(f, ".png"), filename: File.basename(f) }
+    end
+
+    template = File.read(File.expand_path("snapshot_gallery.erb", __dir__))
+    File.write(output_path, ERB.new(template, trim_mode: "-").result(binding))
     puts output_path
   end
 end
diff --git a/lib/ketchup/snapshots.rb b/lib/ketchup/snapshots.rb
index ed84089..06a0657 100644
--- a/lib/ketchup/snapshots.rb
+++ b/lib/ketchup/snapshots.rb
@@ -9,137 +9,31 @@ module Ketchup
       File.join(base, "ketchup", "snapshots")
     end
 
-    def self.generate_diff_html(baseline_dir:, current_dir:, output_path:)
-      baseline_images = Dir.glob(File.join(baseline_dir, "*.png")).map { |f| File.basename(f, ".png") }.sort
-      current_images = Dir.glob(File.join(current_dir, "*.png")).map { |f| File.basename(f, ".png") }.sort
-      all_names = (baseline_images + current_images).uniq.sort
-
-      rows = all_names.map do |name|
-        has_baseline = baseline_images.include?(name)
-        has_current = current_images.include?(name)
-
-        label = if !has_baseline
-                  " <span class=\"label new\">new</span>"
-                elsif !has_current
-                  " <span class=\"label removed\">removed</span>"
-                else
-                  ""
-                end
-
-        baseline_cell = if has_baseline
-                          baseline_rel = File.join("baseline", "#{name}.png")
-                          "<img src=\"#{baseline_rel}\" alt=\"baseline #{name}\">"
-                        else
-                          "<div class=\"placeholder\">No baseline</div>"
-                        end
-
-        current_cell = if has_current
-                         current_rel = File.join("current", "#{name}.png")
-                         "<img src=\"#{current_rel}\" alt=\"current #{name}\">"
-                       else
-                         "<div class=\"placeholder\">Removed</div>"
-                       end
-
-        <<~ROW
-          <div class="snapshot">
-            <h2>#{name}#{label}</h2>
-            <div class="pair">
-              <div class="side">
-                <h3>Baseline</h3>
-                #{baseline_cell}
-              </div>
-              <div class="side">
-                <h3>Current</h3>
-                #{current_cell}
-              </div>
-            </div>
-          </div>
-        ROW
-      end
-
-      html = <<~HTML
-        <!DOCTYPE html>
-        <html lang="en">
-        <head>
-          <meta charset="utf-8">
-          <title>Ketchup Snapshot Diff</title>
-          <style>
-            * { box-sizing: border-box; margin: 0; padding: 0; }
-            body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui, sans-serif; padding: 2rem; }
-            h1 { margin-bottom: 2rem; color: #fff; }
-            .snapshot { margin-bottom: 3rem; }
-            .snapshot h2 { margin-bottom: 1rem; color: #ccc; }
-            .label { font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; vertical-align: middle; }
-            .label.new { background: #2d6a4f; color: #b7e4c7; }
-            .label.removed { background: #6a2d2d; color: #e4b7b7; }
-            .pair { display: flex; gap: 1rem; }
-            .side { flex: 1; min-width: 0; }
-            .side h3 { margin-bottom: 0.5rem; color: #999; font-size: 0.875rem; text-transform: uppercase; }
-            .side img { width: 100%; border: 1px solid #333; border-radius: 4px; }
-            .placeholder { padding: 3rem; text-align: center; color: #666; border: 1px dashed #333; border-radius: 4px; }
-          </style>
-        </head>
-        <body>
-          <h1>Ketchup Snapshot Diff</h1>
-          #{rows.join("\n")}
-        </body>
-        </html>
-      HTML
-
-      File.write(output_path, html)
-    end
-
-    def self.generate_gallery_html(images_dir:, output_path:)
-      images = Dir.glob(File.join(images_dir, "*.png")).sort.map { |f| File.basename(f) }
-
-      items = images.map do |filename|
-        name = File.basename(filename, ".png")
-        <<~ITEM
-          <div class="snapshot">
-            <h2>#{name}</h2>
-            <img src="#{filename}" alt="#{name}">
-          </div>
-        ITEM
-      end
-
-      html = <<~HTML
-        <!DOCTYPE html>
-        <html lang="en">
-        <head>
-          <meta charset="utf-8">
-          <title>Ketchup Snapshots</title>
-          <style>
-            * { box-sizing: border-box; margin: 0; padding: 0; }
-            body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui, sans-serif; padding: 2rem; }
-            h1 { margin-bottom: 2rem; color: #fff; }
-            .snapshot { margin-bottom: 3rem; }
-            .snapshot h2 { margin-bottom: 1rem; color: #ccc; }
-            .snapshot img { max-width: 100%; border: 1px solid #333; border-radius: 4px; }
-          </style>
-        </head>
-        <body>
-          <h1>Ketchup Snapshots</h1>
-          #{items.join("\n")}
-        </body>
-        </html>
-      HTML
-
-      File.write(output_path, html)
-    end
-
     class Capture
-      def initialize(output_dir:, port:)
+      def initialize(output_dir:)
         require "ferrum"
+        require "puma"
+        require "puma/configuration"
 
         @output_dir = output_dir
-        @port = port
-        @base = "http://127.0.0.1:#{port}"
       end
 
       def call
         FileUtils.rm_rf(@output_dir)
         FileUtils.mkdir_p(@output_dir)
 
+        config = Puma::Configuration.new do |c|
+          c.app Web.freeze.app
+          c.bind "tcp://127.0.0.1:0"
+          c.log_requests false
+          c.quiet
+        end
+
+        launcher = Puma::Launcher.new(config)
+        thread = Thread.new { launcher.run }
+        sleep 1 until launcher.connected_ports.any?
+
+        @base = "http://127.0.0.1:#{launcher.connected_ports.first}"
         user = User.find_or_create(login: "snapshot@example.com") { |u| u.name = "Snapshot User" }
 
         @browser = Ferrum::Browser.new(
@@ -190,6 +84,8 @@ module Ketchup
         snap("after-complete")
       ensure
         @browser&.quit
+        launcher&.stop
+        thread&.join
       end
 
       private
diff --git a/snapshot_diff.erb b/snapshot_diff.erb
new file mode 100644
index 0000000..2114ce4
--- /dev/null
+++ b/snapshot_diff.erb
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Ketchup Snapshot Diff</title>
+  <style>
+    * { box-sizing: border-box; margin: 0; padding: 0; }
+    body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui, sans-serif; padding: 2rem; }
+    h1 { margin-bottom: 2rem; color: #fff; }
+    .snapshot { margin-bottom: 3rem; }
+    .snapshot h2 { margin-bottom: 1rem; color: #ccc; }
+    .label { font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 4px; vertical-align: middle; }
+    .label.new { background: #2d6a4f; color: #b7e4c7; }
+    .label.removed { background: #6a2d2d; color: #e4b7b7; }
+    .pair { display: flex; gap: 1rem; }
+    .side { flex: 1; min-width: 0; }
+    .side h3 { margin-bottom: 0.5rem; color: #999; font-size: 0.875rem; text-transform: uppercase; }
+    .side img { width: 100%; border: 1px solid #333; border-radius: 4px; }
+    .placeholder { padding: 3rem; text-align: center; color: #666; border: 1px dashed #333; border-radius: 4px; }
+  </style>
+</head>
+<body>
+  <h1>Ketchup Snapshot Diff</h1>
+  <% snapshots.each do |snap| %>
+  <div class="snapshot">
+    <h2>
+      <%= snap[:name] %>
+      <% if snap[:status] == :new %>
+      <span class="label new">new</span>
+      <% elsif snap[:status] == :removed %>
+      <span class="label removed">removed</span>
+      <% end %>
+    </h2>
+    <div class="pair">
+      <div class="side">
+        <h3>Baseline</h3>
+        <% if snap[:baseline] %>
+        <img src="<%= snap[:baseline] %>" alt="baseline <%= snap[:name] %>">
+        <% else %>
+        <div class="placeholder">No baseline</div>
+        <% end %>
+      </div>
+      <div class="side">
+        <h3>Current</h3>
+        <% if snap[:current] %>
+        <img src="<%= snap[:current] %>" alt="current <%= snap[:name] %>">
+        <% else %>
+        <div class="placeholder">Removed</div>
+        <% end %>
+      </div>
+    </div>
+  </div>
+  <% end %>
+</body>
+</html>
diff --git a/snapshot_gallery.erb b/snapshot_gallery.erb
new file mode 100644
index 0000000..9ddaf8c
--- /dev/null
+++ b/snapshot_gallery.erb
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title><%= title %></title>
+  <style>
+    * { box-sizing: border-box; margin: 0; padding: 0; }
+    body { background: #1a1a2e; color: #e0e0e0; font-family: system-ui, sans-serif; padding: 2rem; }
+    h1 { margin-bottom: 2rem; color: #fff; }
+    .snapshot { margin-bottom: 3rem; }
+    .snapshot h2 { margin-bottom: 1rem; color: #ccc; }
+    .snapshot img { max-width: 100%; border: 1px solid #333; border-radius: 4px; }
+  </style>
+</head>
+<body>
+  <h1><%= title %></h1>
+  <% images.each do |image| %>
+  <div class="snapshot">
+    <h2><%= image[:name] %></h2>
+    <img src="<%= image[:filename] %>" alt="<%= image[:name] %>">
+  </div>
+  <% end %>
+</body>
+</html>