colorful rat Ratfactor.com > Dave's Repos

reporat

A static website generator for Git repos written in Ruby.
git clone http://ratfactor.com/repos/reporat/reporat.git

reporat/reporat.rb

Download raw file: reporat.rb

1 #!/usr/bin/env ruby 2 3 # Copyright (C) 2023 Dave Gauer <dave@ratfactor.com> 4 # This program is free software. Please see the LICENSE file. 5 6 # Required modules from Ruby Std-Lib (not gems) 7 require 'fileutils' # For mkdir_p, cp, mv 8 require 'cgi' # For CGI.escapeHTML 9 10 # .git must exist (we must already be at root of repo) 11 if !Dir.exist?('.git') 12 puts "ERROR: Could not access .git directory." 13 puts "Please run this program from the root of a repo." 14 exit 3 15 end 16 17 # This program must be run from the root of a repo. 18 repo_dir = Dir.pwd 19 name = File.basename(repo_dir) 20 21 # Load and check config file 22 config_file = "#{ENV['HOME']}/.config/reporat.conf.rb" 23 if !File.exist?(config_file) 24 puts "ERROR: Could not find config file '#{config_file}'." 25 puts "See README.md for example." 26 exit 1 27 end 28 require config_file 29 [:my_output_dir, :my_header, :my_footer].each do |m| 30 if !self.respond_to?(m, :include_private) 31 puts "Config file must define method '#{m}'." 32 exit 2 33 end 34 end 35 36 # Call the first config method to get output directory 37 root_output_dir = my_output_dir() 38 39 # Get the repo description (almost certainly exists) 40 description = '' 41 if File.exist?('.git/description') 42 description = File.read('.git/description') 43 end 44 45 puts "RepoRat generating site for:" 46 puts " #{name}" 47 puts " #{description}" 48 49 # Now assemble output paths based on name 50 output_dir = "#{root_output_dir}/#{name}" 51 bare_output_dir = "#{output_dir}/#{name}.git" 52 53 # Get list of files in repo HEAD 54 # (Method cribbed from repo2html by m455.) 55 file_list_str = `git ls-tree -r --name-only HEAD` 56 file_list = file_list_str.split("\n").map(&:chomp) 57 58 # Detect README (very strict naming) and type of formatting 59 readme_type = 'text' 60 readme_file = file_list.find { |f| f.match?(/\bREADME(\.md)?$/) } 61 if !readme_file 62 puts "ERROR: Couldn't find README or README.md" 63 puts "(It must exist and be comitted to the Git repo.)" 64 exit 7 65 end 66 if readme_file.match?(/\.md$/) 67 readme_type = 'markdown' 68 end 69 70 # Prompt to create repo output directory if it doesn't exist yet 71 if !Dir.exist?(output_dir) 72 puts "Output directory '#{output_dir}' does not yet exist." 73 puts "Let's create it and populate it with a \"bare\" Git repo." 74 answer = nil 75 until answer == 'y' or answer == 'n' 76 print "Proceed? (y/n) " 77 answer = $stdin.gets.chomp 78 end 79 if answer == 'y' 80 # Create dir(s)! mkdir_p creates subdirs as needed 81 puts "Creating '#{output_dir}'..." 82 FileUtils.mkdir_p(output_dir) 83 # Create bare repo suitable for "dumb http" git cloning. 84 # (No need to puts here since Git says what it's doing) 85 `git clone --bare . #{bare_output_dir}` 86 else 87 puts "Okay, exiting!" 88 exit 6 89 end 90 end 91 92 # Make raw source files dir 93 raw_dir = "#{output_dir}/raw" 94 FileUtils.mkdir_p(raw_dir) 95 96 # Make html source files dir 97 html_dir = "#{output_dir}/html" 98 FileUtils.mkdir_p(html_dir) 99 100 # Is this file readable by humans? 101 # Returns empty string if readable, otherwise a reason it's not. 102 def is_readable(fname) 103 max_readable_avg = 100 # As determined by me :-) 104 lens = [] 105 total_lens = 0 106 longest_line = 0 107 control_chars = false 108 109 File.open(fname) do |f| 110 line_len = 0 111 f.each_char do |c| 112 113 # If found newline, count 114 line_len += 1 115 if c == "\n" 116 lens.push line_len 117 total_lens += line_len 118 line_len = 0 119 end 120 121 # First byte of encoded char 122 b = c.bytes[0] 123 if b < 9 124 control_chars = true 125 end 126 end 127 128 avg = total_lens / (lens.length+1) 129 130 if control_chars 131 return "it contains one or more control characters" 132 end 133 134 if avg > 100 135 return "the average line length, <b>#{avg}</b> chars, is too long" 136 end 137 138 return "" 139 end 140 end 141 142 file_page_count = 0 143 file_binary_count = 0 144 145 # Each file in repo... 146 file_list.each do |fname| 147 file_page_count += 1 148 149 # Make raw file subdir as needed 150 file_dir = File.dirname("#{raw_dir}/#{fname}") 151 if !Dir.exist?(file_dir) 152 puts "Creating directory '#{file_dir}'..." 153 FileUtils.mkdir_p(file_dir) 154 end 155 156 # copy source file to output 157 FileUtils.cp(fname, "#{raw_dir}/#{fname}") 158 159 # Make HTML output subdir as needed 160 file_html_dir = File.dirname("#{html_dir}/#{fname}") 161 if !Dir.exist?(file_html_dir) 162 puts "Creating directory '#{file_html_dir}'..." 163 FileUtils.mkdir_p(file_html_dir) 164 end 165 166 # make an html file for this file 167 html_out = "#{html_dir}/#{fname}.html" 168 169 # Figure out a path relative to the mini-site's root 170 rrp = '../' * (html_out.count('/') - raw_dir.count('/')) 171 172 File.open(html_out, 'w') do |f| 173 f.puts my_header({ 174 name: name, 175 description: description, 176 page_type: :file, 177 root_rel_prefix: rrp, 178 file_fname: fname, 179 }) 180 181 f.puts "<h2>#{name}/#{fname}</h2>" 182 183 # link to raw source file 184 f.puts "<p>Download raw file: <a href=\"#{rrp}raw/#{fname}\">#{fname}</a></p>" 185 186 # if image, display in page 187 if fname.end_with?(".jpg", ".gif", ".png", ".svg") 188 189 f.puts "<img src=\"#{rrp}raw/#{fname}\" alt=\"\" style=\"margin: 2em auto; display: block;\">" 190 end 191 192 # Human-readable file? ("" or string contains reason it isn't): 193 readability = is_readable(fname) 194 195 # if source, display with line nums 196 if readability == "" 197 f.puts "<div class=\"source-file\">" 198 ln = 0 199 src_txt = File.read(fname) rescue fail("Couldn't read #{fname}") 200 src_txt.each_line do |l| 201 ln += 1 202 hl = CGI.escapeHTML(l) 203 lns = " " * (6 - ln.to_s.length) 204 f.print "<a id=\"L#{ln}\" href=\"#L#{ln}\">#{lns}#{ln} </a>#{hl}" 205 end 206 f.puts "</div>" 207 else 208 file_binary_count += 1 209 f.puts "<div>(This file was determined to not be human-readable because #{readability}.)</div>" 210 end 211 212 f.puts my_footer() 213 end 214 end 215 216 # Create file list page. 217 files_page_out = "#{output_dir}/files.html" 218 219 File.open(files_page_out, 'w') do |f| 220 f.puts my_header({ 221 name: name, 222 description: description, 223 page_type: :files_list, 224 root_rel_prefix: '', 225 }) 226 227 f.puts "<h2>Files</h2>" 228 f.puts "<p>This repo contains #{file_list.length} file(s):</p>" 229 f.puts "<ul class=\"file-list\">" 230 file_list.each do |fname| 231 f.puts " <li><a href=\"html/#{fname}.html\">#{fname}</a></li>" 232 end 233 f.puts "</ul>" 234 235 f.puts my_footer() 236 end 237 238 # Create commit history page. 239 commits_page_out = "#{output_dir}/commits.html" 240 File.open(commits_page_out, 'w') do |f| 241 f.puts my_header({ 242 name: name, 243 description: description, 244 page_type: :commits, 245 root_rel_prefix: '', 246 }) 247 248 f.puts "<h2>Commit history</h2>" 249 f.puts "<pre class=\"commits\">" 250 f.puts `git log` 251 f.puts "</pre>" 252 253 f.puts my_footer() 254 end 255 256 # Create project landing page. 257 index_out = "#{output_dir}/index.html" 258 File.open(index_out, 'w') do |f| 259 f.puts my_header({ 260 name: name, 261 description: description, 262 page_type: :main, 263 root_rel_prefix: '', 264 }) 265 266 file_show_max = 20 267 268 f.puts "<h2>Files</h2>" 269 f.puts "<ul>" 270 file_list.take(file_show_max).each do |fname| 271 f.puts " <li><a href=\"html/#{fname}.html\">#{fname}</a></li>" 272 end 273 if file_list.length > file_show_max 274 f.puts "<li>...<br><a href=\"files.html\">View all #{file_list.length} files</a></li>" 275 end 276 f.puts "</ul>" 277 278 # start div.readme: 279 f.puts "<div class=\"readme\"><b class=\"filename\">#{readme_file}</b><br>" 280 281 if readme_type == 'text' 282 f.puts "<pre>" 283 f.puts File.read(readme_file) 284 f.puts "</pre>" 285 end 286 287 if readme_type == 'markdown' 288 #require 'rdoc' 289 #data = File.read(readme_file) 290 #fmt = RDoc::Markup::ToHtml.new(RDoc::Options.new, nil) 291 #html = RDoc::Markdown.parse(data).accept(fmt) 292 #f.puts html 293 readme_html = `markdown #{readme_file}` 294 f.puts readme_html 295 end 296 297 # end div.readme: 298 f.puts "</div>" 299 300 f.puts my_footer() 301 end 302 303 # Lastly, sync and update the output's bare repo for "dumb http" Git cloning. 304 update_bare = <<CMD 305 cd #{bare_output_dir} 306 git fetch #{repo_dir} '*:*' 307 git update-server-info 308 CMD 309 `#{update_bare}` 310 311 puts "Output complete at '#{output_dir}'"