#!/usr/bin/ruby # # A CGI/HTML application enabling one to identify lines in the agenda and/or # minutes which contain non-UTF-8 characters, and to select a replacement line. # # Requires: Ruby (1.8 or 1.9), the 'cgi-spa' gem, and JQuery. # # To install a suexec-compatible wrapper into your Apache DOCRoot, simply run: # # ruby zap-gremlins.rb --install=/path/to/docroot # # Place the following file into the same directory as the wrapper: # # http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js # require 'rubygems' require 'cgi-spa' # list of files to process $files ||= Dir['board_*.txt'] # replace invalid characters with the Unicode replacement character def cleanse(line) filter = Proc.new {|c| c.unpack('U').first rescue 0xFFFD} if line.respond_to?(:force_encoding) line.chars.map(&filter).pack('U*').force_encoding('utf-8') else line.gsub(/[^\x00-\x7f]+/) { |s| s.split(//u).map(&filter).pack('U*') } end end # replace specified line, after verifying the original $cgi.json do begin line = $param.line.to_i - 1 lines = open($param.file) {|file| file.readlines} if cleanse(lines[line]).strip == $param.original indent = cleanse(lines[line]).slice(/^\s*/) replacement = $param.replacement if replacement.respond_to? :force_encoding replacement.force_encoding(lines[line].encoding) end lines[line] = indent + replacement + "\n" open($param.file, 'w') {|file| file.write lines.join} {:status => :OK} else {:message => "File verification error."} end rescue {:message => $!.to_s + "\n" + $!.backtrace.join("\n")} end end # main output $cgi.html do |x| x.header do x.title 'Gremlin Zapper' x.style! %{ body {margin:0; background: #f5f5dc} footer pre, h2 {margin-left: 10%} fieldset {width: 80%; margin-left: 10%; border-color: #828} legend:before {content: "line "} h2:after {content: ":"} .disabled {opacity: 0.5; cursor: wait important!} h3.dirty {display: none} h3 {margin-left: 20%; color: #828; font-size: 200%} button {color: #828; background: #C9C} h1 { background: #C9C; color: #828; text-align: center; font: 40px "Times New Roman",serif; font-weight: normal; font-variant: small-caps; padding: 10px 0; border-bottom: 2px solid; margin: 0 0 0.5em 0; } legend { color: #dfd; background: #828; font-family: sans-serif; margin: 0 10%; padding: 0.2em 1em; } fieldset, legend, button { -moz-border-radius: 1em; -khtml-border-radius: 1em; } } x.script '', :src => 'jquery-1.3.2.min.js' end x.body do x.h1 'Gremlin Zapper' mac = Iconv.new('utf-8', 'Mac') win = Iconv.new('utf-8', 'Windows-1252') $files.each do |fname| x.section do x.h2 fname open(fname) do |file| dirty = false file.each_line do |line| begin line.unpack('U*') rescue dirty = true clean = cleanse(line) x.fieldset do x.legend file.lineno x.table do x.tr do x.td {x.button 'Src'} x.td {x.pre clean.strip} end x.tr do x.td {x.button 'Win'} x.td {x.pre((win.iconv(line) rescue clean).strip)} end x.tr do x.td {x.button 'Mac'} x.td {x.pre((mac.iconv(line) rescue clean).strip)} end end end end end x.h3 'Clean!', ({:class => 'dirty'} if dirty) end end end x.script! %{ $('section button').click(function() { // disable the fieldset/table var tr = $(this).closest('tr'); var fieldset = tr.closest('fieldset'); fieldset.addClass('disabled'); fieldset.find('button').attr('disabled','disabled'); // gather data: file, line, original (for verification), replacement args = { file : fieldset.siblings('h2').text(), line : fieldset.find('legend').text(), original : fieldset.find('tr:eq(0) td:eq(1) pre').text(), replacement : tr.find('td:eq(1) pre').text() }; // send the request $.getJSON("#{SELF?}", args, function(response) { if (response.message) { alert(response.message); fieldset.removeClass('disabled'); fieldset.find('button').removeAttr('disabled'); } else { fieldset.hide('blind'); if (fieldset.siblings('fieldset:visible').length == 0) { fieldset.siblings('h3').removeClass('dirty'); } } }); // don't propagate event return false; }); } end end # statements to include in installation files __END__ $files = Dir['board_*.txt']