refresh-dither-alt (3332B)
1 #!/usr/bin/env ruby 2 3 require "optparse" 4 require "shellwords" 5 require "tempfile" 6 require_relative "lib/image_alt_text" 7 8 ROOT = File.expand_path("..", __dir__) 9 # Match paired shortcodes, including quoted filenames and multiline alt text. 10 PAIRED_SHORTCODE = /(?<opening>\{\{(?<kind>[<%])\s*dither\s+(?![^{}]*\/\s*[>%]\}\})(?<args>(?:"[^"]*"|'[^']*'|`[^`]*`|[^"'`{}])*?)\s*[>%]\}\})(?<alt>.*?)(?<closing>\{\{\k<kind>\s*\/dither\s*[>%]\}\})/m 11 12 dry_run = false 13 options = OptionParser.new do |parser| 14 parser.banner = "Usage: bin/refresh-dither-alt [--dry-run] [POST_OR_DIRECTORY ...]" 15 parser.on("--dry-run", "List images without decrypting, calling Codex, or editing posts") { dry_run = true } 16 parser.on("-h", "--help", "Show this help") { puts parser; exit } 17 end 18 19 begin 20 options.parse! 21 inputs = ARGV.empty? ? [File.join(ROOT, "content")] : ARGV 22 files = inputs.flat_map do |path| 23 raise "Path not found: #{path}" unless File.exist?(path) 24 File.directory?(path) ? Dir.glob(File.join(path, "**", "*.{md,markdown,html}")) : [path] 25 end.map { |path| File.expand_path(path) }.uniq.sort 26 27 count = 0 28 failures = 0 29 files.each do |post| 30 original = File.read(post) 31 post_count = 0 32 begin 33 updated = original.gsub(PAIRED_SHORTCODE) do |shortcode| 34 match = Regexp.last_match 35 next shortcode if match[:alt].strip.empty? 36 next shortcode if match[:args].rstrip.end_with?("/") 37 38 arguments = match[:args].gsub(/`([^`]*)`/) { Shellwords.escape(Regexp.last_match(1)) } 39 filename = Shellwords.split(arguments).first 40 raise "Missing image filename" if filename.nil? || filename.empty? 41 encrypted = File.expand_path("#{filename}.enc", File.dirname(post)) 42 raise "Encrypted image not found: #{encrypted}" unless File.file?(encrypted) 43 44 puts "#{dry_run ? 'Would update' : 'Updating'} #{post}: #{filename}" 45 post_count += 1 46 next shortcode if dry_run 47 48 alt = nil 49 Dir.mktmpdir("dither-alt-") do |directory| 50 decrypted = File.join(directory, "image#{File.extname(filename)}") 51 output, status = Open3.capture2e( 52 "openssl", "aes-256-cbc", "-d", "-in", encrypted, "-out", decrypted, 53 "-pass", "file:#{File.join(ROOT, 'secret.key')}", "-iter", "1000000" 54 ) 55 raise "Decryption failed for #{filename}: #{output}" unless status.success? 56 alt = ImageAltText.generate(decrypted) 57 end 58 "#{match[:opening]}#{alt}#{match[:closing]}" 59 end 60 61 if updated != original 62 # Finish each post before replacing it; failed generations leave it intact. 63 Tempfile.create([".dither-alt-", ".tmp"], File.dirname(post)) do |temp| 64 temp.write(updated) 65 temp.flush 66 File.chmod(File.stat(post).mode & 0o777, temp.path) 67 raise "Post changed during generation: #{post}" unless File.read(post) == original 68 File.rename(temp.path, post) 69 end 70 end 71 count += post_count 72 rescue StandardError => error 73 warn "#{post}: #{error.message} (post left unchanged)" 74 failures += 1 75 end 76 end 77 puts "#{dry_run ? 'Found' : 'Processed'} #{count} shortcode(s); #{failures} failed post(s)." 78 exit(failures.zero? ? 0 : 1) 79 rescue OptionParser::ParseError, StandardError => error 80 abort error.message 81 end