#!/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']