├── .gitignore ├── win32 ├── iconv.dll ├── charset.dll ├── lib │ └── i386-mswin32 │ │ └── iconv.so └── mouseHole.nsi ├── static ├── icons │ ├── door.png │ ├── feed.png │ ├── ruby.png │ ├── broken.png │ ├── database.png │ ├── lightbulb.png │ └── ruby_gear.png ├── images │ ├── doorway.png │ ├── doorway-jam.png │ └── doorway-tile.png ├── css │ ├── mounts.css │ └── doorway.css └── js │ ├── mouseHole.js │ ├── interface.js │ └── jquery.js ├── test ├── load_files.rb ├── files │ └── basic.xhtml ├── test_proxy.rb └── test_parser.rb ├── lib ├── mouseHole │ ├── basicmount.rb │ ├── textconverter.rb │ ├── mixins │ │ ├── logger.rb │ │ └── handler.rb │ ├── installer.rb │ ├── hacks │ │ ├── json.rb │ │ ├── http.rb │ │ ├── uri.rb │ │ ├── mongrel.rb │ │ └── acts_as_list.rb │ ├── feedconverter.rb │ ├── converters.rb │ ├── htmlconverter.rb │ ├── helpers.rb │ ├── models.rb │ ├── proxyhandler.rb │ ├── controllers.rb │ ├── page.rb │ ├── central.rb │ ├── app.rb │ └── views.rb ├── mouseHole.rb ├── uuidtools.rb ├── feed_tools.rb └── redcloth.rb ├── samples ├── junebug.app.rb ├── comicalt.user.rb ├── proxylike.user.rb ├── coral.rb ├── google-rand.user.rb └── freakylike.user.rb ├── COPYING ├── CHANGELOG ├── Rakefile ├── README.rdoc └── bin └── mouseHole /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /win32/iconv.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/win32/iconv.dll -------------------------------------------------------------------------------- /win32/charset.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/win32/charset.dll -------------------------------------------------------------------------------- /static/icons/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/door.png -------------------------------------------------------------------------------- /static/icons/feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/feed.png -------------------------------------------------------------------------------- /static/icons/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/ruby.png -------------------------------------------------------------------------------- /static/icons/broken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/broken.png -------------------------------------------------------------------------------- /static/icons/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/database.png -------------------------------------------------------------------------------- /static/images/doorway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/images/doorway.png -------------------------------------------------------------------------------- /static/icons/lightbulb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/lightbulb.png -------------------------------------------------------------------------------- /static/icons/ruby_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/icons/ruby_gear.png -------------------------------------------------------------------------------- /static/images/doorway-jam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/images/doorway-jam.png -------------------------------------------------------------------------------- /static/images/doorway-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/static/images/doorway-tile.png -------------------------------------------------------------------------------- /win32/lib/i386-mswin32/iconv.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nogweii/mousehole/HEAD/win32/lib/i386-mswin32/iconv.so -------------------------------------------------------------------------------- /static/css/mounts.css: -------------------------------------------------------------------------------- 1 | #mh2 { 2 | color: #333; 3 | background-color: #ECFADE; 4 | font: normal 11pt verdana, arial, sans-serif; 5 | text-align: left; 6 | font-size: .9em; 7 | margin: 0; padding: 4px; 8 | } 9 | -------------------------------------------------------------------------------- /test/load_files.rb: -------------------------------------------------------------------------------- 1 | module TestFiles 2 | Dir.chdir(File.dirname(__FILE__)) do 3 | Dir['files/*.{html,xhtml}'].each do |fname| 4 | const_set fname[%r!/(\w+)\.\w+$!, 1].upcase, IO.read(fname) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/mouseHole/basicmount.rb: -------------------------------------------------------------------------------- 1 | class MouseHole::BasicMount 2 | def debug(msg); @logger.debug(msg) end 3 | def error(msg); @logger.error(msg) end 4 | def fatal(msg); @logger.fatal(msg) end 5 | def info(msg); @logger.info(msg) end 6 | def warn(msg); @logger.warn(msg) end 7 | end 8 | -------------------------------------------------------------------------------- /lib/mouseHole/textconverter.rb: -------------------------------------------------------------------------------- 1 | require 'mouseHole/converters' 2 | 3 | module MouseHole::Converters 4 | 5 | class Text < Base 6 | mime_type "text/*" 7 | 8 | class << self 9 | 10 | def parse(page, body) 11 | body 12 | end 13 | 14 | def output(document) 15 | document.to_s 16 | end 17 | 18 | end 19 | 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/mouseHole/mixins/logger.rb: -------------------------------------------------------------------------------- 1 | module MouseHole 2 | module LoggerMixin 3 | [:debug, :info, :warn, :error].each do |m| 4 | define_method(m) do |txt, *opts| 5 | opts = opts.first || {} 6 | if opts[:since] 7 | txt = "%s (%0.4f)" % [txt, Time.now.to_f - opts[:since].to_f] 8 | end 9 | MouseHole::CENTRAL.logger.send m, txt 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mouseHole/installer.rb: -------------------------------------------------------------------------------- 1 | class MouseHole::InstallerApp < MouseHole::App 2 | title 'Built-In Installer' 3 | description 'Senses MH2 user scripts and offers to install them.' 4 | version '2.0' 5 | accept Text 6 | 7 | + url("http://*.user.rb") 8 | 9 | def rewrite page 10 | page.headers['Location'] = "http://mh/doorway/install?url=#{page.location}" 11 | page.status = 303 12 | document.replace "" 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/mouseHole/hacks/json.rb: -------------------------------------------------------------------------------- 1 | require 'json/pure' 2 | 3 | class Time 4 | def to_json(*a) 5 | "new Date(#{to_i*1000})" 6 | end 7 | end 8 | 9 | module JSON 10 | class Parser 11 | DATE = /new Date\((\d+)\)/ 12 | alias_method :parse_value2, :parse_value 13 | def parse_value 14 | case 15 | when scan(DATE) 16 | Time.at(self[1].to_i/1000) 17 | else 18 | parse_value2 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mouseHole/hacks/http.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | class Net::HTTP 4 | alias __request__ request 5 | 6 | # Replace the request method in Net::HTTP to sniff the body type 7 | # and set the stream if appropriate 8 | def request(req, body = nil, &block) 9 | if body != nil && body.respond_to?(:read) 10 | req.body_stream = body 11 | return __request__(req, nil, &block) 12 | else 13 | return __request__(req, body, &block) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mouseHole/feedconverter.rb: -------------------------------------------------------------------------------- 1 | require 'mouseHole/converters' 2 | 3 | module MouseHole 4 | module Converters 5 | 6 | class Feed < Base 7 | 8 | mime_type "text/xml" 9 | mime_type "application/xml" 10 | mime_type "application/atom+xml" 11 | 12 | def self.parse(page, body) 13 | require 'feed_tools' 14 | FeedTools.feed_cache = nil 15 | feed = FeedTools::Feed.new 16 | feed.url = page.location.to_s 17 | feed.feed_data_type = :xml 18 | feed.feed_data = body 19 | feed 20 | end 21 | 22 | def self.output(feed, page) 23 | page['content-type'] = 'application/xml+atom' 24 | page.body = feed.build_xml('atom', 1.0) 25 | end 26 | 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/files/basic.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample XHTML 6 | 7 | 8 | 9 | 10 | 11 |

Sample XHTML for MouseHole 2.

12 |

Please filter me!

13 |

The third paragraph

14 |

THE FINAL PARAGRAPH

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/mouseHole/converters.rb: -------------------------------------------------------------------------------- 1 | # Module containing possible mime type convertors (in order to be rewriteable content, a 2 | # convertor class must identify itself as able to handle a mime type.) 3 | module MouseHole 4 | module Converters 5 | 6 | def self.detect_by_mime_type type_str 7 | self.constants.map { |c| const_get(c) }.detect do |c| 8 | if c.respond_to? :handles_mime_type? 9 | c.handles_mime_type? type_str 10 | end 11 | end 12 | end 13 | 14 | class Base 15 | def self.mime_type type_match 16 | if type_match.index('*') 17 | type_match = /^#{Regexp::quote(type_match).gsub(/\\\*/, '.*')}$/ 18 | end 19 | @mime_types ||= [] 20 | @mime_types << type_match 21 | end 22 | def self.handles_mime_type? type_str 23 | (@mime_types || []).any? { |mt| mt === type_str } 24 | end 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/mouseHole/htmlconverter.rb: -------------------------------------------------------------------------------- 1 | require 'mouseHole/converters' 2 | 3 | module MouseHole::Converters 4 | 5 | class HTML < Base 6 | mime_type "text/html" 7 | mime_type "application/xhtml+xml" 8 | 9 | class << self 10 | 11 | def parse(page, body) 12 | charset = 'raw' 13 | if "#{ page.headers['content-type'] }" =~ /charset=([\w\-]+)/ 14 | charset = $1 15 | elsif body =~ %r!]+charset\s*=\s*([\w\-]+)! 16 | charset = $1 17 | end 18 | parse_xhtml(body, true, charset) 19 | end 20 | 21 | def output(document) 22 | if document.respond_to? :to_original_html 23 | document.to_original_html 24 | else 25 | document.to_s 26 | end 27 | end 28 | 29 | def parse_xhtml(str, full_doc = false, charset = nil) 30 | Hpricot.parse(str) 31 | end 32 | 33 | end 34 | 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /samples/junebug.app.rb: -------------------------------------------------------------------------------- 1 | # (From _why: http://rubyforge.org/pipermail/mousehole-scripters/2007-January/000241.html) 2 | # 3 | # MouseHole2 is built on Camping, so you can put Camping apps right in the 4 | # ~/.mouseHole/ directory and they'll startup. Camping's blog example, Tepee, 5 | # etc. 6 | # 7 | # "Junebug is a simple, clean, minimalist wiki intended for personal use." 8 | # (http://www.junebugwiki.com/) 9 | # 10 | # Junebug is written in Camping and follows Camping's rules, but is distributed as 11 | # a Gem. To install Junebug: gem install junebug-wiki. 12 | # 13 | # Copy this file to ~/.mouseHole/junebug.app.rb. And start up 14 | # mouseHole and it'll be mounted at http://localhost:3704/junebug. 15 | # 16 | 17 | require 'junebug/config' 18 | JUNEBUG_ROOT = ENV['JUNEBUG_ROOT'] = File.join(Junebug::Config.rootdir, "deploy") 19 | require(Junebug::Config.script) 20 | 21 | def Junebug.config; {'startpage' => 'Home_Page'}; end 22 | Junebug.create 23 | -------------------------------------------------------------------------------- /static/js/mouseHole.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | /* doorblock sorting and pooling */ 4 | var all_blocks = $('#fullpool').html(); 5 | if (all_blocks != '') 6 | { 7 | var altered = function(ary) { 8 | $.ajax({type: 'POST', url: '/doorway/blocks', data: ary[0].hash}); 9 | $('#fullpool').html(all_blocks).Sortable(doorsort); 10 | $('#userpool a.del').click(removed); 11 | } 12 | var removed = function() { 13 | $('../../../..', this).remove(); 14 | altered([$.SortSerialize('userpool')]); 15 | } 16 | var doorsort = { 17 | accept: 'blocksort', 18 | activeclass: 'blockactive', 19 | hoverclass: 'blockhover', 20 | helperclass: 'sorthelper', 21 | opacity: 0.8, 22 | fx: 200, 23 | revert: true, 24 | tolerance: 'intersect', 25 | onchange: altered 26 | }; 27 | $('ol.doorblocks').Sortable(doorsort); 28 | $('#userpool a.del').click(removed); 29 | } 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /lib/mouseHole/helpers.rb: -------------------------------------------------------------------------------- 1 | module MouseHole::Helpers 2 | 3 | def rss( io ) 4 | feed = Builder::XmlMarkup.new( :target => io, :indent => 2 ) 5 | feed.instruct! :xml, :version => "1.0", :encoding => "UTF-8" 6 | feed.rss( 'xmlns:admin' => 'http://webns.net/mvcb/', 7 | 'xmlns:sy' => 'http://purl.org/rss/1.0/modules/syndication/', 8 | 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/', 9 | 'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 10 | 'version' => '2.0' ) do |rss| 11 | rss.channel do |c| 12 | # channel stuffs 13 | c.dc :language, "en-us" 14 | c.dc :creator, "MouseHole #{ MouseHole::VERSION }" 15 | c.dc :date, Time.now.utc.strftime( "%Y-%m-%dT%H:%M:%S+00:00" ) 16 | c.admin :generatorAgent, "rdf:resource" => "http://builder.rubyforge.org/" 17 | c.sy :updatePeriod, "hourly" 18 | c.sy :updateFrequency, 1 19 | yield c 20 | end 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /win32/mouseHole.nsi: -------------------------------------------------------------------------------- 1 | ; example1.nsi 2 | ; 3 | ; This script is perhaps one of the simplest NSIs you can make. All of the 4 | ; optional settings are left to their default settings. The installer simply 5 | ; prompts the user asking them where to install, and drops a copy of example1.nsi 6 | ; there. 7 | 8 | ;-------------------------------- 9 | 10 | ; The name of the installer 11 | Name "MouseHole 1.1" 12 | 13 | ; The file to write 14 | OutFile "mouseHole-1.1.exe" 15 | 16 | ; The default installation directory 17 | InstallDir $PROGRAMFILES\MouseHole 18 | 19 | ;-------------------------------- 20 | 21 | ; Pages 22 | 23 | Page directory 24 | Page instfiles 25 | 26 | ;-------------------------------- 27 | 28 | ; The stuff to install 29 | Section "" ;No components page, name is not important 30 | 31 | ; Application directory 32 | SetOutPath $INSTDIR 33 | File mouseHole.exe 34 | File iconv.dll 35 | File gdbm.dll 36 | File charset.dll 37 | 38 | ; Images 39 | SetOutPath $INSTDIR\images 40 | File images\mouseHole-neon.png 41 | 42 | SectionEnd ; end the section 43 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | copyright (c) 2006-2009 why the lucky stiff, however here is how I exercise the right: 2 | copyright (c) 2009+ Colin 'Evaryont' Shea, however here is how I exercise the right: 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /samples/comicalt.user.rb: -------------------------------------------------------------------------------- 1 | # Simple MouseHole 2.0 script, based on the Greasemonkey script 2 | # by Adam Vandenberg. His is GPL, so this is GPL. 3 | # 4 | class ComicAlt < MouseHole::App 5 | title "Comics Alt Text" 6 | namespace "http://adamv.com/greases/" 7 | description 'Shows the "hover text" for some comics on the page' 8 | version "0.3" 9 | + url("http://achewood.com/*") 10 | + url("http://*.achewood.com/*") 11 | + url("http://qwantz.com/*") 12 | + url("http://*.qwantz.com/*") 13 | 14 | COMICS = { 15 | "achewood" => 'img[@src^="/comic.php?date="]', 16 | "qwantz" => 'img[@src^="http://www.qwantz.com/comics/"]' 17 | } 18 | 19 | # the pages flow through here 20 | def rewrite(page) 21 | whichSite, xpath = 22 | COMICS.detect do |key,| 23 | page.location.host.include? key 24 | end 25 | return unless whichSite 26 | 27 | comic = document.at(xpath) 28 | return unless comic 29 | 30 | if comic['title'] 31 | div = Hpricot.make("
(#{ comic['title'] })
") 32 | comic.parent.insert_after div, comic 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mouseHole/models.rb: -------------------------------------------------------------------------------- 1 | module MouseHole::Models 2 | 3 | class App < Base 4 | has_many :blocks 5 | attr_accessor :klass 6 | serialize :matches 7 | end 8 | 9 | class Block < Base 10 | belongs_to :app 11 | acts_as_list 12 | serialize :config 13 | end 14 | 15 | class CreateMouseHole < V 1.0 16 | def self.up 17 | create_table :mousehole_apps do |t| 18 | t.column :id, :integer, :null => false 19 | t.column :script, :string 20 | t.column :uri, :string 21 | t.column :active, :integer, :null => false, :default => 1 22 | t.column :matches, :text 23 | t.column :created_at, :timestamp 24 | end 25 | end 26 | def self.down 27 | drop_table :mousehole_apps 28 | end 29 | end 30 | 31 | class CreateDoorway < V 1.01 32 | def self.up 33 | create_table :mousehole_blocks do |t| 34 | t.column :id, :integer, :null => false 35 | t.column :app_id, :integer, :null => false 36 | t.column :title, :string 37 | t.column :position, :integer 38 | t.column :config, :text 39 | t.column :created_at, :timestamp 40 | end 41 | end 42 | def self.down 43 | drop_table :mousehole_blocks 44 | end 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /samples/proxylike.user.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'resolv' 3 | 4 | class Array 5 | def to_h 6 | self.inject({}) do |hash, value| 7 | hash[value.first] = value.last ; hash 8 | end 9 | end 10 | end 11 | 12 | class ProxyLike < MouseHole::App 13 | title 'ProxyLike' 14 | namespace 'http://whytheluckystiff.net/mouseHole/' 15 | description %{ 16 | Run pages through the proxy by passing them in on the URL. 17 | For example, to view Boing Boing through the proxy, use: 18 | http://127.0.0.1:3704/http://boingboing.net/ 19 | } 20 | version '2.1' 21 | 22 | mount "http:" do |page| 23 | page.status = 200 # OK 24 | proxyAddr = URI("http://#{ page.headers['host'] }/") 25 | addresses = Resolv.getaddresses(proxyAddr.host) 26 | if (address = addresses.find { |a| a =~ /([0-9]+\.){3}[0-9]+/ }) # stupid ip address detect 27 | mH = "http://#{ address }:#{proxyAddr.port}/" 28 | else 29 | mH = proxyAddr.to_s 30 | end 31 | uri = URI(page.location.to_s[1..-1]) 32 | options = {:proxy => mH}.merge(page.headers.to_h) 33 | options.delete_if { |k,v| %w(host accept-encoding).include? k } 34 | page.document = 35 | uri.open(options) do |f| 36 | base_uri = uri.dup 37 | base_uri.path = '/' 38 | base_href f.read, base_uri, mH 39 | end 40 | end 41 | 42 | def self.base_href( html, uri, mh ) 43 | html.gsub( /((href|action|src)\s*=\s*["']?)(#{ uri }|\/+)/, "\\1#{ mh }#{ uri }" ). 44 | sub( / (Models::App.table_exists? ? 1.0 : 0.0) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /samples/google-rand.user.rb: -------------------------------------------------------------------------------- 1 | # 2 | # kevin ballard's proof-of-concept for a google ping of mouseHole 3 | # modified by why to count hits, simplified register_uri 4 | # 5 | MouseHole.script do 6 | # declaration 7 | name "Rewriting Test" 8 | namespace "kevin@sb.org" 9 | description "Tests the new rewriting stuff." 10 | include_match :scheme => 'http', :host => %r{^(www\.)?google\.com$}, :path => %r{^(/(index\.html)?)?$} 11 | version "0.2" 12 | 13 | rewrite do |req, res| 14 | document.elements['//head'].add_element 'script', 'type' => 'text/javascript', 'src' => reg('test.js') 15 | lucky = document.elements['//input[@name="btnI"]'] 16 | lucky.parent.add_element 'input', 'type' => 'submit', 'value' => 'Random', 'onclick' => 'pingMouseHole(); return false;' 17 | end 18 | 19 | register_uri "test.js" do |uri, req, res| 20 | res['Content-Type'] = 'text/javascript' 21 | res.body = <<-EOF 22 | function createRequestObject() { 23 | var ro; 24 | var browser = navigator.appName; 25 | if(browser == "Microsoft Internet Explorer"){ 26 | ro = new ActiveXObject("Microsoft.XMLHTTP"); 27 | }else{ 28 | ro = new XMLHttpRequest(); 29 | } 30 | return ro; 31 | } 32 | 33 | var http = createRequestObject(); 34 | 35 | function sndReq(action, handler) { 36 | http.open('get', action); 37 | http.onreadystatechange = function() { 38 | if(http.readyState == 4){ 39 | handler(http.responseText); 40 | } 41 | }; 42 | http.send(null); 43 | } 44 | 45 | function pingMouseHole() { 46 | sndReq('#{ reg 'ping' }', function(txt) { 47 | alert(txt); 48 | }) 49 | } 50 | EOF 51 | end 52 | 53 | register_uri "ping" do 54 | host = request.request_uri.host 55 | @counter ||= {} 56 | @counter[host] ||= 0 57 | @counter[host] += 1 58 | response['Content-Type'] = 'text/plain' 59 | response.body = "Random ##{ @counter[host] } from #{ request.request_uri.host }: #{ rand 100 }" 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/test_proxy.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | 4 | require 'test/unit' 5 | require 'ostruct' 6 | require 'rubygems' 7 | require 'mouseHole' 8 | require 'mongrel' 9 | require 'mongrel/camping' 10 | require 'net/http' 11 | require 'uri' 12 | require 'timeout' 13 | 14 | options = Camping::H[] 15 | options.logger = Logger.new STDOUT 16 | options.logger.level = Logger::INFO 17 | options.database ||= {:adapter => 'sqlite3', :database => 'mh_test.db'} 18 | 19 | $proxy_address = ['127.0.0.1', 9998] 20 | $server = Mongrel::HttpServer.new(*$proxy_address) 21 | MouseHole::CENTRAL = MouseHole::Central.new($server, options) 22 | 23 | class TestProxy < Test::Unit::TestCase 24 | def setup 25 | $server.run 26 | 27 | doorway = Mongrel::Camping::CampingHandler.new(MouseHole) 28 | $server.register("/doorway", doorway) 29 | $server.register('http:', MouseHole::ProxyHandler.new(MouseHole::CENTRAL)) 30 | $server.register('/', Mongrel::Camping::CampingHandler.new(MouseHole)) 31 | sleep(1) 32 | 33 | @client = Net::HTTP.new(*$proxy_address) 34 | @proxy_class = Net::HTTP::Proxy(*$proxy_address) 35 | end 36 | 37 | def teardown 38 | end 39 | 40 | def test_doorway 41 | res = @client.request_get('/doorway') 42 | assert res != nil, "Didn't get a response" 43 | assert res.body =~ /MouseHole/, "Couldn't find doorway" 44 | end 45 | 46 | def test_proxy 47 | def lagado_test(klass) 48 | res = klass.get_response(URI.parse('http://www.lagado.com/proxy-test')) 49 | assert res != nil, "Didn't get a response" 50 | res 51 | end 52 | 53 | res = lagado_test(Net::HTTP) 54 | assert res.body =~ /NOT to have come via a proxy/, "Non-proxy didn't work as expected" 55 | 56 | res = lagado_test(Net::HTTP::Proxy(*$proxy_address)) 57 | assert res.body =~ /This request appears to have come via a proxy/, "Proxy didn't work" 58 | end 59 | 60 | # def test_ssl 61 | # # Mongrel does not support SSL 62 | # url = "https://javacc.dev.java.net/" 63 | # res = @proxy_class.get_response(URI.parse(url)) 64 | # assert res != nil, "Didn't get a response" 65 | #end 66 | end 67 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | --- %YAML:1.0 2 | - version: 1.3 3 | date: 2005-10-09 4 | changes: 5 | lib/mouseHole.rb: separated into classes under lib/mouseHole/*.rb. 6 | lib/mouseHole/proxyserver.rb: 7 | - new streaming support!! (resources not handled by MouseHole are streamed to the browser.) 8 | - decode bug fix from Daniel Sheppard, what a guy. (he also contributed the converters stuff.) 9 | - use converters to parse data and farm out to scripts. 10 | lib/mouseHole/starmonkey.rb: now GM_xmlhttprequest works. 11 | lib/mouseHole/userscript.rb: allow rewrites based on converters used. 12 | 13 | - version: 1.2 14 | date: 2005-09-28 15 | changes: 16 | lib/mouseHole.rb: 17 | - support for either Tidy or HTree as the cleaner. 18 | - now including Builder, an XML construction kit from Jim Weirich. 19 | - matching has been rethunk, matches occur in the order you specify, alternate include 20 | and exclude as you wish, folks. 21 | - reset to factory defaults button on each script's config page. also, uninstall button. 22 | - new register_uri command which allows a script to pass data to a foreign domain. 23 | - support for Greasemonkey scripts which don't leverage the GM_API. (Starmonkey 24 | code by MenTaLguY.) 25 | - support for `mh' and `mouse.hole' hosts. also, mounts are available as `mount' or 26 | also `mouse.mount'. 27 | - gzipped content allowed (from Ryan Leavengood's Wonderland.) 28 | - rss feed of installed user scripts. 29 | - caching of non-altered pages allowed. 30 | - minor fixes to support Ruby 1.8.3. 31 | 32 | lib/dnshack.rb: allow invalid hostnames (cause of http://___._/) and ensure the HOSTS hash works 33 | under any Ruby 1.8.x. 34 | 35 | lib/redcloth.rb: RedCloth included now. 36 | 37 | lib/json/objects.rb: empty hash and empty array fixes. 38 | 39 | - version: 1.1 40 | date: 2005-09-02 41 | changes: 42 | lib/mouseHole.rb: added support for Greasemonkey-like URL matching and 43 | an automated installation process. 44 | 45 | - version: 1.0 46 | date: 2005-08-29 47 | changes: 48 | lib/mouseHole.rb: initial mouseHole release, included HTree for cleaning HTML 49 | and basically ran in the installation directory. 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/clean' 3 | require 'rake/gempackagetask' 4 | require 'rake/rdoctask' 5 | require 'rake/testtask' 6 | require 'fileutils' 7 | include FileUtils 8 | 9 | NAME = "mouseHole" 10 | REV = File.read(".svn/entries")[/committed-rev="(\d+)"/, 1] rescue nil 11 | VERS = "1.9" + (REV ? ".#{REV}" : "") 12 | CLEAN.include ['**/.*.sw?', '*.gem', '.config'] 13 | 14 | Rake::RDocTask.new do |rdoc| 15 | rdoc.rdoc_dir = 'doc/rdoc' 16 | rdoc.options << '--line-numbers' 17 | rdoc.rdoc_files.add ['README.rdoc', 'COPYING', 'lib/**/*.rb', 'doc/**/*.rdoc'] 18 | end 19 | 20 | desc "Packages up MouseHole 2." 21 | task :default => [:package] 22 | task :package => [:clean] 23 | 24 | desc "Run all the tests" 25 | Rake::TestTask.new do |t| 26 | t.libs << "test" 27 | t.test_files = FileList['test/test_*.rb'] 28 | t.verbose = true 29 | end 30 | 31 | spec = 32 | Gem::Specification.new do |s| 33 | s.name = NAME 34 | s.version = VERS 35 | s.platform = Gem::Platform::RUBY 36 | s.has_rdoc = false 37 | s.extra_rdoc_files = [ "README.rdoc" ] 38 | s.summary = "a scriptable proxy, an alternative to Greasemonkey and personal web server." 39 | s.description = s.summary 40 | s.executables = ['mouseHole'] 41 | 42 | s.author = "Colin 'Evaryont' Shea" 43 | s.email = "colin@evaryont.me" 44 | s.homepage = "http://github.com/evaryont/mousehole" 45 | 46 | s.add_dependency('camping-omnibus', '>= 1.5.180') 47 | s.add_dependency('hpricot', '>=0.5') 48 | s.add_dependency('json', '>= 1.0.2') 49 | s.required_ruby_version = '>= 1.8.4' 50 | 51 | s.files = %w(COPYING README.rdoc Rakefile) + 52 | Dir.glob("{bin,doc/rdoc,test,lib,static}/**/*") + 53 | Dir.glob("ext/**/*.{h,c,rb}") + 54 | Dir.glob("samples/**/*.rb") + 55 | Dir.glob("tools/*.rb") 56 | 57 | s.require_path = "lib" 58 | # s.extensions = FileList["ext/**/extconf.rb"].to_a 59 | s.bindir = "bin" 60 | end 61 | 62 | Rake::GemPackageTask.new(spec) do |p| 63 | p.need_tar = true 64 | p.gem_spec = spec 65 | end 66 | 67 | task :install do 68 | sh %{rake package} 69 | sh %{sudo gem install pkg/#{NAME}-#{VERS}} 70 | end 71 | 72 | task :uninstall => [:clean] do 73 | sh %{sudo gem uninstall mongrel} 74 | end 75 | -------------------------------------------------------------------------------- /lib/mouseHole/mixins/handler.rb: -------------------------------------------------------------------------------- 1 | module MouseHole 2 | 3 | module HandlerMixin 4 | 5 | def proxy_auth(req, res) 6 | if proc = @config[:ProxyAuthProc] 7 | proc.call(req, res) 8 | end 9 | req.header.delete("proxy-authorization") 10 | end 11 | 12 | # Some header fields shuold not be transfered. 13 | HopByHop = %w( connection keep-alive proxy-authenticate upgrade 14 | proxy-authorization te trailers transfer-encoding ) 15 | ShouldNotTransfer = %w( proxy-connection ) 16 | def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end 17 | 18 | def choose_header(src, dst) 19 | connections = split_field(src['connection'].to_s) 20 | src.each do |key, value| 21 | key = key.downcase 22 | if HopByHop.member?(key) || # RFC2616: 13.5.1 23 | connections.member?(key) || # RFC2616: 14.10 24 | ShouldNotTransfer.member?(key) # pragmatics 25 | # @logger.debug("choose_header: `#{key}: #{value}'") 26 | next 27 | end 28 | dst << [key.downcase, value.length == 1 ? value.first : value] 29 | end 30 | end 31 | 32 | def set_via(h) 33 | h << ['Via', "MouseHole/#{VERSION}"] 34 | end 35 | 36 | def proxy_uri(req, res) 37 | @config[:ProxyURI] 38 | end 39 | 40 | def output(page, response) 41 | clength = nil 42 | response.status = page.status 43 | page.headers.each do |k, v| 44 | if k =~ /^CONTENT-LENGTH$/i 45 | clength = v.to_i 46 | else 47 | [*v].each do |vi| 48 | response.header[k] = vi 49 | end 50 | end 51 | end 52 | 53 | body = page.body 54 | response.send_status(body.length) 55 | response.send_header 56 | response.write(body) 57 | end 58 | 59 | def page_headers(request) 60 | reqh, env = {}, {} 61 | request.params.each do |k, v| 62 | k = k.downcase.gsub('_','-') 63 | env[k] = v 64 | if k =~ /^http-/ and k != "http-version" 65 | reqh[$'] = v 66 | end 67 | end 68 | return reqh, env 69 | end 70 | 71 | def page_prep(request) 72 | reqh, env = page_headers(request) 73 | uri = "http:#{env['path-info']}" 74 | if uri.match(/[#{Regexp::quote('{}|\^[]`')}]/) 75 | uri = URI.escape(uri) 76 | end 77 | return URI(uri), reqh, env 78 | end 79 | 80 | end 81 | 82 | end 83 | -------------------------------------------------------------------------------- /lib/mouseHole/proxyhandler.rb: -------------------------------------------------------------------------------- 1 | require 'mouseHole/page' 2 | 3 | module MouseHole 4 | 5 | class MountHandler < Mongrel::HttpHandler 6 | include HandlerMixin 7 | include LoggerMixin 8 | 9 | def initialize(block) 10 | @block = block 11 | end 12 | 13 | def process(request, response) 14 | reqh, env = page_headers(request) 15 | header = [] 16 | choose_header(reqh, header) 17 | page = Page.new(URI(env['request-uri']), 404, header) 18 | @block.call(page) 19 | output(page, response) 20 | end 21 | 22 | end 23 | 24 | class ProxyHandler < Mongrel::HttpHandler 25 | include HandlerMixin 26 | include LoggerMixin 27 | 28 | def initialize(central) 29 | @central = central 30 | end 31 | 32 | def process(request, response) 33 | start = Time.now 34 | uri, reqh, env = page_prep(request) 35 | 36 | if uri.path =~ %r!/([\w\-]{32})/! 37 | token, trail = $1, $' 38 | app = @central.find_app :token => token 39 | if app 40 | hdlr = app.find_handler :is => :mount, :on => :all, :name => trail 41 | return hdlr.process(request, response) 42 | end 43 | end 44 | 45 | header = [] 46 | choose_header(reqh, header) 47 | set_via(header) 48 | 49 | http = Net::HTTP.new(env['server-name'], env['server-port'], @central.proxy_host, @central.proxy_port) 50 | http.open_timeout = 10 51 | http.read_timeout = 20 52 | reqm = Net::HTTP.const_get(env['request-method'].capitalize) 53 | debug "-> connecting to #{uri}", :since => start 54 | resin = http.request(reqm.new(uri.request_uri, header), reqm::REQUEST_HAS_BODY ? request.body : nil) do |resin| 55 | header = [] 56 | debug " > opened #{uri}", :since => start 57 | choose_header(resin.to_hash, header) 58 | set_via(header) 59 | 60 | page = Page.new(uri, resin.code, header) 61 | if page.converter and !DOMAINS.include?(env['server-name']) and @central.rewrite(page, resin) 62 | info "*> rewriting #{page.location}", :since => start 63 | output(page, response) 64 | else 65 | debug " > streaming #{page.location}", :since => start 66 | response.status = resin.code.to_i 67 | header.each { |k, v| response.header[k] = v } 68 | response.send_plain_status 69 | response.send_header 70 | resin.read_body do |chunk| 71 | response.write(chunk) 72 | end 73 | end 74 | end 75 | debug "-> finished #{uri}", :since => start 76 | resin 77 | end 78 | 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /lib/mouseHole/controllers.rb: -------------------------------------------------------------------------------- 1 | module MouseHole::Controllers 2 | 3 | class RIndex < R '/' 4 | def make app, b 5 | paths = {'SCRIPT_NAME' => File.join(R(RIndex), app.mount_on)} 6 | controller = b.new(nil, @env.merge(paths), @method) 7 | controller.instance_variable_set("@app", app) 8 | controller.service 9 | [app, b, controller] 10 | end 11 | def get 12 | @doorblocks = 13 | Block.find(:all, :include => :app).map do |b| 14 | app = MouseHole::CENTRAL.find_app(b.app.script) 15 | make(app, app.doorblock_get(b.title)) rescue nil 16 | end.compact 17 | @allblocks = 18 | MouseHole::CENTRAL.doorblocks.map do |app, b| 19 | make app, b 20 | end 21 | doorway :index 22 | end 23 | end 24 | 25 | class RAbout < R '/about' 26 | def get 27 | doorway :about 28 | end 29 | end 30 | 31 | class RApps < R '/apps' 32 | def get 33 | @apps = MouseHole::CENTRAL.app_list.sort_by { |app| app.title } 34 | doorway :apps 35 | end 36 | end 37 | 38 | class RData < R '/data' 39 | def get 40 | doorway :data 41 | end 42 | end 43 | 44 | class RApp < R '/app/(.+)' 45 | def get(name) 46 | @app = MouseHole::CENTRAL.find_app name 47 | if @app 48 | doorway :app 49 | else 50 | r(404, 'Not Found') 51 | end 52 | end 53 | end 54 | 55 | class RInstaller < R '/install' 56 | def get 57 | if @input.url 58 | @url = @input.url 59 | URI.parse(@input.url).open do |f| 60 | @body = f.read 61 | end 62 | doorway :installer 63 | end 64 | end 65 | def post 66 | app = MouseHole::CENTRAL.save_app @input.url, @input.script 67 | redirect RApp, app.path 68 | end 69 | end 70 | 71 | class RBlocks < R '/blocks' 72 | def post 73 | Block.delete_all 74 | [*@input.userpool].each_with_index do |b, i| 75 | is_valid, appk, doork = *b.match(/=(\w+)::MouseHole::(\w+)$/) 76 | raise ArgumentError unless is_valid 77 | klass = MouseHole::CENTRAL.find_app :klass => appk 78 | app = MouseHole::Models::App.find_by_script klass.path 79 | block = Block.create :app_id => app.id, :title => doork, :position => i 80 | end.inspect 81 | end 82 | end 83 | 84 | class AppsRss < R '/apps.rss' 85 | def get 86 | @apps = MouseHole::CENTRAL.app_list.sort_by { |app| app.title } 87 | server_rss 88 | end 89 | end 90 | 91 | class MountsRss < R '/mounts.rss' 92 | def get 93 | @apps = MouseHole::CENTRAL.app_list.sort_by { |app| app.title } 94 | server_rss :mounts 95 | end 96 | end 97 | 98 | class Static < R '/static/(css|js|icons|images)/(.+)' 99 | MIME_TYPES = {'.css' => 'text/css', '.js' => 'text/javascript', '.png' => 'image/png'} 100 | def get(dir, path) 101 | @headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] || "text/plain" 102 | @headers['X-Sendfile'] = File.join(File.expand_path('../../../static', __FILE__), dir, path) 103 | end 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /lib/mouseHole/page.rb: -------------------------------------------------------------------------------- 1 | module MouseHole 2 | 3 | class PageHeaders < Array 4 | def []( k ) 5 | self.assoc(k.to_s.downcase).to_a[1] 6 | end 7 | def []=(k, v) 8 | k = k.to_s.downcase 9 | if (tmp = self.assoc(k)) 10 | tmp[1] = v 11 | else 12 | self << [k, v] 13 | end 14 | v 15 | end 16 | end 17 | 18 | class Page 19 | Attributes = [:location, :status, :headers, :converter, :document, :input] 20 | attr_accessor *Attributes 21 | def initialize(uri, status, headers) 22 | @location = uri 23 | @status = status 24 | @input = Camping.qsp(uri.query) 25 | @headers = PageHeaders[*headers] 26 | if @headers['Content-Type'] 27 | ctype = @headers['Content-Type'].split(";") 28 | ctype = ctype.first if ctype.respond_to? :first 29 | if ctype 30 | @converter = Converters.detect_by_mime_type ctype 31 | end 32 | end 33 | end 34 | 35 | # Used for loading marshalled Page object into Sandbox 36 | def self.restore(attribs) 37 | page = Page.new(attribs[Attributes.index(:location)], nil, 38 | attribs[Attributes.index(:headers)]) 39 | 40 | Attributes.each_with_index do |attr, ndx| 41 | page.send(attr.to_s+'=', attribs[ndx]) 42 | end 43 | 44 | page 45 | end 46 | 47 | def to_a # See self.restore(attribs) 48 | arr = Attributes.map { |attr| self.send(attr) } 49 | arr[Attributes.index(:headers)] = arr[Attributes.index(:headers)].to_a 50 | arr 51 | end 52 | 53 | # MrCode's gzip decoding from WonderLand! Also reads in remainder of the body from the 54 | # stream. 55 | def decode(resin) 56 | body = '' 57 | resin.read_body do |chunk| 58 | body += chunk 59 | end 60 | 61 | case resin['content-encoding'] 62 | when 'gzip' then 63 | gzr = Zlib::GzipReader.new(StringIO.new(body)) 64 | body = gzr.read 65 | gzr.close 66 | self.headers['content-encoding'] = nil 67 | when 'deflate' then 68 | body = Zlib::Inflate.inflate(body) 69 | self.headers['content-encoding'] = nil 70 | end 71 | 72 | @document = @converter.parse(self, body) 73 | if @document 74 | true 75 | else 76 | @document = body 77 | false 78 | end 79 | end 80 | 81 | def body=(str) 82 | @document = str 83 | end 84 | 85 | def body 86 | if @converter 87 | @converter.output(document) 88 | else 89 | document.to_s 90 | end 91 | end 92 | 93 | class ElementNotFound < StandardError; end 94 | 95 | def method_missing(ele, &b) 96 | case @document 97 | when String 98 | @document.replace Markaby::Builder.new({},self,&b).to_s 99 | else 100 | node = @document.at(ele) 101 | if node 102 | node.inner_html = Markaby::Builder.new({},self,&b).to_s 103 | else 104 | raise ElementNotFound, "No `#{ele}' found on the page." 105 | end 106 | end 107 | end 108 | 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /samples/freakylike.user.rb: -------------------------------------------------------------------------------- 1 | # It's freaky! 2 | 3 | require 'open-uri' 4 | require 'sandbox' 5 | 6 | class Array; def to_h 7 | self.inject({}) { |hash, value| hash[value.first] = value.last ; hash } 8 | end; end 9 | 10 | def marshal_dump *var 11 | var.map { |v| "Marshal.load(#{Marshal.dump(v).dump})" }.join(',') 12 | end 13 | 14 | class FreakyLike < MouseHole::App 15 | title 'FreakyLike' 16 | namespace 'http://www.stanford.edu/' 17 | description %{ 18 | Extend ProxyLike to be scriptable at proxy(run)time, 19 | with the help of the freakyfreaky sandbox. 20 | 21 | For example, to view Boing Boing through the proxy, 22 | using a proxy script defined at http://127.0.0.1:3300/script, 23 | http://127.0.0.1:3704/rewrite://boingboing.net/http://127.0.0.1:3300/script 24 | } 25 | version '1.0' 26 | 27 | RewritePrefix = "rewrite:" 28 | 29 | mount RewritePrefix do |page| 30 | page.status = 200 # OK 31 | 32 | match = %r{(#{RewritePrefix}//.+)(http://.+)}.match(page.location.to_s).captures 33 | match[0].gsub!(RewritePrefix, 'http:') 34 | page_uri, script_uri = URI(match[0]), URI(match[1]) 35 | 36 | mH = "http://#{ page.headers['host'] }/" 37 | 38 | # http GET page_uri 39 | options = page.headers.to_h 40 | options.delete_if { |k,v| %w(host accept-encoding).include? k } 41 | page.document = page_uri.open(options) { |f| f.read } 42 | 43 | # http GET script_uri, and apply sandboxed script to document 44 | begin 45 | script = script_uri.open(options) { |f| puts f.inspect; f.read } 46 | rescue 47 | warn "Couldn't read #{script_uri}" 48 | end 49 | 50 | begin 51 | code = %{ 52 | $host = #{marshal_dump(mH)} 53 | page = MouseHole::Page.restore(#{marshal_dump(page.to_a)}) 54 | eval #{marshal_dump(script)} 55 | s = '' 56 | Freaky.rewrite(page).to_s.each { |line| s << line } 57 | s 58 | } # TODO: figure out why String can't be referred if returned directly 59 | page.document = Box.eval(code) 60 | rescue Sandbox::Exception => e 61 | page.document = "(Caught sandbox exception: #{e})" 62 | end 63 | 64 | base_uri = page_uri.dup 65 | base_uri.path = '/' 66 | page.document = base_href(page.document, base_uri, mH, script_uri) 67 | end 68 | 69 | def self.base_href( html, uri, mh, script ) 70 | # TODO: postfix script to outgoing URLs 71 | # rewrite_uri = uri.to_s.gsub(%r(http://), RewritePrefix+'//') 72 | 73 | rewrite_uri = uri 74 | doc = html.gsub( /(href\s*=\s*["']?)(#{ uri }|\/+)/, 75 | "\\1#{ mh }#{ rewrite_uri }") 76 | doc.sub(/ InstallerApp.new(@server)} 25 | # user-specific directories and utilities 26 | @etags, @sandbox = {}, {} 27 | @working_dir = options.working_dir 28 | @dir = options.mouse_dir 29 | FileUtils.mkdir_p( @dir ) 30 | @started = Time.now 31 | 32 | # connect to the database, get some data 33 | ActiveRecord::Base.establish_connection options.database 34 | ActiveRecord::Base.logger = options.logger 35 | MouseHole.create 36 | # load_conf 37 | 38 | # read user apps on startup 39 | @last_refresh = Time.now 40 | @min_interval = 5.seconds 41 | load_all_apps :force 42 | end 43 | 44 | def user_apps 45 | @apps.reject { |rb,| rb =~ /^\$/ } 46 | end 47 | 48 | def load_all_apps action = nil 49 | apps = self.user_apps.keys + Dir["#{ @dir }/*.rb"].map { |rb| File.basename(rb) } 50 | apps.uniq! 51 | 52 | apps.each do |rb| 53 | path = File.join(@dir, rb) 54 | unless File.exists? path 55 | @apps.delete(rb) 56 | next 57 | end 58 | unless action == :force 59 | next if @apps[rb] and File.mtime(path) <= @apps[rb].mtime 60 | end 61 | load_app rb 62 | end 63 | end 64 | 65 | def save_app url, full_script 66 | rb = File.basename(url) 67 | path = File.join(@dir, rb) 68 | open(path, 'w') do |f| 69 | f << full_script 70 | end 71 | Models::App.create(:script => rb, :uri => url) 72 | load_app rb 73 | end 74 | 75 | def load_app rb 76 | return @apps[rb] if rb =~ /^\$/ 77 | if @apps.has_key? rb 78 | @apps[rb].unload(@server) 79 | end 80 | path = File.join(@dir, rb) 81 | app = @apps[rb] = App.load(@server, rb, path) 82 | app.mtime = File.mtime(path) 83 | app 84 | end 85 | 86 | def refresh_apps 87 | return if Time.now - @last_refresh < @min_interval 88 | load_all_apps 89 | end 90 | 91 | def find_rewrites page 92 | refresh_apps 93 | @apps.values.find_all do |app| 94 | app.rewrites? page 95 | end 96 | end 97 | 98 | def rewrite(page, resin) 99 | apps = find_rewrites(page) 100 | return false if apps.empty? 101 | 102 | if page.decode(resin) 103 | apps.each do |app| 104 | app.do_rewrite(page) 105 | end 106 | end 107 | true 108 | end 109 | 110 | def app_list 111 | refresh_apps 112 | self.user_apps.values 113 | end 114 | 115 | def find_app crit 116 | case crit 117 | when String 118 | self.user_apps[crit] 119 | when Hash 120 | (self.user_apps.detect { |name, app| 121 | crit.all? { |k,v| app.send(k) == v } 122 | } || []).last 123 | end 124 | end 125 | 126 | def doorblocks 127 | app_list.inject([]) do |ary, app| 128 | app.doorblock_classes.each do |k| 129 | ary << [app, k] 130 | end 131 | ary 132 | end 133 | end 134 | 135 | end 136 | 137 | end 138 | -------------------------------------------------------------------------------- /test/test_parser.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'rubygems' 5 | require 'hpricot' 6 | require 'load_files' 7 | 8 | #class TestParser < Test::Unit::TestCase 9 | # def setup 10 | # @basic = Hpricot.parse(TestFiles::BASIC) 11 | # # @boingboing = HTree.parse(TestFiles::BOINGBOING) 12 | # end 13 | 14 | # def test_set_attr 15 | # @basic.search('//p').set('class', 'para') 16 | # assert_equal '', @basic.search('//p').map { |x| x.attributes } 17 | # end 18 | 19 | # def test_get_element_by_id 20 | # assert_equal 'link1', @basic.get_element_by_id('link1').get_attribute('id').to_s 21 | # assert_equal 'link1', @basic.get_element_by_id('body1').get_element_by_id('link1').get_attribute('id').to_s 22 | # end 23 | 24 | # def test_get_element_by_tag_name 25 | # assert_equal 'link1', @basic.get_elements_by_tag_name('a')[0].get_attribute('id').to_s 26 | # assert_equal 'link1', @basic.get_elements_by_tag_name('body')[0].get_element_by_id('link1').get_attribute('id').to_s 27 | # end 28 | 29 | # def test_scan_basic 30 | # assert_equal 'link1', @basic./('#link1').first.get_attribute('id').to_s 31 | # assert_equal 'link1', @basic./('p a').first.get_attribute('id').to_s 32 | # assert_equal 'link1', (@basic/:p/:a).first.get_attribute('id').to_s 33 | # assert_equal 'link1', @basic.search('p').search('a').first.get_attribute('id').to_s 34 | # assert_equal 'link2', (@basic/'p').filter('.ohmy').search('a').first.get_attribute('id').to_s 35 | # assert_equal (@basic/'p')[2], (@basic/'p').filter(':nth(2)')[0] 36 | # assert_equal 4, (@basic/'p').filter('*').length 37 | # assert_equal 4, (@basic/'p').filter('* *').length 38 | # eles = (@basic/'p').filter('.ohmy') 39 | # assert_equal 1, eles.length 40 | # assert_equal 'ohmy', eles.first.get_attribute('class').to_s 41 | # assert_equal 3, (@basic/'p:not(.ohmy)').length 42 | # assert_equal 3, (@basic/'p').not('.ohmy').length 43 | # assert_equal 3, (@basic/'p').not(eles.first).length 44 | # assert_equal 2, (@basic/'p').filter('[@class]').length 45 | # assert_equal 'last final', (@basic/'p[@class~="final"]').first.get_attribute('class').to_s 46 | # assert_equal 1, (@basic/'p').filter('[@class~="final"]').length 47 | # assert_equal 2, (@basic/'p > a').length 48 | # assert_equal 1, (@basic/'p.ohmy > a').length 49 | # assert_equal 2, (@basic/'p / a').length 50 | # assert_equal 2, (@basic/'link ~ link').length 51 | # assert_equal 3, (@basic/'title ~ link').length 52 | # end 53 | 54 | # def test_scan_boingboing 55 | # assert_equal 60, (@boingboing/'p.posted').length 56 | # assert_equal 1, @boingboing.search("//a[@name='027906']").length 57 | # end 58 | 59 | # def test_abs_xpath 60 | # assert_equal 60, @boingboing.search("/html/body//p[@class='posted']").length 61 | # assert_equal 60, @boingboing.search("/*/body//p[@class='posted']").length 62 | # divs = @boingboing.search("//script/../div") 63 | # assert_equal 2, divs.length 64 | # assert_equal 1, divs.search('a').length 65 | # assert_equal 16, @boingboing.search('//div').search('p/a/img').length 66 | # imgs = @boingboing.search('//div/p/a/img') 67 | # assert_equal 16, imgs.length 68 | # assert imgs.all? { |x| x.qualified_name == 'img' } 69 | # end 70 | 71 | # def test_predicates 72 | # assert_equal 1, @boingboing.search('//input[@checked]').length 73 | # assert_equal 2, @boingboing.search('//link[@rel="alternate"]').length 74 | # p_imgs = @boingboing.search('//div/p[/a/img]') 75 | # assert_equal 16, p_imgs.length 76 | # assert p_imgs.all? { |x| x.qualified_name == 'p' } 77 | # p_imgs = @boingboing.search('//div/p[a/img]') 78 | # assert_equal 21, p_imgs.length 79 | # assert p_imgs.all? { |x| x.qualified_name == 'p' } 80 | # end 81 | 82 | # def test_alt_predicates 83 | # assert_equal 2, @boingboing.search('//table/tr:last').length 84 | # assert_equal "

The third paragraph", 85 | # @basic.search('p:eq(2)').html 86 | # assert_equal 'last final', @basic.search('//p:last-of-type').first.get_attribute('class').to_s 87 | # end 88 | 89 | # def test_many_paths 90 | # assert_equal 23, @boingboing.search('//div/p[a/img]|//link[@rel="alternate"]').length 91 | # assert_equal 62, @boingboing.search('p.posted, link[@rel="alternate"]').length 92 | # end 93 | # end 94 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ,. 4 | ;::: 5 | , _ .___mouseHole_;:::_web_proxy___. __, . 6 | 7 | 8 | ... rewrite web pages 9 | ... run little apps 10 | ... crossite ajax 11 | ... persistence 12 | ... all that! 13 | 14 | = about mouseHole = 15 | 16 | MouseHole is a personal web proxy written in Ruby designed to be simple to 17 | script. Scripts can rewrite the web as you view it, altering content and 18 | behavior as you browse. Basically, it's an alternative to Greasemonkey, 19 | which does similar things from inside the Firefox web browser. 20 | 21 | Believe it or not, though, MouseHole does a lot more than that. 22 | Here's a taste of what you'll be seeing: 23 | 24 | 25 | = running mouseHole = 26 | 27 | MouseHole can either intrude completely upon your browsing experience or you 28 | can keep it off in the outskirts, for whenever you've got a second to duck into 29 | that little crack in the wall. You can run it on one machine and use it 30 | wherever, allowing whomever you want to poke their head in. 31 | 32 | The underlying ideas are always the same, though. 33 | 34 | * [1] Start MouseHole. 35 | * [2] Visit http://127.0.0.1:3704/ to view your settings. 36 | * [3] Set MouseHole as your web proxy. 37 | * [4] Install user scripts. 38 | * [5] Or, write your own. 39 | 40 | 41 | == starting mouseHole == 42 | 43 | To start using mouseHole, simply run the script. On Windows, click on 44 | mouseHole.exe. On other operating systems, run `ruby bin/mouseHole' from inside the 45 | unpacked mouseHole directory. A text window should popup showing that 46 | mouseHole is started. 47 | 48 | You can pass in the hostname and port number you'd like to run MouseHole on. 49 | 50 | * mouseHole 203.203.203.203 to run on publicly accessible IP 203.203.203.203. 51 | * mouseHole 127.0.0.1 5300 to run on a different port on localhost. 52 | 53 | If your Web access itself is through a proxy, set the HTTP_PROXY environment 54 | variable to the IP:PORT of that proxy. 55 | 56 | 57 | == the doorway == 58 | 59 | Visit the hostname and port you choose above in your browser. Normally, this 60 | will be http://127.0.0.1:3704/. The doorway will appear. A page with a bit 61 | of mouseHole-related graffitis. (Once you get the proxy setup, you'll be able 62 | to use http://mh/ or http://mouse.hole/ instead of the IP:PORT.) 63 | 64 | The doorway lets you configure the scripts you have installed. There's not 65 | much to it. You can turn scripts on and off. Or you can tell it what sites it 66 | can or can't control. The doorway will also let you know if a script has been 67 | loaded or if it has errors. 68 | 69 | 70 | == setting mouseHole as proxy == 71 | 72 | This is the moment of decision. Will you use mouseHole for your whole browser 73 | experience? Or will you use it only occassionally? Or would you like to be 74 | able to turn it on and off at will? 75 | 76 | 77 | === using mouseHole exclusively === 78 | 79 | If you're using Firefox, open the Preferences window. Select the General tab. 80 | Click the Connection Settings button. Activate the Manual Proxy Configuration 81 | settings. Fill in MouseHole's hostname and port. (Again, the default is 82 | 127.0.0.1 and port 3704.) 83 | 84 | If you're on OS X and want to use it to proxy all Safari traffic, go to the 85 | Network preference pane (in System Preferences), select Web Proxy, enter 86 | 127.0.0.1 as address and 3704 as port. 87 | 88 | 89 | === speeding up with lighttpd or apache2 === 90 | 91 | If you want to go robust, you can run MouseHole through LightTPD/FastCGI (a 92 | free web server) or Apache2/mod_ruby (also free). The LightTPD seems to work 93 | better for most people, so (if you're on Linux or OSX) give it a try. 94 | 95 | You'll need LightTPD, FastCGI, and FCGI-Ruby installed. Once all are installed, 96 | fire it all up with: 97 | 98 | mouseHole -s lighttpd 99 | 100 | You can pass in all the typical commandline options as well, if you like. 101 | 102 | mouseHole -s lighttpd --no-tidy 127.0.0.1 3704 103 | 104 | Logs for script errors are stored in ~/.mouseHole/log. 105 | 106 | If you'd like to run Apache2, be sure you have Apache2 and mod_ruby installed. 107 | Then, get it started with: 108 | 109 | mouseHole -s apache2 110 | 111 | To stop Apache: 112 | 113 | apachectl -f ~/.mouseHole/temp/apache/httpd.conf -k stop 114 | 115 | === occasional mouseHole === 116 | 117 | Firefox has a few useful extensions for managing proxies. 118 | 119 | * [http://addons.mozilla.org/extensions/moreinfo.php?id=648 ProxyButton] which 120 | adds a button to your toolbar. The button turns proxying on and off. 121 | 122 | * [http://addons.mozilla.org/extensions/moreinfo.php?id=125 SwitchProxy] adds a 123 | toolbar, a context menu and/or a status bar menu for switching between several 124 | proxies. The status bar menu is especially handy as it is small, but displays 125 | the name of your current proxy, which you can right-click on to change. 126 | 127 | 128 | === using proxyLike === 129 | 130 | ProxyLike is a mouseHole user script which lets you pass URLs into mouseHole 131 | for rewriting on a case-by-case. To install ProxyLike, you'll need to use 132 | mouseHole as your proxy for a bit. Once you've got mouseHole running as your 133 | proxy (described above), visit 134 | http://www.whytheluckystiff.net/mouseHole/proxylike.user.rb. 135 | mouseHole will guide you through the installation. 136 | 137 | You can now turn off your proxy settings in your browser. Pass URLs into 138 | ProxyLike by prefixing the URL with your mouseHole IP:PORT. 139 | 140 | * http://127.0.0.1:3704/http:/google.com will run Google through mouseHole. 141 | * http://127.0.0.1:3704/http:/hoodwink.d/onslaught will view Hoodwink'd 142 | Onslaught through mouseHole. 143 | 144 | You can also install scripts through ProxyLike. If you have it installed, you 145 | can upgrade ProxyLike with: 146 | 147 | http://127.0.0.1:3704/http:/www.whytheluckystiff.net/mouseHole/proxylike.user.rb. 148 | 149 | 150 | == install user scripts == 151 | 152 | To install a script, simply visit an existing script and MouseHole will 153 | auto-detect this and prompt you for installation. 154 | 155 | At this time, there is no way to install a script sitting on your hard drive 156 | (because file:// URLs don't go through proxies) without throwing it in 157 | .mouseHole/userScripts and restarting MouseHole or uploading the script to a 158 | remote server. (1.2 and up detects new scripts in .mouseHole/userScripts and 159 | refreshes them as you edit it.) 160 | 161 | A list of known MouseHole scripts is available at UserScripts. 162 | 163 | == Making Your Own Scripts == 164 | 165 | For help in writing your own script, see 166 | http://mousehole.rubyforge.org/wiki/wiki.pl?Editing_MouseHole_Scripts. 167 | 168 | For debugging purposes, you can visit http://localhost:3704/mouseHole/database 169 | to view a database dump of MouseHole. 170 | 171 | -------------------------------------------------------------------------------- /static/css/doorway.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | background-color: #ECFADE; 4 | font: normal 11pt verdana, arial, sans-serif; 5 | margin: 0; padding: 0; 6 | } 7 | 8 | h1, h2, h3, h4, h5, h6 { 9 | font-family: georgia, serif; 10 | font-weight: normal; 11 | text-align: center; 12 | margin: 0; 13 | } 14 | 15 | h1 span { 16 | color: #999; 17 | } 18 | 19 | a img { 20 | border: none; 21 | } 22 | 23 | /* overall background and images */ 24 | #mousehole { 25 | background: white url(/static/images/doorway-tile.png); 26 | width: 638px; 27 | margin: 10px auto; 28 | } 29 | #page { 30 | background: url(/static/images/doorway-jam.png) no-repeat bottom left; 31 | padding: 0px 5px 0px 5px; 32 | } 33 | #footer { 34 | padding: 0px 5px 20px 5px; 35 | } 36 | 37 | /* appearance of doorblocks */ 38 | .block { 39 | display: block; 40 | margin: 4px 0; padding: 0; 41 | background: #eef; 42 | } 43 | 44 | ol.doorblocks .block { 45 | background: white; 46 | } 47 | 48 | ol.doorblocks .block .inside, 49 | ol.doorblocks .block .actions { 50 | display: block; 51 | } 52 | 53 | .block .title { 54 | margin: 1px; 55 | padding: 4px; 56 | border: solid 1px #ccc; 57 | cursor: move; 58 | } 59 | 60 | .block .actions { 61 | float: right; 62 | font-size: 10px; 63 | display: none; 64 | } 65 | 66 | .block .inside { 67 | border-top: solid 2px #eee7ee; 68 | font-size: .6em; 69 | margin: 1px; 70 | padding: 5px 20px; 71 | display: none; 72 | } 73 | 74 | .block .inside h1, 75 | .block .inside h2, 76 | .block .inside h3, 77 | .block .inside h4, 78 | .block .inside h5, 79 | .block .inside h6 { 80 | font-family: verdana, arial, sans-serif; 81 | font-weight: bold; 82 | text-align: left; 83 | margin: 8px 0; 84 | } 85 | 86 | .block p { 87 | margin: 6px 0; 88 | font-size: 13px; 89 | } 90 | 91 | .block .title h1 { 92 | display: inline; 93 | font-size: 19px; 94 | color: #353; 95 | text-align: left; 96 | margin-right: 6px; 97 | } 98 | 99 | .block .title h2 { 100 | display: inline; 101 | font-size: 13px; 102 | color: #797; 103 | text-align: left; 104 | } 105 | 106 | li.stub { 107 | padding: 10px; 108 | } 109 | 110 | #mouseHole .block .inside a { 111 | color: #03c; 112 | padding: 2px; 113 | } 114 | 115 | #mouseHole .pool { 116 | padding: 8px; 117 | height: 22px; 118 | background-color: #353; 119 | color: white; 120 | } 121 | 122 | #mouseHole .pool .block { 123 | margin: 0; padding: 0; 124 | margin-top: -5px; 125 | } 126 | 127 | #mouseHole .pool ol li { 128 | float: left; 129 | margin: 0 4px; 130 | padding: 0; 131 | } 132 | 133 | #mouseHole .pool ol li.stub { 134 | float: none; 135 | } 136 | 137 | #mouseHole .pool ol .title { 138 | color: white; 139 | } 140 | 141 | #mouseHole .pool ol .title h1 { 142 | font-size: 12px; 143 | } 144 | 145 | #mouseHole .pool ol .title h2 { 146 | font-size: 10px; 147 | } 148 | 149 | #mouseHole .pool ol .block .inside, 150 | #mouseHole .pool ol .block .actions { 151 | display: none; 152 | } 153 | 154 | /* styling of doorway controls */ 155 | #mousehole .control { 156 | font-size: 11px; 157 | list-style: none; 158 | padding: 8px; 159 | margin: 0px 15px; 160 | border: solid 1px #e9a; 161 | background-color: #fff8f2; 162 | } 163 | 164 | #mousehole .control .doorway { 165 | background: url(/static/icons/door.png) no-repeat; 166 | } 167 | 168 | #mousehole .control .apps { 169 | background: url(/static/icons/ruby.png) no-repeat; 170 | } 171 | 172 | #mousehole .control .data { 173 | background: url(/static/icons/database.png) no-repeat; 174 | } 175 | 176 | #mousehole .control .help { 177 | background: url(/static/icons/lightbulb.png) no-repeat top right; 178 | float: right; 179 | margin-top: -2px; 180 | } 181 | 182 | #mousehole a { 183 | color: #c30; 184 | padding: 2px; 185 | } 186 | 187 | #mousehole .control a { 188 | text-decoration: none; 189 | padding: 0px 6px 0px 20px; 190 | font-weight: bold; 191 | } 192 | 193 | #mousehole a:hover { 194 | color: #6a3; 195 | } 196 | 197 | #mousehole .help a { 198 | padding: 0px 20px 0px 6px; 199 | } 200 | 201 | #mousehole .control li { 202 | display: inline; 203 | margin: 2px; 204 | padding: 2px; 205 | /* border-right: solid 1px #ebc; */ 206 | } 207 | 208 | #mousehole .control li input { 209 | border: solid 1px black; 210 | } 211 | 212 | #mousehole .main { 213 | padding: 10px 20px; 214 | } 215 | 216 | #mousehole ul.apps { 217 | list-style: none; 218 | margin: 0; padding: 0; 219 | } 220 | 221 | #mousehole ul.apps li { 222 | padding: 0 10px 0 20px; 223 | margin: 6px 0px; 224 | width: 44%; 225 | float: left; 226 | } 227 | 228 | #mousehole .apps h2 { 229 | float: left; 230 | font: normal 19px verdana, arial, sans-serif; 231 | text-align: left; 232 | padding: 2px; 233 | } 234 | 235 | #mousehole .app-broken { 236 | background: url(../icons/broken.png) 0px 4px no-repeat; 237 | } 238 | 239 | #mousehole .app-ruby { 240 | background: url(../icons/ruby.png) 0px 4px no-repeat; 241 | } 242 | 243 | #mousehole .app-ruby_gear { 244 | background: url(../icons/ruby_gear.png) 0px 4px no-repeat; 245 | } 246 | 247 | #mousehole .apps h2 a { 248 | color: #08E; 249 | } 250 | 251 | #mousehole .apps h2.broken a { 252 | color: black; 253 | background-color: #eee; 254 | } 255 | 256 | #mousehole .apps h2 a:hover { 257 | background-color: #F30; 258 | color: white; 259 | text-decoration: none; 260 | } 261 | 262 | #mousehole .apps .mount { 263 | float: left; 264 | font-size: 9px; 265 | padding: 0 4px 4px 4px; 266 | margin-left: 4px; 267 | border-left: solid 1px #ccc; 268 | } 269 | 270 | #mousehole .apps .mount a { 271 | font-size: 14px; 272 | } 273 | 274 | #mousehole .apps .description { 275 | clear: both; 276 | font-size: 11px; 277 | color: #999; 278 | padding: 2px; 279 | } 280 | 281 | #mousehole ol.doorblocks { 282 | list-style: none; 283 | margin: 0; padding: 0; 284 | } 285 | 286 | #mousehole #footer { 287 | clear: both; 288 | color: #555; 289 | font-size: 10px; 290 | margin: 6px 6px; 291 | text-align: center; 292 | padding-top: 6px; 293 | border-top: solid 1px #eec; 294 | } 295 | 296 | #mousehole #footer img { 297 | margin: -6px 2px -6px 9px; 298 | } 299 | 300 | /* app editor */ 301 | #mousehole .config { 302 | width: 44%; 303 | float: left; 304 | } 305 | 306 | #mousehole div.rules { 307 | width: 54%; 308 | float: left; 309 | margin: 4px; 310 | } 311 | 312 | #mousehole div.rules h2 { 313 | font-size: 14px; 314 | color: green; 315 | font-weight: bold; 316 | } 317 | 318 | #mousehole div.rules select { 319 | width: 100%; 320 | } 321 | 322 | #mousehole div.blocks { 323 | clear: both; 324 | font-size: .6em; 325 | background-color: #eee; 326 | color: #666; 327 | padding: 4px; 328 | } 329 | 330 | #mousehole #app .submits { 331 | text-align: center; 332 | } 333 | 334 | #mousehole #app .submits input { 335 | margin: 3px; 336 | } 337 | 338 | #mousehole #app .description { 339 | color: #666; 340 | clear: both; 341 | font-size: 14px; 342 | padding: 6px; 343 | } 344 | 345 | #mousehole #app .config ul { 346 | list-style: none; 347 | } 348 | 349 | #mousehole #app .exception { 350 | background-color: #f5f5f5; 351 | color: #777; 352 | padding: 9px; 353 | } 354 | 355 | #mousehole #app .exception h2 { 356 | text-align: left; 357 | color: #555; 358 | } 359 | 360 | #mousehole #app .exception h3 { 361 | font-size: 14px; 362 | text-align: left; 363 | } 364 | 365 | #mousehole #app .exception ul { 366 | font-size: 12px; 367 | list-style: none; 368 | margin: 0; 369 | } 370 | -------------------------------------------------------------------------------- /lib/mouseHole/hacks/acts_as_list.rb: -------------------------------------------------------------------------------- 1 | require 'activerecord' 2 | unless ActiveRecord::Base.respond_to? :acts_as_list 3 | # this code is entirely cribbed from 4 | # http://dev.rubyonrails.org/browser/plugins/acts_as_list 5 | # to add the "missing" acts_as_list functionality to activerecord 6 | # in the (entirely likely) case that the user is running activerecord 2 or 7 | # greater 8 | module ActiveRecord 9 | module Acts 10 | module List 11 | def self.included(base) 12 | base.extend(ClassMethods) 13 | end 14 | 15 | module ClassMethods 16 | 17 | def acts_as_list(options = {}) 18 | configuration = { :column => "position", :scope => "1 = 1" } 19 | configuration.update(options) if options.is_a?(Hash) 20 | 21 | configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ 22 | 23 | if configuration[:scope].is_a?(Symbol) 24 | scope_condition_method = %( 25 | def scope_condition 26 | if #{configuration[:scope].to_s}.nil? 27 | "#{configuration[:scope].to_s} IS NULL" 28 | else 29 | "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" 30 | end 31 | end 32 | ) 33 | else 34 | scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" 35 | end 36 | 37 | class_eval <<-EOV 38 | include ActiveRecord::Acts::List::InstanceMethods 39 | 40 | def acts_as_list_class 41 | ::#{self.name} 42 | end 43 | 44 | def position_column 45 | '#{configuration[:column]}' 46 | end 47 | 48 | #{scope_condition_method} 49 | 50 | before_destroy :remove_from_list 51 | before_create :add_to_list_bottom 52 | EOV 53 | end 54 | end 55 | 56 | 57 | module InstanceMethods 58 | 59 | def insert_at(position = 1) 60 | insert_at_position(position) 61 | end 62 | 63 | 64 | def move_lower 65 | return unless lower_item 66 | 67 | acts_as_list_class.transaction do 68 | lower_item.decrement_position 69 | increment_position 70 | end 71 | end 72 | 73 | def move_higher 74 | return unless higher_item 75 | 76 | acts_as_list_class.transaction do 77 | higher_item.increment_position 78 | decrement_position 79 | end 80 | end 81 | 82 | def move_to_bottom 83 | return unless in_list? 84 | acts_as_list_class.transaction do 85 | decrement_positions_on_lower_items 86 | assume_bottom_position 87 | end 88 | end 89 | 90 | def move_to_top 91 | return unless in_list? 92 | acts_as_list_class.transaction do 93 | increment_positions_on_higher_items 94 | assume_top_position 95 | end 96 | end 97 | 98 | 99 | def remove_from_list 100 | if in_list? 101 | decrement_positions_on_lower_items 102 | update_attribute position_column, nil 103 | end 104 | end 105 | 106 | 107 | def increment_position 108 | return unless in_list? 109 | update_attribute position_column, self.send(position_column).to_i + 1 110 | end 111 | 112 | 113 | def decrement_position 114 | return unless in_list? 115 | update_attribute position_column, self.send(position_column).to_i - 1 116 | end 117 | 118 | 119 | def first? 120 | return false unless in_list? 121 | self.send(position_column) == 1 122 | end 123 | 124 | 125 | def last? 126 | return false unless in_list? 127 | self.send(position_column) == bottom_position_in_list 128 | end 129 | 130 | 131 | def higher_item 132 | return nil unless in_list? 133 | acts_as_list_class.find(:first, :conditions => 134 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" 135 | ) 136 | end 137 | 138 | 139 | def lower_item 140 | return nil unless in_list? 141 | acts_as_list_class.find(:first, :conditions => 142 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" 143 | ) 144 | end 145 | 146 | 147 | def in_list? 148 | !send(position_column).nil? 149 | end 150 | 151 | private 152 | def add_to_list_top 153 | increment_positions_on_all_items 154 | end 155 | 156 | def add_to_list_bottom 157 | self[position_column] = bottom_position_in_list.to_i + 1 158 | end 159 | 160 | 161 | def scope_condition() "1" end 162 | 163 | def bottom_position_in_list(except = nil) 164 | item = bottom_item(except) 165 | item ? item.send(position_column) : 0 166 | end 167 | 168 | def bottom_item(except = nil) 169 | conditions = scope_condition 170 | conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except 171 | acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC") 172 | end 173 | 174 | def assume_bottom_position 175 | update_attribute(position_column, bottom_position_in_list(self).to_i + 1) 176 | end 177 | 178 | 179 | def assume_top_position 180 | update_attribute(position_column, 1) 181 | end 182 | 183 | 184 | def decrement_positions_on_higher_items(position) 185 | acts_as_list_class.update_all( 186 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}" 187 | ) 188 | end 189 | 190 | 191 | def decrement_positions_on_lower_items 192 | return unless in_list? 193 | acts_as_list_class.update_all( 194 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}" 195 | ) 196 | end 197 | 198 | 199 | def increment_positions_on_higher_items 200 | return unless in_list? 201 | acts_as_list_class.update_all( 202 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}" 203 | ) 204 | end 205 | 206 | 207 | def increment_positions_on_lower_items(position) 208 | acts_as_list_class.update_all( 209 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" 210 | ) 211 | end 212 | 213 | def increment_positions_on_all_items 214 | acts_as_list_class.update_all( 215 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" 216 | ) 217 | end 218 | 219 | def insert_at_position(position) 220 | remove_from_list 221 | increment_positions_on_lower_items(position) 222 | self.update_attribute(position_column, position) 223 | end 224 | end 225 | end 226 | end 227 | end 228 | ActiveRecord::Base.send :include, ActiveRecord::Acts::List 229 | end 230 | 231 | 232 | -------------------------------------------------------------------------------- /bin/mouseHole: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift File.expand_path("../../lib", __FILE__) 3 | begin require 'sandbox'; rescue LoadError; end 4 | require 'optparse' 5 | require 'ostruct' 6 | require 'rubygems' 7 | require 'mouseHole' 8 | 9 | options = Camping::H[ 10 | 'tidy' => false, 'server' => 'mongrel', 11 | 'daemon' => false, 'working_dir' => Dir.pwd, 12 | 'server_log' => '-', 'log_level' => Logger::WARN 13 | ] 14 | 15 | # locate ~/.mouseHole 16 | homes = [] 17 | homes << [ENV['HOME'], File.join( ENV['HOME'], '.mouseHole' )] if ENV['HOME'] 18 | homes << [ENV['APPDATA'], File.join( ENV['APPDATA'], 'MouseHole' )] if ENV['APPDATA'] 19 | homes.each do |home_top, home_dir| 20 | next unless home_top 21 | if File.exists? home_top 22 | FileUtils.mkdir_p( home_dir ) 23 | conf = File.join( home_dir, 'options.yaml' ) 24 | if File.exists? conf 25 | YAML.load_file( conf ).each { |k,v| options.method("#{k}=").call(v) } 26 | end 27 | options.mouse_dir = home_dir 28 | break 29 | end 30 | end 31 | 32 | opts = OptionParser.new do |opts| 33 | opts.banner = "Usage: mouseHole [options] [ip or hostname] [port]" 34 | 35 | opts.separator "" 36 | opts.separator "Specific options:" 37 | 38 | opts.on("-d", "--directory DIRECTORY", 39 | "MouseHole directory (defaults to #{options.mouse_dir || 'None'})") do |d| 40 | options.mouse_dir = d 41 | end 42 | 43 | opts.on("-s", "--server SERVER_APP", 44 | "Web server to launch: mongrel, lighttpd or apache2 (default is mongrel)") do |s| 45 | options.server = s 46 | end 47 | 48 | opts.on("-D", "--[no-]daemon", "Daemon mode") do |d| 49 | options.daemon = d 50 | end 51 | 52 | opts.on("-t", "--[no-]tidy", "Use Tidy?") do |t| 53 | options.tidy = t 54 | end 55 | 56 | opts.on("-v", "--verbose", "Run verbosely") do |v| 57 | options.log_level = Logger::INFO 58 | end 59 | 60 | opts.on("--log filename", "Log to a file (defaults to '-' which is STDOUT)") do |l| 61 | options.server_log = l 62 | end 63 | 64 | opts.on("--debug", "Run with debugging data (proxy details, SQL queries)") do |v| 65 | options.log_level = Logger::DEBUG 66 | end 67 | 68 | opts.separator "" 69 | opts.separator "Common options:" 70 | 71 | # No argument, shows at tail. This will print an options summary. 72 | # Try it and see! 73 | opts.on_tail("-h", "--help", "Show this message") do 74 | puts opts 75 | exit 76 | end 77 | 78 | # Another typical switch to print the version. 79 | opts.on_tail("--version", "Show version") do 80 | puts MouseHole::VERSION 81 | exit 82 | end 83 | end 84 | 85 | opts.parse! ARGV 86 | options.host = ARGV[0] || "0.0.0.0" 87 | options.port = ARGV[1] || 3704 88 | 89 | proxy_uri = nil 90 | if env_http_proxy = ENV["HTTP_PROXY"] 91 | proxy_uri = URI.parse(env_http_proxy) 92 | options.proxy_host = proxy_uri.host 93 | options.proxy_port = proxy_uri.port 94 | end 95 | options.app_dir = File.expand_path('../..', __FILE__) 96 | if defined? RUBYSCRIPT2EXE_APPEXE 97 | options.app_dir = File.dirname( RUBYSCRIPT2EXE_APPEXE ) 98 | end 99 | options.lib_dir = File.join( options.app_dir, 'lib' ) 100 | options.share_dir = File.join( options.app_dir, 'share' ) 101 | options.auto_marshal = %{Marshal.load( #{ Marshal.dump( options ).dump } )} 102 | options.database ||= {:adapter => 'sqlite3', :database => File.join( options.mouse_dir, '+DATA' )} 103 | 104 | options.logger = 105 | case options.server_log 106 | when "-" 107 | Logger.new STDOUT 108 | else 109 | Logger.new options.server_log 110 | end 111 | options.logger.level = options.log_level 112 | 113 | case options.server 114 | when "mongrel" 115 | require 'mongrel' 116 | require 'mongrel/camping' 117 | class RedirectHandler < Mongrel::HttpHandler 118 | def initialize(path) 119 | @response = "HTTP/1.1 302 Found\r\nLocation: #{path}\r\nConnection: close\r\n\r\n" 120 | end 121 | def process(request, response) 122 | response.socket.write(@response) 123 | end 124 | end 125 | config = Mongrel::Configurator.new :host => options.host do 126 | daemonize :cwd => options.working_dir, :log_file => options.server_log if options.daemon 127 | listener :port => options.port do 128 | MouseHole::CENTRAL = MouseHole::Central.new(@listener, options) 129 | uri 'http:', :handler => MouseHole::ProxyHandler.new(MouseHole::CENTRAL) 130 | uri '/', :handler => RedirectHandler.new("/doorway") 131 | uri '/doorway', :handler => Mongrel::Camping::CampingHandler.new(MouseHole) 132 | uri '/static', :handler => Mongrel::DirHandler.new(File.join(options.app_dir, 'static')) 133 | trap('INT') { stop } 134 | run 135 | end 136 | end 137 | puts "** MouseHole running on #{options.host}:#{options.port}" 138 | config.join 139 | 140 | # THESE SERVERS NEED TO BE REWRITTEN, RETESTED 141 | # when "lighttpd" 142 | # require 'erb' 143 | # File.makedirs( File.join( options.temp_dir, 'lighttpd' ) ) 144 | # lighttpd_conf = File.join( options.temp_dir, 'lighttpd', 'lighttpd.conf' ) 145 | # File.open( lighttpd_conf, 'w' ) do |f| 146 | # f << ERB.new( File.read( options.share_dir + '/lighttpd/lighttpd.conf' ) ).result 147 | # end 148 | # dispatch_cgi = File.join( options.temp_dir, 'lighttpd', 'dispatch.fcgi' ) 149 | # File.open( dispatch_cgi, 'w' ) do |f| 150 | # f << ERB.new( File.read( options.share_dir + '/lighttpd/dispatch.fcgi' ) ).result 151 | # end 152 | # File.chmod( 0755, dispatch_cgi ) 153 | # lighttpd_path = `which lighttpd 2>/dev/null; whereis lighttpd`. 154 | # scan( %r!(?:^|\s)/\S+/lighttpd(?:$|\s)! ).detect { |ctl| ctl.strip! 155 | # `#{ctl} -v` =~ %r!lighttpd-1\.! } 156 | # abort( "** No lighttpd found, make sure it's in your PATH?" ) unless lighttpd_path 157 | # `#{ lighttpd_path } #{ options.daemon ? '' : '-D' } -f #{ lighttpd_conf }` 158 | # when "apache2" 159 | # require 'erb' 160 | # a2_dir = File.join( options.share_dir, 'apache2' ) 161 | # a2_temp = File.join( options.temp_dir, 'apache2' ) 162 | # a2_path = 163 | # `which apache2ctl 2>/dev/null; whereis apache2ctl; 164 | # which apachectl 2>/dev/null; whereis apachectl`. 165 | # scan( %r!\s/\S+/apache2?ctl\s! ).detect { |ctl| ctl.strip! 166 | # `#{ctl} -v` =~ %r!Apache/2\.! } 167 | # abort( "** No apachectl or apache2ctl found, make sure it's in your PATH?" ) unless a2_path 168 | # a2 = `#{ a2_path } -V`.scan( /-D\s*(\w+)\s*=\s*"(.+)"/ ). 169 | # inject({}) { |hsh,(k,v)| hsh[k] = v; hsh } 170 | # a2_conf = File.expand_path( a2['SERVER_CONFIG_FILE'], a2['HTTPD_ROOT'] ) 171 | # File.foreach( a2_conf ) do |line| 172 | # case line 173 | # when /^\s*ServerRoot\s+("(.+?)"|(\S+))/ 174 | # options.server_root = ($2 || $1).strip 175 | # when /^\s*LoadModule\s+(\w+)\s+(\S+)/ 176 | # mod_name, mod_path = $1, $2 177 | # options.modules ||= Hash.new do |hsh,k| 178 | # `find #{ options.server_root } -name "mod_#{ k }.*"`. 179 | # gsub( /^#{ options.server_root }\/?/, '' ) 180 | # end 181 | # options.modules[mod_name.gsub( /_module$/, '' )] = mod_path 182 | # end 183 | # end 184 | # 185 | # files = {} 186 | # Dir["#{a2_dir}/**/*"].each do |from_file| 187 | # next if File.directory? from_file 188 | # to_file = from_file.gsub( a2_dir, a2_temp ). 189 | # gsub( /\/dot\./, '/.' ) 190 | # unless File.exists? File.dirname( to_file ) 191 | # File.makedirs( File.dirname( to_file ) ) 192 | # end 193 | # File.open( to_file, 'w' ) do |f| 194 | # f << ERB.new( File.read( from_file ) ).result 195 | # end 196 | # files[to_file.gsub("#{a2_temp}/", '')] = to_file 197 | # end 198 | # File.chmod( 0755, files['htdocs/index.rbx'] ) 199 | # `#{ a2_path } -f #{ files['httpd.conf'] }` 200 | else 201 | abort "** Server `#{ options.server }' not supported." 202 | end 203 | -------------------------------------------------------------------------------- /lib/mouseHole/app.rb: -------------------------------------------------------------------------------- 1 | module MouseHole 2 | 3 | class App 4 | 5 | include REXML 6 | include Converters 7 | include LoggerMixin 8 | 9 | METADATA = [:title, :namespace, :description, :version, :rules, :handlers, :accept] 10 | 11 | attr_reader :token 12 | attr_accessor :document, :path, :mount_on, :mtime, :active, 13 | :registered_uris, :klass, :model, :app_style, 14 | *METADATA 15 | 16 | def basic_setup 17 | @accept ||= HTML 18 | @token ||= MouseHole.token 19 | end 20 | 21 | def initialize server, klass_name = nil, model = nil, rb = nil 22 | klass_name ||= self.class.name 23 | self.model = model 24 | self.title = klass_name 25 | METADATA.each do |f| 26 | self.send("#{f}=", self.class.send("default_#{f}")) 27 | end 28 | self.klass = klass_name 29 | self.path = rb 30 | if self.handlers 31 | self.handlers.each do |h_is, h_name, h_blk| 32 | next unless h_is == :mount 33 | server.unregister "/#{h_name}" 34 | server.register "/#{h_name}", h_blk 35 | end 36 | end 37 | basic_setup 38 | end 39 | 40 | def install_uri; @model.uri if @model end 41 | 42 | def icon; "ruby_gear" end 43 | 44 | def broken?; false end 45 | 46 | def summary 47 | s = description[/.{10,100}[.?!\)]+|^.{1,100}(\b|$)/m, 0] 48 | s += "..." if s =~ /\w$/ and s.length < description.length 49 | s 50 | end 51 | 52 | def rewrites? page 53 | if @rules 54 | return false unless @accept == page.converter 55 | rule = @rules.detect { |rule| rule.match_uri(page.location) } 56 | return false unless rule and rule.action == :rewrite 57 | true 58 | end 59 | end 60 | 61 | def do_rewrite(page) 62 | @document = page.document 63 | begin 64 | rewrite(page) 65 | rescue Exception => e 66 | error "[#{self.title}] #{e.class}: #{e.message}" 67 | end 68 | end 69 | 70 | def find_handler(opts = {}) 71 | if handlers 72 | handlers.each do |h_is, h_name, h_blk, h_opts| 73 | next unless h_is == opts[:is] if opts[:is] 74 | next unless h_name == opts[:name] if opts[:name] 75 | next unless h_opts[:on] == opts[:on] if opts[:on] 76 | return h_blk 77 | end 78 | end 79 | end 80 | 81 | def mount_path(path) 82 | p = path.to_s.gsub(%r!^/!, '') 83 | hdlr = find_handler :is => :mount, :name => p, :on => :all 84 | if hdlr 85 | "/#@token/#{p}" 86 | else 87 | raise MountError, "no `#{path}' mount found on app #{self.class.name}." 88 | end 89 | end 90 | 91 | def doorblocks 92 | if @klass 93 | k = Object.const_get(@klass) 94 | if k.const_defined? :MouseHole 95 | k::MouseHole.constants 96 | end 97 | end || [] 98 | end 99 | 100 | def doorblock_get(b) 101 | Object.const_get(@klass)::MouseHole.const_get(b) rescue nil 102 | end 103 | 104 | def doorblock_classes 105 | doorblocks.map do |b| 106 | doorblock_get(b) 107 | end 108 | end 109 | 110 | def self.load(server, rb, path) 111 | title = File.basename(rb)[/^(\w+)/,1] 112 | 113 | # Load the application at the toplevel. We want everything to work as if it was loaded from 114 | # the commandline by Ruby. 115 | klass, klass_name, source = nil, nil, File.read(path) 116 | begin 117 | source.gsub!('__FILE__', "'" + path + "'") 118 | eval(source, TOPLEVEL_BINDING) 119 | klass_name = Object.constants.grep(/^#{title}$/i)[0] 120 | klass = Object.const_get(klass_name) 121 | klass.create if klass.respond_to? :create 122 | rescue Exception => e 123 | warn "Warning, found broken app: '#{title}'" 124 | return BrokenApp.new(source[/\b#{title}\b/i, 0], rb, e) 125 | end 126 | 127 | return unless klass and klass_name 128 | 129 | # Hook up the general configuration from the object. 130 | model = Models::App.find_by_script(rb) || Models::App.create(:script => rb) 131 | if klass.respond_to? :run 132 | server.register "/#{title}", Mongrel::Camping::CampingHandler.new(klass) 133 | end 134 | 135 | if klass < App 136 | klass.new(server, klass_name, model, rb) 137 | else 138 | if klass.const_defined? :MouseHole 139 | klass::MouseHole.constants.each do |c| 140 | dk = klass::MouseHole.const_get(c) 141 | if dk.is_a? Class 142 | dk.class_eval do 143 | def self.title 144 | name[/::([^:]+?)$/, 1] 145 | end 146 | include C, Base, Models 147 | end 148 | end 149 | end 150 | end 151 | klass.meta_eval do 152 | alias_method :__run__, :run 153 | define_method :run do |*a| 154 | x = __run__(*a) 155 | x_is_html = true unless x.respond_to? :headers and x.headers['Content-Type'] != 'text/html' 156 | if (x.respond_to? :body and not x.body.nil?) and x_is_html 157 | begin 158 | doc = Hpricot(x.body) 159 | (doc/:head).append("") 160 | (doc/:body).prepend("

MouseHole // You are using #{klass_name} (edit)
") 161 | x.body = doc.to_original_html 162 | rescue => e 163 | warn "Hpricot couldn't parse #{x.body.class} at #{x.env['REQUEST_PATH']} (#{e}) -- #{__FILE__}" if x.respond_to? :env 164 | end 165 | end 166 | x 167 | end 168 | end 169 | CampingApp.new(title, klass_name, model, rb) 170 | end 171 | end 172 | 173 | def unload(server) 174 | if @mount_on 175 | server.unregister @mount_on 176 | end 177 | if handlers 178 | handlers.each do |h_is, h_name, h_blk| 179 | next unless h_is == :mount 180 | server.unregister "/#{h_name}" 181 | end 182 | end 183 | if @klass 184 | Object.send :remove_const, @klass 185 | end 186 | end 187 | 188 | class << self 189 | METADATA.each do |f| 190 | attr_accessor "default_#{f}" 191 | define_method(f) do |str| 192 | instance_variable_set("@default_#{f}", str) 193 | end 194 | end 195 | 196 | def mount(path, opts = {}, &b) 197 | (@default_handlers ||= []) << [:mount, path.to_s.gsub(%r!^/!, ''), MouseHole::MountHandler.new(b), opts] 198 | end 199 | 200 | [:url].each do |rt| 201 | define_method(rt) do |*expr| 202 | r = const_get(constants.grep(/^#{rt}$/i)[0]).new(*expr) 203 | (@default_rules ||= []) << r 204 | r 205 | end 206 | end 207 | 208 | end 209 | 210 | class Rule 211 | attr_accessor :expr, :action 212 | def initialize(*expr) 213 | @expr = expr 214 | end 215 | def -@; @action = :ignore end 216 | def +@; @action = :rewrite end 217 | end 218 | 219 | class URL < Rule 220 | def initialize(expr) 221 | @expr = expr 222 | @action = :rewrite 223 | end 224 | def match_uri(uri) 225 | if @expr.respond_to? :source 226 | uri.to_s.match @expr 227 | elsif @expr.respond_to? :to_str 228 | uri.to_s.match /^#{ Regexp.quote(@expr).gsub( "\\*", '.*' ) }$/ 229 | elsif @expr.respond_to? :keys 230 | @expr.detect do |k, v| 231 | uri.__send__(k) == v 232 | end 233 | end 234 | end 235 | def to_s 236 | "#{@action} #{@expr}" 237 | end 238 | end 239 | 240 | end 241 | 242 | class CampingApp < App 243 | def initialize title, klass_name, model, rb 244 | self.mount_on = "/#{title}" 245 | self.title = klass_name 246 | self.klass = klass_name 247 | self.model = model 248 | self.path = rb 249 | basic_setup 250 | end 251 | def icon; "ruby" end 252 | end 253 | 254 | class BrokenApp < App 255 | def initialize(title, path, e) 256 | self.title = title 257 | self.path = path 258 | self.error = e 259 | basic_setup 260 | end 261 | attr_accessor :error 262 | def icon; "broken" end 263 | def broken?; true end 264 | end 265 | 266 | class MountError < Exception; end 267 | 268 | end 269 | -------------------------------------------------------------------------------- /lib/mouseHole/views.rb: -------------------------------------------------------------------------------- 1 | require 'redcloth' 2 | 3 | module MouseHole::Views 4 | 5 | def doorway(meth) 6 | html do 7 | head do 8 | title "MouseHole" 9 | link :href => R(AppsRss), :title => 'Apps RSS', 10 | :rel => 'alternate', :type => 'application/rss+xml' 11 | link :href => R(MountsRss), :title => 'Apps (Mounts Only) RSS', 12 | :rel => 'alternate', :type => 'application/rss+xml' 13 | script :type => "text/javascript", :src => R(Static, 'js', 'jquery.js') 14 | script :type => "text/javascript", :src => R(Static, 'js', 'interface.js') 15 | script :type => "text/javascript", :src => R(Static, 'js', 'mouseHole.js') 16 | style "@import '#{R(Static, 'css', 'doorway.css')}';", :type => 'text/css' 17 | end 18 | body do 19 | div.mousehole! do 20 | img :src => R(Static, 'images', 'doorway.png') 21 | ul.control do 22 | li.help { a "about", :href => R(RAbout) } 23 | li.doorway { a "doorway", :href => R(RIndex) } 24 | li.apps { a "apps", :href => R(RApps) } 25 | li.data { a "data", :href => R(RData) } 26 | end 27 | div.page! do 28 | div.send("#{meth}!") do 29 | send(meth) 30 | end 31 | div.footer! do 32 | strong "feeds: " 33 | a :href => R(AppsRss) do 34 | img :src => R(Static, 'icons', 'feed.png') 35 | text "apps" 36 | end 37 | a :href => R(MountsRss) do 38 | img :src => R(Static, 'icons', 'feed.png') 39 | text "mounts" 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | 48 | def block_list blocks 49 | blocks.each do |app, klass, c| 50 | li.blocksort :id => "#{MouseHole.token}=#{klass.name}" do 51 | div.block.send(klass.title) do 52 | div.title do 53 | div.actions do 54 | a.del "hide", :href => "javascript://" 55 | end 56 | t = c.title if c.respond_to? :title 57 | h1(t || klass.title) 58 | if app.mount_on 59 | h2 do 60 | text "from " 61 | a app.title, :href => "..#{app.mount_on}" 62 | end 63 | else 64 | h2 "from #{app.title}" 65 | end 66 | end 67 | div.inside do 68 | self << c.body.to_s 69 | end 70 | end 71 | end 72 | end 73 | end 74 | 75 | def index 76 | div.main do 77 | if @allblocks.any? 78 | ol.doorblocks.userpool! do 79 | block_list @doorblocks 80 | end 81 | div.pool do 82 | ol.doorblocks.fullpool! do 83 | li "Blocks:" 84 | block_list @allblocks 85 | end 86 | end 87 | else 88 | p "None of your installed apps have any doorblocks." 89 | end 90 | end 91 | end 92 | 93 | def installer 94 | div.main do 95 | h1 "Install" 96 | h3 @url 97 | form :method => 'POST', :action => R(RInstaller) do 98 | input :name => 'url', :type => 'hidden', :value => @url 99 | textarea @body, :cols => 68, :rows => 20, :name => 'script' 100 | div.submits do 101 | input :type => 'submit', :value => 'Install' 102 | end 103 | end 104 | end 105 | end 106 | 107 | def about 108 | div.main do 109 | red %{ 110 | h1. About %MouseHole 2% 111 | 112 | It's true. This is the *second* MouseHole. The first only lasted a few months. Very experimental. 113 | Meaning: slow and sloppy. You now hold the much improved *MouseHole 2*, a personal-sized web server. 114 | 115 | About your installation: You are running %MouseHole #{MouseHole::VERSION}% on top of 116 | %Ruby #{::RUBY_VERSION}%, built on #{::RUBY_RELEASE_DATE} for the #{::RUBY_PLATFORM} platform. 117 | 118 | h2. Credits 119 | 120 | MouseHole was first conceived by the readers of RedHanded, a blog exploring the fringes of the Ruby 121 | programming language. First it was called Hoodlum, then it was called Wonderland. We traded 122 | code back and forth and got it hacked together. 123 | During the end of "August 2005":http://redhanded.hobix.com/2005/08/. 124 | 125 | Right now, MouseHole is under the care of "why the lucky stiff":http://whytheluckystiff.net/. 126 | It's a very small operation and you are welcome to come hop aboard! 127 | 128 | The icons included with MouseHole are from the "Silk":http://www.famfamfam.com/lab/icons/silk/ 129 | set by a nice British guy named Mark James. He even had Ruby kinds. Thankyyouu!! 130 | } 131 | end 132 | end 133 | 134 | def apps 135 | div.main do 136 | h1 { "#{span('Your Installed')} Apps" } 137 | ul.apps do 138 | @apps.each do |app| 139 | li :class => "app-#{app.icon}" do 140 | if app.broken? 141 | h2.broken { a app.title, :href => R(RApp, app.path) } 142 | div.description "This app is broken." 143 | else 144 | div.title do 145 | h2 { a app.title, :href => R(RApp, app.path) } 146 | if app.mount_on 147 | div.mount do 148 | "mounted on:" + br + 149 | a(app.mount_on, :href => "..#{app.mount_on}") 150 | end 151 | end 152 | end 153 | blocks = app.doorblocks 154 | unless blocks.blank? 155 | div.blocks { 156 | strong "Blocks:" 157 | blocks.each do |b| 158 | text " #{b}" 159 | end 160 | } 161 | end 162 | if app.description 163 | div.description app.summary 164 | end 165 | end 166 | end 167 | end 168 | end 169 | end 170 | end 171 | 172 | def app 173 | div.main do 174 | h1 { "#{span(@app.title)} Setup" } 175 | case @app 176 | when MouseHole::BrokenApp 177 | div.description do 178 | "This app is broken. The exception causing the problem is listed below:" 179 | end 180 | div.exception do 181 | h2 "#{@app.error.class}" 182 | self << h3(@app.error.message).gsub(/\n/, '
') 183 | ul.backtrace do 184 | @app.error.backtrace.each do |bt| 185 | li "from #{bt}" 186 | end 187 | end 188 | end 189 | when MouseHole::CampingApp 190 | when MouseHole::App 191 | div.config do 192 | div.description @app.description if @app.description 193 | ul do 194 | li do 195 | input :type => 'checkbox' 196 | span "Enabled" 197 | end 198 | end 199 | end 200 | div.rules do 201 | h2 "Rules" 202 | select :size => 5 do 203 | [*@app.rules].each do |rule| 204 | option rule 205 | end 206 | end 207 | div.submits do 208 | input :type => 'button', :value => 'Add...' 209 | input :type => 'button', :value => 'Remove' 210 | end 211 | end 212 | end 213 | if @app.install_uri 214 | p "Installed from #{@app.install_uri}." 215 | else 216 | p "Originally installed by hand." 217 | end 218 | end 219 | end 220 | 221 | def data 222 | div.main do 223 | h1 { 'Data ' + span('Viewer') } 224 | p %{Welcome to MouseHole.} 225 | end 226 | end 227 | 228 | def red str 229 | RedCloth.new(str.gsub(/^ +/, '')).to_html 230 | end 231 | 232 | # RSS feed of all user scripts. Two good uses of this: your browser can build a bookmark list of all 233 | # your user scripts from the feed (or) if you share a proxy, you can be informed concerning the user scripts 234 | # people are installing. 235 | def server_rss(only = nil) 236 | @headers['Content-Type'] = 'text/xml' 237 | rss( @body = "" ) do |c| 238 | uri = URL('/') 239 | uri.scheme = "http" 240 | 241 | c.title "MouseHole User Scripts: #{ uri.host }" 242 | c.link "#{ uri }" 243 | c.description "A list of user script installed for the MouseHole proxy at #{ uri }" 244 | 245 | c.item do |item| 246 | item.title "MouseHole" 247 | item.link "#{ uri }" 248 | item.guid "#{ uri }" 249 | item.dc :creator, "MouseHole" 250 | item.dc :date, @started 251 | item.description "The primary MouseHole configuration page." 252 | end 253 | 254 | @apps.each do |app| 255 | uri = URL(RApp, app.path) 256 | uri.scheme = "http" 257 | 258 | unless only == :mounts 259 | c.item do |item| 260 | item.title "#{ app.title }: Configuration" 261 | item.link "#{ uri }" 262 | item.guid "#{ uri }" 263 | item.dc :creator, "MouseHole" 264 | item.dc :date, app.mtime 265 | item.description app.description 266 | end 267 | end 268 | if app.mount_on 269 | c.item do |item| 270 | uri.path = app.mount_on 271 | item.title "#{ app.title }: Mounted at #{ app.mount_on }" 272 | item.link "#{ uri }" 273 | item.guid "#{ uri }" 274 | end 275 | end 276 | end 277 | end 278 | end 279 | 280 | end 281 | -------------------------------------------------------------------------------- /static/js/interface.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Interface elements for jQuery - http://interface.eyecon.ro 3 | * 4 | * Copyright (c) 2006 Stefan Petre 5 | * Dual licensed under the MIT (MIT-LICENSE.txt) 6 | * and GPL (GPL-LICENSE.txt) licenses. 7 | */ 8 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('5.L={4R:A(e,s){B l=0;B t=0;B 2N=0;B 2H=0;B w=5.F(e,\'2c\');B h=5.F(e,\'25\');B Z=e.3s;B W=e.3g;2J(e.3n){l+=e.2F+(e.1o?I(e.1o.3t)||0:0);t+=e.2S+(e.1o?I(e.1o.3i)||0:0);9(s){2N+=e.1I.1V||0;2H+=e.1I.1O||0}e=e.3n}l+=e.2F+(e.1o?I(e.1o.3t)||0:0);t+=e.2S+(e.1o?I(e.1o.3i)||0:0);2H=t-2H;2N=l-2N;E{x:l,y:t,64:2N,5g:2H,w:w,h:h,Z:Z,W:W}},2z:A(e){B x=0;B y=0;B 41=g;B 11=e.O;9(5(e).F(\'T\')==\'12\'){3p=11.2i;3l=11.1s;11.2i=\'3O\';11.T=\'1Z\';11.1s=\'3r\';41=G}B z=e;2J(z){x+=z.2F+(z.1o&&!5.1P.3w?I(z.1o.3t)||0:0);y+=z.2S+(z.1o&&!5.1P.3w?I(z.1o.3i)||0:0);z=z.3n}z=e;2J(z&&z.5h.4K()!=\'15\'){x-=z.1V||0;y-=z.1O||0;z=z.1I}9(41){11.T=\'12\';11.1s=3l;11.2i=3p}E{x:x,y:y}},3I:A(z){B x=0,y=0;2J(z){x+=z.2F||0;y+=z.2S||0;z=z.3n}E{x:x,y:y}},1U:A(e){B w=5.F(e,\'2c\');B h=5.F(e,\'25\');B Z=0;B W=0;B 11=e.O;9(5(e).F(\'T\')!=\'12\'){Z=e.3s;W=e.3g}U{3p=11.2i;3l=11.1s;11.2i=\'3O\';11.T=\'1Z\';11.1s=\'3r\';Z=e.3s;W=e.3g;11.T=\'12\';11.1s=3l;11.2i=3p}E{w:w,h:h,Z:Z,W:W}},3K:A(z){E{Z:z.3s||0,W:z.3g||0}},4i:A(e){B h,w,2l;9(e){w=e.2w;h=e.2y}U{2l=M.1p;w=28.3Q||2Y.3Q||(2l&&2l.2w)||M.15.2w;h=28.3R||2Y.3R||(2l&&2l.2y)||M.15.2y}E{w:w,h:h}},4M:A(e){B t,l,w,h,2k,2j;9(e&&e.5l.4K()!=\'15\'){t=e.1O;l=e.1V;w=e.3B;h=e.3P;2k=0;2j=0}U{9(M.1p&&M.1p.1O){t=M.1p.1O;l=M.1p.1V;w=M.1p.3B;h=M.1p.3P}U 9(M.15){t=M.15.1O;l=M.15.1V;w=M.15.3B;h=M.15.3P}2k=2Y.3Q||M.1p.2w||M.15.2w||0;2j=2Y.3R||M.1p.2y||M.15.2y||0}E{t:t,l:l,w:w,h:h,2k:2k,2j:2j}},4S:A(e,2s){B z=5(e);B t=z.F(\'2G\')||\'\';B r=z.F(\'2P\')||\'\';B b=z.F(\'2Q\')||\'\';B l=z.F(\'2R\')||\'\';9(2s)E{t:I(t)||0,r:I(r)||0,b:I(b)||0,l:I(l)};U E{t:t,r:r,b:b,l:l}},5n:A(e,2s){B z=5(e);B t=z.F(\'5o\')||\'\';B r=z.F(\'5p\')||\'\';B b=z.F(\'61\')||\'\';B l=z.F(\'5s\')||\'\';9(2s)E{t:I(t)||0,r:I(r)||0,b:I(b)||0,l:I(l)};U E{t:t,r:r,b:b,l:l}},2X:A(e,2s){B z=5(e);B t=z.F(\'3i\')||\'\';B r=z.F(\'5t\')||\'\';B b=z.F(\'5u\')||\'\';B l=z.F(\'3t\')||\'\';9(2s)E{t:I(t)||0,r:I(r)||0,b:I(b)||0,l:I(l)||0};U E{t:t,r:r,b:b,l:l}},3L:A(2U){B x=2U.5v||(2U.5T+(M.1p.1V||M.15.1V))||0;B y=2U.5x||(2U.5z+(M.1p.1O||M.15.1O))||0;E{x:x,y:y}},3W:A(1v,3V){3V(1v);1v=1v.5B;2J(1v){5.L.3W(1v,3V);1v=1v.5D}},56:A(1v){5.L.3W(1v,A(z){1q(B 1j 1R z){9(53 z[1j]===\'A\'){z[1j]=N}}})},5F:A(z,V){B 1E=$.L.4M();B 3Z=$.L.1U(z);9(!V||V==\'2h\')$(z).F({1k:1E.t+((1n.34(1E.h,1E.2j)-1E.t-3Z.W)/2)+\'1b\'});9(!V||V==\'2n\')$(z).F({1m:1E.l+((1n.34(1E.w,1E.2k)-1E.l-3Z.Z)/2)+\'1b\'})},5G:A(z,4P){B 4O=$(\'5H[@3j*="3k"]\',z||M),3k;4O.1d(A(){3k=j.3j;j.3j=4P;j.O.3D="5I:5J.5K.5L(3j=\'"+3k+"\')"})}};[].4Q||(46.5M.4Q=A(v,n){n=(n==N)?0:n;B m=j.1f;1q(B i=n;i0){2K(2L)}5.C.2p=[];5(\'15\').1M(5.C.H.K(0))},3m:A(e,o){9(!5.k.7)E;B 1J=g;B i=0;9(e.q.z.4U()>0){1q(i=e.q.z.4U();i>0;i--){9(e.q.z.K(i-1)!=5.k.7){9(!e.2t.45){9((e.q.z.K(i-1).24.y+e.q.z.K(i-1).24.W/2)>5.k.7.6.1x){1J=e.q.z.K(i-1)}U{5O}}U{9((e.q.z.K(i-1).24.x+e.q.z.K(i-1).24.Z/2)>5.k.7.6.1B&&(e.q.z.K(i-1).24.y+e.q.z.K(i-1).24.W/2)>5.k.7.6.1x){1J=e.q.z.K(i-1)}}}}}9(1J&&5.C.2q!=1J){5.C.2q=1J;5(1J).5P(5.C.H.K(0))}U 9(!1J&&(5.C.2q!=N||5.C.H.K(0).1I!=e)){5.C.2q=N;5(e).1M(5.C.H.K(0))}5.C.H.K(0).O.T=\'1Z\'},3M:A(e){9(5.k.7==N){E}B i;e.q.z.1d(A(){j.24=5.1C(5.L.1U(j),5.L.2z(j))})},3a:A(s){B i;B h=\'\';B o={};9(s){9(5.C.1D[s]){o[s]=[];5(\'#\'+s+\' .\'+5.C.1D[s]).1d(A(){9(h.1f>0){h+=\'&\'}h+=s+\'[]=\'+5.1j(j,\'X\');o[s][o[s].1f]=5.1j(j,\'X\')})}U{1q(a 1R s){9(5.C.1D[s[a]]){o[s[a]]=[];5(\'#\'+s[a]+\' .\'+5.C.1D[s[a]]).1d(A(){9(h.1f>0){h+=\'&\'}h+=s[a]+\'[]=\'+5.1j(j,\'X\');o[s[a]][o[s[a]].1f]=5.1j(j,\'X\')})}}}}U{1q(i 1R 5.C.1D){o[i]=[];5(\'#\'+i+\' .\'+5.C.1D[i]).1d(A(){9(h.1f>0){h+=\'&\'}h+=i+\'[]=\'+5.1j(j,\'X\');o[i][o[i].1f]=5.1j(j,\'X\')})}}E{3f:h,o:o}},52:A(e){9(!e.5Q){E}E j.1d(A(){9(!j.2t||!5(e).3H(\'.\'+j.2t.1z))5(e).3b(j.2t.1z);5(e).3C(j.2t.6)})},2d:A(o){9(o.1z&&5.L&&5.k&&5.D){9(!5.C.H){5(\'15\',M).1M(\'<3d X="4W">&4V;\');5.C.H=5(\'#4W\');5.C.H.K(0).O.T=\'12\'}j.4j({1z:o.1z,31:o.31?o.31:g,33:o.33?o.33:g,1S:o.1S?o.1S:g,2A:A(43,Q){5.C.H.4X(43);9(Q>0){5(43).5R(Q)}},26:o.26||o.4D,2f:o.2f||o.4G,44:G,18:o.18||o.5S,Q:o.Q?o.Q:g,1e:o.1e?G:g,20:o.20?o.20:\'3S\'});E j.1d(A(){6={1T:o.1T?G:g,4Z:50,16:o.16?40(o.16):g,21:o.1S?o.1S:g,Q:o.Q?o.Q:g,1h:G,1e:o.1e?G:g,1Q:o.1Q?o.1Q:N,P:o.P?o.P:N,1u:o.1u&&o.1u.1g==1N?o.1u:g,1l:o.1l&&o.1l.1g==1N?o.1l:g,V:/2h|2n/.4T(o.V)?o.V:g,1Y:o.1Y?I(o.1Y)||0:g,Y:o.Y?o.Y:g};5(\'.\'+o.1z,j).3C(6);j.5W=G;j.2t={1z:o.1z,1T:o.1T?G:g,4Z:50,16:o.16?40(o.16):g,21:o.1S?o.1S:g,Q:o.Q?o.Q:g,1h:G,1e:o.1e?G:g,1Q:o.1Q?o.1Q:N,P:o.P?o.P:N,45:o.45?G:g,6:6}})}}};5.4c.1C({5X:5.C.2d,51:5.C.52});5.5Z=5.C.3a;5.k={H:N,7:N,3v:A(){E j.1d(A(){9(j.2V){j.6.1y.3E(\'4h\',5.k.3y);j.6=N;j.2V=g}})},3y:A(e){9(5.k.7!=N){5.k.2Z(e);E g}B 8=j.2W;5(M).3x(\'4m\',5.k.3J).3x(\'4n\',5.k.2Z);8.6.13=5.L.3L(e);8.6.1i=8.6.13;8.6.36=g;8.6.60=j!=j.2W;5.k.7=8;9(8.6.22&&j!=j.2W){3Y=5.L.2z(8.1I);47=5.L.1U(8);49={x:I(5.F(8,\'1m\'))||0,y:I(5.F(8,\'1k\'))||0};S=8.6.1i.x-3Y.x-47.Z/2-49.x;R=8.6.1i.y-3Y.y-47.W/2-49.y;5.3N.62(8,[S,R])}E g},4C:A(e){8=5.k.7;8.6.36=G;30=8.O;8.6.2e=5.F(8,\'T\');8.6.2o=5.F(8,\'1s\');9(!8.6.4a)8.6.4a=8.6.2o;8.6.10={x:I(5.F(8,\'1m\'))||0,y:I(5.F(8,\'1k\'))||0};8.6.37=0;8.6.39=0;9(5.1P.3u){4b=5.L.2X(8,G);8.6.37=4b.l||0;8.6.39=4b.t||0}8.6.J=5.1C(5.L.2z(8),5.L.1U(8));9(8.6.2o!=\'54\'&&8.6.2o!=\'3r\'){30.1s=\'54\'}5.k.H.3A();1c=8.55(G);5.L.56(1c);5(1c).F({T:\'1Z\',1m:\'1A\',1k:\'1A\'});1c.O.2G=\'0\';1c.O.2P=\'0\';1c.O.2Q=\'0\';1c.O.2R=\'0\';5.k.H.1M(1c);9(8.6.1u)8.6.1u.1w(8,[1c]);19=5.k.H.K(0).O;9(8.6.42){19.2c=\'57\';19.25=\'57\'}U{19.25=8.6.J.W+\'1b\';19.2c=8.6.J.Z+\'1b\'}19.T=\'1Z\';19.2G=\'1A\';19.2P=\'1A\';19.2Q=\'1A\';19.2R=\'1A\';5.1C(8.6.J,5.L.1U(1c));9(8.6.Y){9(8.6.Y.1m){8.6.10.x+=8.6.13.x-8.6.J.x-8.6.Y.1m;8.6.J.x=8.6.13.x-8.6.Y.1m}9(8.6.Y.1k){8.6.10.y+=8.6.13.y-8.6.J.y-8.6.Y.1k;8.6.J.y=8.6.13.y-8.6.Y.1k}9(8.6.Y.4e){8.6.10.x+=8.6.13.x-8.6.J.x-8.6.J.W+8.6.Y.4e;8.6.J.x=8.6.13.x-8.6.J.Z+8.6.Y.4e}9(8.6.Y.4f){8.6.10.y+=8.6.13.y-8.6.J.y-8.6.J.W+8.6.Y.4f;8.6.J.y=8.6.13.y-8.6.J.W+8.6.Y.4f}}8.6.1B=8.6.10.x;8.6.1x=8.6.10.y;9(8.6.2I||8.6.P==\'3q\'){2T=5.L.2X(8.1I,G);8.6.J.x=8.2F+(5.1P.3u?0:5.1P.3w?-2T.l:2T.l);8.6.J.y=8.2S+(5.1P.3u?0:5.1P.3w?-2T.t:2T.t);5(8.1I).1M(5.k.H.K(0))}9(8.6.P){5.k.4g(8);8.6.1G.P=5.k.4w}9(8.6.22){5.3N.59(8)}19.1m=8.6.J.x-8.6.37+\'1b\';19.1k=8.6.J.y-8.6.39+\'1b\';19.2c=8.6.J.Z+\'1b\';19.25=8.6.J.W+\'1b\';5.k.7.6.35=g;9(8.6.2v){8.6.1G.1K=5.k.4u}9(8.6.27!=g){5.k.H.F(\'27\',8.6.27)}9(8.6.16){5.k.H.F(\'16\',8.6.16);9(28.3h){5.k.H.F(\'3D\',\'4k(16=\'+8.6.16*4l+\')\')}}9(5.D&&5.D.2D>0){5.D.4s(8)}9(8.6.1e==g){30.T=\'12\'}E g},4g:A(8){9(8.6.P.1g==4Y){9(8.6.P==\'3q\'){8.6.14=5.1C({x:0,y:0},5.L.1U(8.1I));2x=5.L.2X(8.1I,G);8.6.14.w=8.6.14.Z-2x.l-2x.r;8.6.14.h=8.6.14.W-2x.t-2x.b}U 9(8.6.P==\'M\'){3z=5.L.4i();8.6.14={x:0,y:0,w:3z.w,h:3z.h}}}U 9(8.6.P.1g==46){8.6.14={x:I(8.6.P[0])||0,y:I(8.6.P[1])||0,w:I(8.6.P[2])||0,h:I(8.6.P[3])||0}}8.6.14.S=8.6.14.x-8.6.J.x;8.6.14.R=8.6.14.y-8.6.J.y},32:A(7){9(7.6.2I||7.6.P==\'3q\'){5(\'15\',M).1M(5.k.H.K(0))}5.k.H.3A().5a().F(\'16\',1);9(28.3h){5.k.H.F(\'3D\',\'4k(16=4l)\')}},2Z:A(e){5(M).3E(\'4m\',5.k.3J).3E(\'4n\',5.k.2Z);9(5.k.7==N){E}7=5.k.7;5.k.7=N;9(7.6.36==g){E g}9(7.6.1h==G){5(7).F(\'1s\',7.6.2o)}30=7.O;9(7.22){5.k.H.F(\'4H\',\'4I\')}9(7.6.1T==g){9(7.6.Q>0){9(!7.6.V||7.6.V==\'2n\'){x=4o 5.Q(7,{4p:7.6.Q},\'1m\');x.4q(7.6.10.x,7.6.2B)}9(!7.6.V||7.6.V==\'2h\'){y=4o 5.Q(7,{4p:7.6.Q},\'1k\');y.4q(7.6.10.y,7.6.2C)}}U{9(!7.6.V||7.6.V==\'2n\')7.O.1m=7.6.2B+\'1b\';9(!7.6.V||7.6.V==\'2h\')7.O.1k=7.6.2C+\'1b\'}5.k.32(7);9(7.6.1e==g){5(7).F(\'T\',7.6.2e)}}U 9(7.6.Q>0){7.6.35=G;9(5.D&&5.D.1r&&5.C&&7.6.1h){2b=5.L.2z(5.C.H.K(0))}U{2b=g}5.k.H.5b({1m:2b?2b.x:7.6.J.x,1k:2b?2b.y:7.6.J.y},7.6.Q,A(){7.6.35=g;9(7.6.1e==g){7.O.T=7.6.2e}5.k.32(7)})}U{5.k.32(7);9(7.6.1e==g){5(7).F(\'T\',7.6.2e)}}9(5.D&&5.D.2D>0){5.D.4N(7)}9(5.C&&5.D.1r&&7.6.1h){5.C.4r(7)}9(7.6.18&&(7.6.2B!=7.6.10.x||7.6.2C!=7.6.10.y)){7.6.18.1w(7,7.6.5c||[0,0,7.6.2B,7.6.2C])}9(7.6.1l)7.6.1l.1w(7);E g},4u:A(x,y,S,R){9(S!=0)S=I((S+(j.6.2v*S/1n.4v(S))/2)/j.6.2v)*j.6.2v;9(R!=0)R=I((R+(j.6.2O*R/1n.4v(R))/2)/j.6.2O)*j.6.2O;E{S:S,R:R,x:0,y:0}},4w:A(x,y,S,R){S=1n.4x(1n.34(S,j.6.14.S),j.6.14.w+j.6.14.S-j.6.J.Z);R=1n.4x(1n.34(R,j.6.14.R),j.6.14.h+j.6.14.R-j.6.J.W);E{S:S,R:R,x:0,y:0}},3J:A(e){9(5.k.7==N||5.k.7.6.35==G){E}B 7=5.k.7;7.6.1i=5.L.3L(e);9(7.6.36==g){4A=1n.5d(1n.4z(7.6.13.x-7.6.1i.x,2)+1n.4z(7.6.13.y-7.6.1i.y,2));9(4A<7.6.1Y){E}U{5.k.4C(e)}}S=7.6.1i.x-7.6.13.x;R=7.6.1i.y-7.6.13.y;1q(i 1R 7.6.1G){1W=7.6.1G[i].1w(7,[7.6.10.x+S,7.6.10.y+R,S,R]);9(1W&&1W.1g==5f){S=i!=\'2r\'?1W.S:(1W.x-7.6.10.x);R=i!=\'2r\'?1W.R:(1W.y-7.6.10.y)}}7.6.1B=7.6.J.x+S-7.6.37;7.6.1x=7.6.J.y+R-7.6.39;9(7.6.22&&(7.6.2m||7.6.18)){5.3N.2m(7,7.6.1B,7.6.1x)}9(7.6.2u)7.6.2u.1w(7,[7.6.10.x+S,7.6.10.y+R]);9(!7.6.V||7.6.V==\'2n\'){7.6.2B=7.6.10.x+S;5.k.H.K(0).O.1m=7.6.1B+\'1b\'}9(!7.6.V||7.6.V==\'2h\'){7.6.2C=7.6.10.y+R;5.k.H.K(0).O.1k=7.6.1x+\'1b\'}9(5.D&&5.D.2D>0){5.D.3m(7,1c)}E g},2d:A(o){9(!5.k.H){5(\'15\',M).1M(\'<3d X="4E">\');5.k.H=5(\'#4E\');z=5.k.H.K(0);1L=z.O;1L.1s=\'3r\';1L.T=\'12\';1L.4H=\'4I\';1L.5i=\'12\';1L.5j=\'3O\';9(28.3h){z.3T=A(){E g};z.4B=A(){E g}}U{1L.5k=\'12\';1L.5m=\'12\'}}9(!o){o={}}E j.1d(A(){9(j.2V||!5.L)E;9(28.3h){j.3T=A(){E g};j.4B=A(){E g}}B z=j;B 1y=o.1Q?5(j).5q(o.1Q):5(j);9(5.1P.3u){1y.1d(A(){j.3T=A(){E g};j.5w=A(){E g};j.5y="5A"})}U{1y.F(\'-5C-2r-3X\',\'12\');1y.F(\'2r-3X\',\'12\');1y.F(\'-5E-2r-3X\',\'12\')}j.6={1y:1y,1T:o.1T?G:g,1e:o.1e?G:g,1h:o.1h?o.1h:g,22:o.22?o.22:g,2I:o.2I?o.2I:g,27:o.27?I(o.27)||0:g,16:o.16?40(o.16):g,Q:I(o.Q)||N,21:o.21?o.21:g,1G:{},13:{},1u:o.1u&&o.1u.1g==1N?o.1u:g,1l:o.1l&&o.1l.1g==1N?o.1l:g,18:o.18&&o.18.1g==1N?o.18:g,V:/2h|2n/.4T(o.V)?o.V:g,1Y:o.1Y?I(o.1Y)||0:0,Y:o.Y?o.Y:g,42:o.42?G:g};9(o.1G&&o.1G.1g==1N)j.6.1G.2r=o.1G;9(o.2u&&o.2u.1g==1N)j.6.2u=o.2u;9(o.P&&((o.P.1g==4Y&&(o.P==\'3q\'||o.P==\'M\'))||(o.P.1g==46&&o.P.1f==4))){j.6.P=o.P}9(o.48){j.6.48=o.48}9(o.1K){9(53 o.1K==\'63\'){j.6.2v=I(o.1K)||1;j.6.2O=I(o.1K)||1}U 9(o.1K.1f==2){j.6.2v=I(o.1K[0])||1;j.6.2O=I(o.1K[1])||1}}9(o.2m&&o.2m.1g==1N){j.6.2m=o.2m}j.2V=G;1y.1d(A(){j.2W=z});1y.3x(\'4h\',5.k.3y)})}};5.4c.1C({4d:5.k.3v,3C:5.k.2d});5.D={4L:A(1H,1F,29,2a){E 1H<=5.k.7.6.1B&&(1H+29)>=(5.k.7.6.1B+5.k.7.6.J.w)&&1F<=5.k.7.6.1x&&(1F+2a)>=(5.k.7.6.1x+5.k.7.6.J.h)?G:g},3S:A(1H,1F,29,2a){E!(1H>(5.k.7.6.1B+5.k.7.6.J.w)||(1H+29)<5.k.7.6.1B||1F>(5.k.7.6.1x+5.k.7.6.J.h)||(1F+2a)<5.k.7.6.1x)?G:g},13:A(1H,1F,29,2a){E 1H<5.k.7.6.1i.x&&(1H+29)>5.k.7.6.1i.x&&1F<5.k.7.6.1i.y&&(1F+2a)>5.k.7.6.1i.y?G:g},1r:g,1a:{},2D:0,17:{},4s:A(8){9(5.k.7==N){E}B i;5.D.1a={};38=g;1q(i 1R 5.D.17){9(5.D.17[i]!=N){u=5.D.17[i].K(0);9(5(5.k.7).3H(\'.\'+u.q.a)){9(u.q.m==g){u.q.p=5.1C(5.L.3I(u),5.L.3K(u));u.q.m=G}9(u.q.1X){5.D.17[i].3b(u.q.1X)}5.D.1a[i]=5.D.17[i];9(5.C&&u.q.s&&5.k.7.6.1h){u.q.z=5(\'.\'+u.q.a,u);8.O.T=\'12\';5.C.3M(u);8.O.T=8.6.2e;38=G}9(u.q.3e){u.q.3e.1w(5.D.17[i].K(0),[5.k.7])}}}}9(38){5.C.4t()}},58:A(){5.D.1a={};1q(i 1R 5.D.17){9(5.D.17[i]!=N){u=5.D.17[i].K(0);9(5(5.k.7).3H(\'.\'+u.q.a)){u.q.p=5.1C(5.L.3I(u),5.L.3K(u));9(u.q.1X){5.D.17[i].3b(u.q.1X)}5.D.1a[i]=5.D.17[i];9(5.C&&u.q.s&&5.k.7.6.1h){u.q.z=5(\'.\'+u.q.a,u);8.O.T=\'12\';5.C.3M(u);8.O.T=8.6.2e;38=G}}}}},3m:A(e){9(5.k.7==N){E}5.D.1r=g;B i;3U=g;4J=0;1q(i 1R 5.D.1a){u=5.D.1a[i].K(0);9(5.D.1r==g&&5.D[u.q.t](u.q.p.x,u.q.p.y,u.q.p.Z,u.q.p.W)){9(u.q.23&&u.q.h==g){5.D.1a[i].3b(u.q.23)}9(u.q.h==g&&u.q.26){3U=G}u.q.h=G;5.D.1r=u;9(5.C&&u.q.s&&5.k.7.6.1h){5.C.H.K(0).4F=u.q.4y;5.C.3m(u)}4J++}U 9(u.q.h==G){9(u.q.2f){u.q.2f.1w(u,[e,1c,u.q.Q])}9(u.q.23){5.D.1a[i].3o(u.q.23)}u.q.h=g}}9(5.C&&!5.D.1r&&5.k.7.1h){5.C.H.K(0).O.T=\'12\'}9(3U){5.D.1r.q.26.1w(5.D.1r,[e,1c])}},4N:A(e){B i;1q(i 1R 5.D.1a){u=5.D.1a[i].K(0);9(u.q.1X){5.D.1a[i].3o(u.q.1X)}9(u.q.23){5.D.1a[i].3o(u.q.23)}9(u.q.s){5.C.2p[5.C.2p.1f]=i}9(u.q.2A&&u.q.h==G){u.q.h=g;u.q.2A.1w(u,[e,u.q.Q])}u.q.m=g;u.q.h=g}5.D.1a={}},3v:A(){E j.1d(A(){9(j.3c){9(j.q.s){X=5.1j(j,\'X\');5.C.1D[X]=N;5(\'.\'+j.q.a,j).4d()}5.D.17[\'d\'+j.3F]=N;j.3c=g;j.f=N}})},2d:A(o){E j.1d(A(){9(j.3c==G||!o.1z||!5.L||!5.k){E}j.q={a:o.1z,1X:o.31||g,23:o.33||g,4y:o.1S||g,2A:o.5e||o.2A||g,26:o.26||o.4D||g,2f:o.2f||o.4G||g,3e:o.3e||g,t:o.20&&(o.20==\'4L\'||o.20==\'3S\')?o.20:\'13\',Q:o.Q?o.Q:g,m:g,h:g};9(o.44==G&&5.C){X=5.1j(j,\'X\');5.C.1D[X]=j.q.a;j.q.s=G;9(o.18){j.q.18=o.18;j.q.3G=5.C.3a(X).3f}}j.3c=G;j.3F=I(1n.5U()*5Y);5.D.17[\'d\'+j.3F]=5(j);5.D.2D++})}};5.4c.1C({5V:5.D.3v,4j:5.D.2d});5.5r=5.D.58;',62,377,'|||||jQuery|dragCfg|dragged|elm|if|||||||false|||this|iDrag||||||dropCfg||||iEL|||||el|function|var|iSort|iDrop|return|css|true|helper|parseInt|oC|get|iUtil|document|null|style|containment|fx|dy|dx|display|else|axis|hb|id|cursorAt|wb|oR|es|none|pointer|cont|body|opacity|zones|onChange|dhs|highlighted|px|clonedEl|each|ghosting|length|constructor|so|currentPointer|attr|top|onStop|left|Math|currentStyle|documentElement|for|overzone|position|shs|onStart|nodeEl|apply|ny|dhe|accept|0px|nx|extend|collected|clientScroll|zoney|onDragModifier|zonex|parentNode|cur|grid|els|append|Function|scrollTop|browser|handle|in|helperclass|revert|getSize|scrollLeft|newCoords|ac|snapDistance|block|tolerance|hpc|si|hc|pos|height|onHover|zIndex|window|zonew|zoneh|dh|width|build|oD|onOut|cs|vertically|visibility|ih|iw|de|onSlide|horizontally|oP|changed|inFrontOf|user|toInteger|sortCfg|onDrag|gx|clientWidth|contBorders|clientHeight|getPosition|onDrop|nRx|nRy|count|margins|offsetLeft|marginTop|st|insideParent|while|fnc|ts|ser|sl|gy|marginRight|marginBottom|marginLeft|offsetTop|parentBorders|event|isDraggable|dragElem|getBorder|self|dragstop|dEs|activeclass|hidehelper|hoverclass|max|prot|init|diffX|oneIsSortable|diffY|serialize|addClass|isDroppable|div|onActivate|hash|offsetHeight|ActiveXObject|borderTopWidth|src|png|oldPosition|checkhover|offsetParent|removeClass|oldVisibility|parent|absolute|offsetWidth|borderLeftWidth|msie|destroy|opera|bind|draginit|clnt|empty|scrollWidth|Draggable|filter|unbind|idsa|os|is|getPositionLite|dragmove|getSizeLite|getPointer|measure|iSlider|hidden|scrollHeight|innerWidth|innerHeight|intersect|onselectstart|applyOnHover|func|traverseDOM|select|parentPos|windowSize|parseFloat|restoreStyle|autoSize|drag|sortable|floats|Array|sliderSize|fractions|sliderPos|initialPosition|oldBorder|fn|DraggableDestroy|right|bottom|getContainment|mousedown|getClient|Droppable|alpha|100|mousemove|mouseup|new|duration|custom|check|highlight|start|snapToGrid|abs|fitToContainer|min|shc|pow|distance|ondragstart|dragstart|onhover|dragHelper|className|onout|cursor|move|hlt|toLowerCase|fit|getScroll|checkdrop|images|emptyGIF|indexOf|getPos|getMargins|test|size|nbsp|sortHelper|after|String|zindex|3000|SortableAddItem|addItem|typeof|relative|cloneNode|purgeEvents|auto|remeasure|modifyContainer|hide|animate|lastSi|sqrt|ondrop|Object|sy|tagName|listStyle|overflow|mozUserSelect|nodeName|userSelect|getPadding|paddingTop|paddingRight|find|recallDroppables|paddingLeft|borderRightWidth|borderBottomWidth|pageX|ondrag|pageY|selectable|clientY|on|firstChild|moz|nextSibling|khtml|centerEl|fixPNG|img|progid|DXImageTransform|Microsoft|AlphaImageLoader|prototype|html|break|before|childNodes|fadeIn|onchange|clientX|random|DroppableDestroy|isSortable|Sortable|10000|SortSerialize|fromHandler|paddingBottom|dragmoveBy|number|sx'.split('|'),0,{})) 9 | -------------------------------------------------------------------------------- /static/js/jquery.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery 1.1 - New Wave Javascript 3 | * 4 | * Copyright (c) 2007 John Resig (jquery.com) 5 | * Dual licensed under the MIT (MIT-LICENSE.txt) 6 | * and GPL (GPL-LICENSE.txt) licenses. 7 | * 8 | * $Date: 2007-01-14 17:37:33 -0500 (Sun, 14 Jan 2007) $ 9 | * $Rev: 1073 $ 10 | */ 11 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('k(1o 1D.6=="R"){1D.R=1D.R;u 6=l(a,c){k(1D==7)q 1v 6(a,c);a=a||11;k(6.1q(a)&&!a.1Q&&a[0]==R)q 1v 6(11)[6.C.26?"26":"2D"](a);k(1o a=="21"){u m=/^[^<]*(<.+>)[^>]*$/.2M(a);a=m?6.3f([m[1]]):6.2o(a,c)}q 7.4M(a.1g==2x&&a||(a.3R||a.G&&a!=1D&&!a.1Q&&a[0]!=R&&a[0].1Q)&&6.3G(a)||[a])};k(1o $!="R")6.31$=$;u $=6;6.C=6.8i={3R:"1.1",8j:l(){q 7.G},G:0,2g:l(1S){q 1S==R?6.3G(7):7[1S]},2p:l(a){u J=6(7);J.6i=7;q J.4M(a)},4M:l(a){7.G=0;[].1i.W(7,a);q 7},I:l(C,1t){q 6.I(7,C,1t)},4U:l(18){u 4F=-1;7.I(l(i){k(7==18)4F=i});q 4F},1x:l(1U,O,v){u 18=1U;k(1U.1g==49)k(O==R)q 6[v||"1x"](7[0],1U);H{18={};18[1U]=O}q 7.I(l(){N(u F 1y 18)6.1x(v?7.1n:7,F,6.F(7,18[F],v))})},1h:l(1U,O){q 7.1x(1U,O,"30")},2F:l(e){k(1o e=="21")q 7.3m().3h(11.8l(e));u t="";6.I(e||7,l(){6.I(7.2Q,l(){k(7.1Q!=8)t+=7.1Q!=1?7.62:6.C.2F([7])})});q t},2r:l(){u a=6.3f(1u);q 7.I(l(){u b=a[0].3T(U);7.V.3d(b,7);22(b.16)b=b.16;b.4s(7)})},3h:l(){q 7.3a(1u,U,1,l(a){7.4s(a)})},5V:l(){q 7.3a(1u,U,-1,l(a){7.3d(a,7.16)})},5e:l(){q 7.3a(1u,14,1,l(a){7.V.3d(a,7)})},5g:l(){q 7.3a(1u,14,-1,l(a){7.V.3d(a,7.2a)})},4A:l(){q 7.6i||6([])},2o:l(t){q 7.2p(6.2Z(7,l(a){q 6.2o(t,a)}))},4q:l(4z){q 7.2p(6.2Z(7,l(a){q a.3T(4z!=R?4z:U)}))},1w:l(t){q 7.2p(6.1q(t)&&6.2n(7,l(2H,4U){q t.W(2H,[4U])})||6.3v(t,7))},2f:l(t){q 7.2p(t.1g==49&&6.3v(t,7,U)||6.2n(7,l(a){k(t.1g==2x||t.3R)q 6.3u(t,a)<0;H q a!=t}))},1F:l(t){q 7.2p(6.2h(7.2g(),1o t=="21"?6(t).2g():t))},4Y:l(1l){q 1l?6.1w(1l,7).r.G>0:14},19:l(19){q 19==R?(7.G?7[0].O:1a):7.1x("O",19)},4P:l(19){q 19==R?(7.G?7[0].2z:1a):7.3m().3h(19)},3a:l(1t,1V,3D,C){u 4q=7.G>1;u a=6.3f(1t);k(3D<0)a.8n();q 7.I(l(){u 18=7;k(1V&&7.1O.1N()=="8o"&&a[0].1O.1N()=="8r")18=7.5C("28")[0]||7.4s(11.6g("28"));6.I(a,l(){C.W(18,[4q?7.3T(U):7])})})}};6.1p=6.C.1p=l(){u 1M=1u[0],a=1;k(1u.G==1){1M=7;a=0}u F;22(F=1u[a++])N(u i 1y F)1M[i]=F[i];q 1M};6.1p({8s:l(){k(6.31$)$=6.31$},1q:l(C){q C&&1o C=="l"},I:l(18,C,1t){k(18.G==R)N(u i 1y 18)C.W(18[i],1t||[i,18[i]]);H N(u i=0,5G=18.G;i<5G;i++)k(C.W(18[i],1t||[i,18[i]])===14)4o;q 18},F:l(B,O,v){k(6.1q(O))q O.3s(B);k(O.1g==3J&&v=="30")q O+"46";q O},12:{1F:l(B,c){6.I(c.3t(/\\s+/),l(i,M){k(!6.12.2T(B.12,M))B.12+=(B.12?" ":"")+M})},29:l(B,c){B.12=c?6.2n(B.12.3t(/\\s+/),l(M){q!6.12.2T(c,M)}).55(" "):""},2T:l(t,c){t=t.12||t;q t&&1v 4l("(^|\\\\s)"+c+"(\\\\s|$)").1K(t)}},40:l(e,o,f){N(u i 1y o){e.1n["1J"+i]=e.1n[i];e.1n[i]=o[i]}f.W(e,[]);N(u i 1y o)e.1n[i]=e.1n["1J"+i]},1h:l(e,p){k(p=="2e"||p=="3V"){u 1J={},3S,3e,d=["8u","8v","7s","8x"];6.I(d,l(){1J["8G"+7]=0;1J["8A"+7+"8B"]=0});6.40(e,1J,l(){k(6.1h(e,"1e")!="1X"){3S=e.8C;3e=e.8D}H{e=6(e.3T(U)).2o(":4d").5k("2V").4A().1h({4b:"1z",3U:"6k",1e:"2A",6l:"0",6m:"0"}).5d(e.V)[0];u 2P=6.1h(e.V,"3U");k(2P==""||2P=="3Z")e.V.1n.3U="6n";3S=e.6o;3e=e.6p;k(2P==""||2P=="3Z")e.V.1n.3U="3Z";e.V.38(e)}});q p=="2e"?3S:3e}q 6.30(e,p)},30:l(B,F,4Z){u J;k(F=="1d"&&6.T.1j)q 6.1x(B.1n,"1d");k(F=="4N"||F=="2O")F=6.T.1j?"3j":"2O";k(!4Z&&B.1n[F])J=B.1n[F];H k(11.3W&&11.3W.4V){k(F=="2O"||F=="3j")F="4N";F=F.1Y(/([A-Z])/g,"-$1").4T();u M=11.3W.4V(B,1a);k(M)J=M.53(F);H k(F=="1e")J="1X";H 6.40(B,{1e:"2A"},l(){u c=11.3W.4V(7,"");J=c&&c.53(F)||""})}H k(B.4R){u 54=F.1Y(/\\-(\\w)/g,l(m,c){q c.1N()});J=B.4R[F]||B.4R[54]}q J},3f:l(a){u r=[];6.I(a,l(i,1H){k(!1H)q;k(1H.1g==3J)1H=1H.6r();k(1o 1H=="21"){u s=6.2B(1H),1Z=11.6g("1Z"),2d=[];u 2r=!s.15("<1m")&&[1,"<3O>",""]||(!s.15("<8m")||!s.15("<28")||!s.15("<6u"))&&[1,"<1V>",""]||!s.15("<41")&&[2,"<1V><28>",""]||(!s.15("<6v")||!s.15("<6x"))&&[3,"<1V><28><41>",""]||[0,"",""];1Z.2z=2r[1]+s+2r[2];22(2r[0]--)1Z=1Z.16;k(6.T.1j){k(!s.15("<1V")&&s.15("<28")<0)2d=1Z.16&&1Z.16.2Q;H k(2r[1]=="<1V>"&&s.15("<28")<0)2d=1Z.2Q;N(u n=2d.G-1;n>=0;--n)k(2d[n].1O.1N()=="6y"&&!2d[n].2Q.G)2d[n].V.38(2d[n])}1H=1Z.2Q}k(1H.G===0)q;k(1H[0]==R)r.1i(1H);H r=6.2h(r,1H)});q r},1x:l(B,17,O){u 2m={"N":"8a","6B":"12","4N":6.T.1j?"3j":"2O",2O:6.T.1j?"3j":"2O",2z:"2z",12:"12",O:"O",2X:"2X",2V:"2V",6D:"6E",2Y:"2Y"};k(17=="1d"&&6.T.1j&&O!=R){B.83=1;q B.1w=B.1w.1Y(/4I\\([^\\)]*\\)/6F,"")+(O==1?"":"4I(1d="+O*67+")")}H k(17=="1d"&&6.T.1j)q B.1w?4m(B.1w.6H(/4I\\(1d=(.*)\\)/)[1])/67:1;k(17=="1d"&&6.T.3b&&O==1)O=0.6J;k(2m[17]){k(O!=R)B[2m[17]]=O;q B[2m[17]]}H k(O==R&&6.T.1j&&B.1O&&B.1O.1N()=="7X"&&(17=="6K"||17=="7V"))q B.7U(17).62;H k(B.6N){k(O!=R)B.6Q(17,O);q B.3B(17)}H{17=17.1Y(/-([a-z])/6R,l(z,b){q b.1N()});k(O!=R)B[17]=O;q B[17]}},2B:l(t){q t.1Y(/^\\s+|\\s+$/g,"")},3G:l(a){u r=[];k(a.1g!=2x)N(u i=0,2w=a.G;i<2w;i++)r.1i(a[i]);H r=a.3F(0);q r},3u:l(b,a){N(u i=0,2w=a.G;i<2w;i++)k(a[i]==b)q i;q-1},2h:l(2v,3M){u r=[].3F.3s(2v,0);N(u i=0,66=3M.G;i<66;i++)k(6.3u(3M[i],r)==-1)2v.1i(3M[i]);q 2v},2n:l(1L,C,44){k(1o C=="21")C=1v 4L("a","i","q "+C);u 1c=[];N(u i=0,2H=1L.G;i<2H;i++)k(!44&&C(1L[i],i)||44&&!C(1L[i],i))1c.1i(1L[i]);q 1c},2Z:l(1L,C){k(1o C=="21")C=1v 4L("a","q "+C);u 1c=[],r=[];N(u i=0,2H=1L.G;i<2H;i++){u 19=C(1L[i],i);k(19!==1a&&19!=R){k(19.1g!=2x)19=[19];1c=1c.6U(19)}}u r=1c.G?[1c[0]]:[];61:N(u i=1,5a=1c.G;i<5a;i++){N(u j=0;jm[3]-0",24:"m[3]-0==i",5j:"m[3]-0==i",2v:"i==0",2S:"i==r.G-1",5J:"i%2==0",5L:"i%2","24-3r":"6.24(a.V.16,m[3],\'2a\',a)==a","2v-3r":"6.24(a.V.16,1,\'2a\')==a","2S-3r":"6.24(a.V.7h,1,\'5l\')==a","7j-3r":"6.2I(a.V.16).G==1",5n:"a.16",3m:"!a.16",5o:"6.C.2F.W([a]).15(m[3])>=0",39:\'a.v!="1z"&&6.1h(a,"1e")!="1X"&&6.1h(a,"4b")!="1z"\',1z:\'a.v=="1z"||6.1h(a,"1e")=="1X"||6.1h(a,"4b")=="1z"\',7l:"!a.2X",2X:"a.2X",2V:"a.2V",2Y:"a.2Y||6.1x(a,\'2Y\')",2F:"a.v==\'2F\'",4d:"a.v==\'4d\'",5z:"a.v==\'5z\'",3Y:"a.v==\'3Y\'",5s:"a.v==\'5s\'",4O:"a.v==\'4O\'",5t:"a.v==\'5t\'",5u:"a.v==\'5u\'",4e:\'a.v=="4e"||a.1O=="7n"\',5w:"/5w|3O|7o|4e/i.1K(a.1O)"},".":"6.12.2T(a,m[2])","@":{"=":"z==m[4]","!=":"z!=m[4]","^=":"z&&!z.15(m[4])","$=":"z&&z.2R(z.G - m[4].G,m[4].G)==m[4]","*=":"z&&z.15(m[4])>=0","":"z",4k:l(m){q["",m[1],m[3],m[2],m[5]]},5H:"z=a[m[3]]||6.1x(a,m[3]);"},"[":"6.2o(m[2],a).G"},5E:[/^\\[ *(@)([a-2j-3x-]*) *([!*$^=]*) *(\'?"?)(.*?)\\4 *\\]/i,/^(\\[)\\s*(.*?(\\[.*?\\])?[^[]*?)\\s*\\]/,/^(:)([a-2j-3x-]*)\\("?\'?(.*?(\\(.*?\\))?[^(]*?)"?\'?\\)/i,/^([:.#]*)([a-2j-3x*-]*)/i],1P:[/^(\\/?\\.\\.)/,"a.V",/^(>|\\/)/,"6.2I(a.16)",/^(\\+)/,"6.24(a,2,\'2a\')",/^(~)/,l(a){u s=6.2I(a.V.16);q s.3F(0,6.3u(a,s))}],3v:l(1l,1L,2f){u 1J,M=[];22(1l&&1l!=1J){1J=1l;u f=6.1w(1l,1L,2f);1l=f.t.1Y(/^\\s*,\\s*/,"");M=2f?1L=f.r:6.2h(M,f.r)}q M},2o:l(t,1r){k(1o t!="21")q[t];k(1r&&!1r.1Q)1r=1a;1r=1r||11;k(!t.15("//")){1r=1r.4v;t=t.2R(2,t.G)}H k(!t.15("/")){1r=1r.4v;t=t.2R(1,t.G);k(t.15("/")>=1)t=t.2R(t.15("/"),t.G)}u J=[1r],2b=[],2S=1a;22(t&&2S!=t){u r=[];2S=t;t=6.2B(t).1Y(/^\\/\\//i,"");u 3w=14;u 1C=/^[\\/>]\\s*([a-2j-9*-]+)/i;u m=1C.2M(t);k(m){6.I(J,l(){N(u c=7.16;c;c=c.2a)k(c.1Q==1&&(c.1O==m[1].1N()||m[1]=="*"))r.1i(c)});J=r;t=6.2B(t.1Y(1C,""));3w=U}H{N(u i=0;i<6.1P.G;i+=2){u 1C=6.1P[i];u m=1C.2M(t);k(m){r=J=6.2Z(J,6.1q(6.1P[i+1])?6.1P[i+1]:l(a){q 3A(6.1P[i+1])});t=6.2B(t.1Y(1C,""));3w=U;4o}}}k(t&&!3w){k(!t.15(",")){k(J[0]==1r)J.4K();6.2h(2b,J);r=J=[1r];t=" "+t.2R(1,t.G)}H{u 32=/^([a-2j-3x-]+)(#)([a-2j-9\\\\*31-]*)/i;u m=32.2M(t);k(m){m=[0,m[2],m[3],m[1]]}H{32=/^([#.]?)([a-2j-9\\\\*31-]*)/i;m=32.2M(t)}k(m[1]=="#"&&J[J.G-1].4Q){u 3y=J[J.G-1].4Q(m[2]);J=r=3y&&(!m[3]||3y.1O==m[3].1N())?[3y]:[]}H{k(m[1]==".")u 4g=1v 4l("(^|\\\\s)"+m[2]+"(\\\\s|$)");6.I(J,l(){u 3g=m[1]!=""||m[0]==""?"*":m[2];k(7.1O.1N()=="7r"&&3g=="*")3g="2U";6.2h(r,m[1]!=""&&J.G!=1?6.4H(7,[],m[1],m[2],4g):7.5C(3g))});k(m[1]=="."&&J.G==1)r=6.2n(r,l(e){q 4g.1K(e.12)});k(m[1]=="#"&&J.G==1){u 5D=r;r=[];6.I(5D,l(){k(7.3B("3P")==m[2]){r=[7];q 14}})}J=r}t=t.1Y(32,"")}}k(t){u 19=6.1w(t,r);J=r=19.r;t=6.2B(19.t)}}k(J&&J[0]==1r)J.4K();6.2h(2b,J);q 2b},1w:l(t,r,2f){22(t&&/^[a-z[({<*:.#]/i.1K(t)){u p=6.5E,m;6.I(p,l(i,1C){m=1C.2M(t);k(m){t=t.7u(m[0].G);k(6.1l[m[1]].4k)m=6.1l[m[1]].4k(m);q 14}});k(m[1]==":"&&m[2]=="2f")r=6.1w(m[3],r,U).r;H k(m[1]=="."){u 1C=1v 4l("(^|\\\\s)"+m[2]+"(\\\\s|$)");r=6.2n(r,l(e){q 1C.1K(e.12||"")},2f)}H{u f=6.1l[m[1]];k(1o f!="21")f=6.1l[m[1]][m[2]];3A("f = l(a,i){"+(6.1l[m[1]].5H||"")+"q "+f+"}");r=6.2n(r,f,2f)}}q{r:r,t:t}},4H:l(o,r,1P,17,1C){N(u s=o.16;s;s=s.2a)k(s.1Q==1){u 1F=U;k(1P==".")1F=s.12&&1C.1K(s.12);H k(1P=="#")1F=s.3B("3P")==17;k(1F)r.1i(s);k(1P=="#"&&r.G)4o;k(s.16)6.4H(s,r,1P,17,1C)}q r},4E:l(B){u 4r=[];u M=B.V;22(M&&M!=11){4r.1i(M);M=M.V}q 4r},24:l(M,1c,3D,B){1c=1c||1;u 1S=0;N(;M;M=M[3D]){k(M.1Q==1)1S++;k(1S==1c||1c=="5J"&&1S%2==0&&1S>1&&M==B||1c=="5L"&&1S%2==1&&M==B)q M}},2I:l(n,B){u r=[];N(;n;n=n.2a){k(n.1Q==1&&(!B||n!=B))r.1i(n)}q r}});6.E={1F:l(Q,v,1I,D){k(6.T.1j&&Q.45!=R)Q=1D;k(D)1I.D=D;k(!1I.2q)1I.2q=7.2q++;k(!Q.1E)Q.1E={};u 34=Q.1E[v];k(!34){34=Q.1E[v]={};k(Q["35"+v])34[0]=Q["35"+v]}34[1I.2q]=1I;Q["35"+v]=7.5P;k(!7.1f[v])7.1f[v]=[];7.1f[v].1i(Q)},2q:1,1f:{},29:l(Q,v,1I){k(Q.1E)k(v&&v.v)4x Q.1E[v.v][v.1I.2q];H k(v&&Q.1E[v])k(1I)4x Q.1E[v][1I.2q];H N(u i 1y Q.1E[v])4x Q.1E[v][i];H N(u j 1y Q.1E)7.29(Q,j)},1R:l(v,D,Q){D=6.3G(D||[]);k(!Q){u g=7.1f[v];k(g)6.I(g,l(){6.E.1R(v,D,7)})}H k(Q["35"+v]){D.5R(7.2m({v:v,1M:Q}));u 19=Q["35"+v].W(Q,D);k(19!==14&&6.1q(Q[v]))Q[v]()}},5P:l(E){k(1o 6=="R")q 14;E=6.E.2m(E||1D.E||{});u 3I;u c=7.1E[E.v];u 1t=[].3F.3s(1u,1);1t.5R(E);N(u j 1y c){1t[0].1I=c[j];1t[0].D=c[j].D;k(c[j].W(7,1t)===14){E.2k();E.2y();3I=14}}k(6.T.1j)E.1M=E.2k=E.2y=E.1I=E.D=1a;q 3I},2m:l(E){k(!E.1M&&E.5S)E.1M=E.5S;k(E.5T==R&&E.5W!=R){u e=11.4v,b=11.7C;E.5T=E.5W+(e.5X||b.5X);E.7E=E.7F+(e.5Y||b.5Y)}k(6.T.2E&&E.1M.1Q==3){u 37=E;E=6.1p({},37);E.1M=37.1M.V;E.2k=l(){q 37.2k()};E.2y=l(){q 37.2y()}}k(!E.2k)E.2k=l(){7.3I=14};k(!E.2y)E.2y=l(){7.7J=U};q E}};6.C.1p({3L:l(v,D,C){q 7.I(l(){6.E.1F(7,v,C||D,D)})},5U:l(v,D,C){q 7.I(l(){6.E.1F(7,v,l(E){6(7).60(E);q(C||D).W(7,1u)},D)})},60:l(v,C){q 7.I(l(){6.E.29(7,v,C)})},1R:l(v,D){q 7.I(l(){6.E.1R(v,D,7)})},3p:l(){u a=1u;q 7.68(l(e){7.4B=7.4B==0?1:0;e.2k();q a[7.4B].W(7,[e])||14})},7L:l(f,g){l 47(e){u p=(e.v=="3N"?e.7M:e.7O)||e.7P;22(p&&p!=7)2N{p=p.V}2u(e){p=7};k(p==7)q 14;q(e.v=="3N"?f:g).W(7,[e])}q 7.3N(47).6a(47)},26:l(f){k(6.3K)f.W(11,[6]);H{6.2W.1i(l(){q f.W(7,[6])})}q 7}});6.1p({3K:14,2W:[],26:l(){k(!6.3K){6.3K=U;k(6.2W){6.I(6.2W,l(){7.W(11)});6.2W=1a}k(6.T.3b||6.T.3c)11.7R("6e",6.26,14)}}});1v l(){6.I(("7S,7T,2D,7W,7Y,4f,68,7Z,"+"80,81,82,3N,6a,85,3O,"+"4O,86,88,89,2L").3t(","),l(i,o){6.C[o]=l(f){q f?7.3L(o,f):7.1R(o)}});k(6.T.3b||6.T.3c)11.8c("6e",6.26,14);H k(6.T.1j){11.8d("<8e"+"8g 3P=69 8k=U "+"4y=//:><\\/2c>");u 2c=11.4Q("69");k(2c)2c.2l=l(){k(7.3z!="20")q;7.V.38(7);6.26()};2c=1a}H k(6.T.2E)6.4W=45(l(){k(11.3z=="8p"||11.3z=="20"){5r(6.4W);6.4W=1a;6.26()}},10);6.E.1F(1D,"2D",6.26)};k(6.T.1j)6(1D).5U("4f",l(){u 1f=6.E.1f;N(u v 1y 1f){u 4D=1f[v],i=4D.G;k(i&&v!=\'4f\')8E 6.E.29(4D[i-1],v);22(--i)}});6.C.1p({1G:l(P,K){u 1z=7.1w(":1z");q P?1z.23({2e:"1G",3V:"1G",1d:"1G"},P,K):1z.I(l(){7.1n.1e=7.2K?7.2K:"";k(6.1h(7,"1e")=="1X")7.1n.1e="2A"})},1B:l(P,K){u 39=7.1w(":39");q P?39.23({2e:"1B",3V:"1B",1d:"1B"},P,K):39.I(l(){7.2K=7.2K||6.1h(7,"1e");k(7.2K=="1X")7.2K="2A";7.1n.1e="1X"})},52:6.C.3p,3p:l(C,4S){u 1t=1u;q 6.1q(C)&&6.1q(4S)?7.52(C,4S):7.I(l(){6(7)[6(7).4Y(":1z")?"1G":"1B"].W(6(7),1t)})},6s:l(P,K){q 7.23({2e:"1G"},P,K)},6t:l(P,K){q 7.23({2e:"1B"},P,K)},6w:l(P,K){q 7.I(l(){u 6d=6(7).4Y(":1z")?"1G":"1B";6(7).23({2e:6d},P,K)})},6z:l(P,K){q 7.23({1d:"1G"},P,K)},6A:l(P,K){q 7.23({1d:"1B"},P,K)},6C:l(P,3q,K){q 7.23({1d:3q},P,K)},23:l(F,P,1k,K){q 7.1A(l(){7.2s=6.1p({},F);u 1m=6.P(P,1k,K);N(u p 1y F){u e=1v 6.36(7,1m,p);k(F[p].1g==3J)e.2t(e.M(),F[p]);H e[F[p]](F)}})},1A:l(v,C){k(!C){C=v;v="36"}q 7.I(l(){k(!7.1A)7.1A={};k(!7.1A[v])7.1A[v]=[];7.1A[v].1i(C);k(7.1A[v].G==1)C.W(7)})}});6.1p({P:l(P,1k,C){u 1m=P&&P.1g==6G?P:{20:C||!C&&1k||6.1q(P)&&P,27:P,1k:C&&1k||1k&&1k.1g!=4L&&1k};1m.27=(1m.27&&1m.27.1g==3J?1m.27:{6M:6O,6S:50}[1m.27])||6T;1m.1J=1m.20;1m.20=l(){6.5Z(7,"36");k(6.1q(1m.1J))1m.1J.W(7)};q 1m},1k:{},1A:{},5Z:l(B,v){v=v||"36";k(B.1A&&B.1A[v]){B.1A[v].4K();u f=B.1A[v][0];k(f)f.W(B)}},36:l(B,1b,F){u z=7;u y=B.1n;u 42=6.1h(B,"1e");y.1e="2A";y.5v="1z";z.a=l(){k(1b.3n)1b.3n.W(B,[z.2i]);k(F=="1d")6.1x(y,"1d",z.2i);H k(5c(z.2i))y[F]=5c(z.2i)+"46"};z.5h=l(){q 4m(6.1h(B,F))};z.M=l(){u r=4m(6.30(B,F));q r&&r>-7b?r:z.5h()};z.2t=l(48,3q){z.4j=(1v 5p()).5q();z.2i=48;z.a();z.43=45(l(){z.3n(48,3q)},13)};z.1G=l(){k(!B.1s)B.1s={};B.1s[F]=7.M();1b.1G=U;z.2t(0,B.1s[F]);k(F!="1d")y[F]="5m"};z.1B=l(){k(!B.1s)B.1s={};B.1s[F]=7.M();1b.1B=U;z.2t(B.1s[F],0)};z.3p=l(){k(!B.1s)B.1s={};B.1s[F]=7.M();k(42=="1X"){1b.1G=U;k(F!="1d")y[F]="5m";z.2t(0,B.1s[F])}H{1b.1B=U;z.2t(B.1s[F],0)}};z.3n=l(33,3E){u t=(1v 5p()).5q();k(t>1b.27+z.4j){5r(z.43);z.43=1a;z.2i=3E;z.a();k(B.2s)B.2s[F]=U;u 2b=U;N(u i 1y B.2s)k(B.2s[i]!==U)2b=14;k(2b){y.5v="";y.1e=42;k(6.1h(B,"1e")=="1X")y.1e="2A";k(1b.1B)y.1e="1X";k(1b.1B||1b.1G)N(u p 1y B.2s)k(p=="1d")6.1x(y,p,B.1s[p]);H y[p]=""}k(2b&&6.1q(1b.20))1b.20.W(B)}H{u n=t-7.4j;u p=n/1b.27;z.2i=1b.1k&&6.1k[1b.1k]?6.1k[1b.1k](p,n,33,(3E-33),1b.27):((-5I.7v(p*5I.7w)/2)+0.5)*(3E-33)+33;z.a()}}}});6.C.1p({7x:l(S,1W,K){7.2D(S,1W,K,1)},2D:l(S,1W,K,1T){k(6.1q(S))q 7.3L("2D",S);K=K||l(){};u v="63";k(1W)k(6.1q(1W.1g)){K=1W;1W=1a}H{1W=6.2U(1W);v="6c"}u 4u=7;6.3Q({S:S,v:v,D:1W,1T:1T,20:l(2G,Y){k(Y=="2J"||!1T&&Y=="5F")4u.1x("2z",2G.3H).4t().I(K,[2G.3H,Y,2G]);H K.W(4u,[2G.3H,Y,2G])}});q 7},7D:l(){q 6.2U(7)},4t:l(){q 7.2o("2c").I(l(){k(7.4y)6.6b(7.4y);H 6.4J(7.2F||7.7G||7.2z||"")}).4A()}});k(6.T.1j&&1o 3l=="R")3l=l(){q 1v 7K("7N.7Q")};6.I("57,5N,5M,6f,5K,5A".3t(","),l(i,o){6.C[o]=l(f){q 7.3L(o,f)}});6.1p({2g:l(S,D,K,v,1T){k(6.1q(D)){K=D;D=1a}q 6.3Q({S:S,D:D,2J:K,4p:v,1T:1T})},84:l(S,D,K,v){q 6.2g(S,D,K,v,1)},6b:l(S,K){q 6.2g(S,1a,K,"2c")},87:l(S,D,K){q 6.2g(S,D,K,"65")},8b:l(S,D,K,v){q 6.3Q({v:"6c",S:S,D:D,2J:K,4p:v})},8f:l(25){6.3X.25=25},8h:l(6j){6.1p(6.3X,6j)},3X:{1f:U,v:"63",25:0,59:"8q/x-8t-8y-8F",51:U,4C:U,D:1a},3i:{},3Q:l(s){s=6.1p({},6.3X,s);k(s.D){k(s.51&&1o s.D!="21")s.D=6.2U(s.D);k(s.v.4T()=="2g")s.S+=((s.S.15("?")>-1)?"&":"?")+s.D}k(s.1f&&!6.4X++)6.E.1R("57");u 4w=14;u L=1v 3l();L.6I(s.v,s.S,s.4C);k(s.D)L.3k("6L-6P",s.59);k(s.1T)L.3k("6V-4a-6Y",6.3i[s.S]||"72, 75 77 79 4c:4c:4c 7f");L.3k("X-7i-7k","3l");k(L.7m)L.3k("7p","7q");k(s.5x)s.5x(L);k(s.1f)6.E.1R("5A",[L,s]);u 2l=l(4i){k(L&&(L.3z==4||4i=="25")){4w=U;u Y;2N{Y=6.6h(L)&&4i!="25"?s.1T&&6.56(L,s.S)?"5F":"2J":"2L";k(Y!="2L"){u 3C;2N{3C=L.4h("58-4a")}2u(e){}k(s.1T&&3C)6.3i[s.S]=3C;u D=6.5B(L,s.4p);k(s.2J)s.2J(D,Y);k(s.1f)6.E.1R("5K",[L,s])}H 6.3o(s,L,Y)}2u(e){Y="2L";6.3o(s,L,Y,e)}k(s.1f)6.E.1R("5M",[L,s]);k(s.1f&&!--6.4X)6.E.1R("5N");k(s.20)s.20(L,Y);L.2l=l(){};L=1a}};L.2l=2l;k(s.25>0)64(l(){k(L){L.7A();k(!4w)2l("25")}},s.25);u 4G=L;2N{4G.7I(s.D)}2u(e){6.3o(s,L,1a,e)}k(!s.4C)2l();q 4G},3o:l(s,L,Y,e){k(s.2L)s.2L(L,Y,e);k(s.1f)6.E.1R("6f",[L,s,e])},4X:0,6h:l(r){2N{q!r.Y&&8w.8z=="3Y:"||(r.Y>=50&&r.Y<6q)||r.Y==5b||6.T.2E&&r.Y==R}2u(e){}q 14},56:l(L,S){2N{u 5Q=L.4h("58-4a");q L.Y==5b||5Q==6.3i[S]||6.T.2E&&L.Y==R}2u(e){}q 14},5B:l(r,v){u 4n=r.4h("7t-v");u D=!v&&4n&&4n.15("L")>=0;D=v=="L"||D?r.7y:r.3H;k(v=="2c")6.4J(D);k(v=="65")3A("D = "+D);k(v=="4P")6("<1Z>").4P(D).4t();q D},2U:l(a){u s=[];k(a.1g==2x||a.3R)6.I(a,l(){s.1i(2C(7.17)+"="+2C(7.O))});H N(u j 1y a)k(a[j].1g==2x)6.I(a[j],l(){s.1i(2C(j)+"="+2C(7))});H s.1i(2C(j)+"="+2C(a[j]));q s.55("&")},4J:l(D){k(1D.5y)1D.5y(D);H k(6.T.2E)1D.64(D,0);H 3A.3s(1D,D)}})}',62,539,'||||||jQuery|this|||||||||||||if|function|||||return||||var|type||||||elem|fn|data|event|prop|length|else|each|ret|callback|xml|cur|for|value|speed|element|undefined|url|browser|true|parentNode|apply||status|||document|className||false|indexOf|firstChild|name|obj|val|null|options|result|opacity|display|global|constructor|css|push|msie|easing|expr|opt|style|typeof|extend|isFunction|context|orig|args|arguments|new|filter|attr|in|hidden|queue|hide|re|window|events|add|show|arg|handler|old|test|elems|target|toUpperCase|nodeName|token|nodeType|trigger|num|ifModified|key|table|params|none|replace|div|complete|string|while|animate|nth|timeout|ready|duration|tbody|remove|nextSibling|done|script|tb|height|not|get|merge|now|z0|preventDefault|onreadystatechange|fix|grep|find|pushStack|guid|wrap|curAnim|custom|catch|first|al|Array|stopPropagation|innerHTML|block|trim|encodeURIComponent|load|safari|text|res|el|sibling|success|oldblock|error|exec|try|cssFloat|parPos|childNodes|substr|last|has|param|checked|readyList|disabled|selected|map|curCSS|_|re2|firstNum|handlers|on|fx|originalEvent|removeChild|visible|domManip|mozilla|opera|insertBefore|oWidth|clean|tag|append|lastModified|styleFloat|setRequestHeader|XMLHttpRequest|empty|step|handleError|toggle|to|child|call|split|inArray|multiFilter|foundToken|9_|oid|readyState|eval|getAttribute|modRes|dir|lastNum|slice|makeArray|responseText|returnValue|Number|isReady|bind|second|mouseover|select|id|ajax|jquery|oHeight|cloneNode|position|width|defaultView|ajaxSettings|file|static|swap|tr|oldDisplay|timer|inv|setInterval|px|handleHover|from|String|Modified|visibility|00|radio|button|unload|rec|getResponseHeader|isTimeout|startTime|_resort|RegExp|parseFloat|ct|break|dataType|clone|matched|appendChild|evalScripts|self|documentElement|requestDone|delete|src|deep|end|lastToggle|async|els|parents|pos|xml2|getAll|alpha|globalEval|shift|Function|setArray|float|submit|html|getElementById|currentStyle|fn2|toLowerCase|index|getComputedStyle|safariTimer|active|is|force|200|processData|_toggle|getPropertyValue|newProp|join|httpNotModified|ajaxStart|Last|contentType|rl|304|parseInt|appendTo|before|gt|after|max|lt|eq|removeAttr|previousSibling|1px|parent|contains|Date|getTime|clearInterval|password|image|reset|overflow|input|beforeSend|execScript|checkbox|ajaxSend|httpData|getElementsByTagName|tmp|parse|notmodified|ol|_prefix|Math|even|ajaxSuccess|odd|ajaxComplete|ajaxStop|webkit|handle|xmlRes|unshift|srcElement|pageX|one|prepend|clientX|scrollLeft|scrollTop|dequeue|unbind|check|nodeValue|GET|setTimeout|json|sl|100|click|__ie_init|mouseout|getScript|POST|state|DOMContentLoaded|ajaxError|createElement|httpSuccess|prevObject|settings|absolute|right|left|relative|clientHeight|clientWidth|300|toString|slideDown|slideUp|tfoot|td|slideToggle|th|TBODY|fadeIn|fadeOut|class|fadeTo|readonly|readOnly|gi|Object|match|open|9999|action|Content|slow|tagName|600|Type|setAttribute|ig|fast|400|concat|If|continue|userAgent|Since|compatible|boxModel|compatMode|Thu|next|siblings|01|children|Jan|prependTo|1970|insertAfter|10000|removeAttribute|addClass|removeClass|GMT|toggleClass|lastChild|Requested|only|With|enabled|overrideMimeType|BUTTON|textarea|Connection|close|OBJECT|Right|content|substring|cos|PI|loadIfModified|responseXML|prev|abort|CSS1Compat|body|serialize|pageY|clientY|textContent|navigator|send|cancelBubble|ActiveXObject|hover|fromElement|Microsoft|toElement|relatedTarget|XMLHTTP|removeEventListener|blur|focus|getAttributeNode|method|resize|FORM|scroll|dblclick|mousedown|mouseup|mousemove|zoom|getIfModified|change|keydown|getJSON|keypress|keyup|htmlFor|post|addEventListener|write|scr|ajaxTimeout|ipt|ajaxSetup|prototype|size|defer|createTextNode|thead|reverse|TABLE|loaded|application|TR|noConflict|www|Top|Bottom|location|Left|form|protocol|border|Width|offsetHeight|offsetWidth|do|urlencoded|padding'.split('|'),0,{})) 12 | -------------------------------------------------------------------------------- /lib/uuidtools.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Copyright (c) 2005 Robert Aman 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | #++ 23 | 24 | UUID_TOOLS_VERSION = "0.1.4" 25 | 26 | require 'uri' 27 | require 'time' 28 | require 'thread' 29 | require 'digest/sha1' 30 | require 'digest/md5' 31 | 32 | # Because it's impossible to hype a UUID generator on its genuine merits, 33 | # I give you... Really bad ASCII art in the comments: 34 | # 35 | # 36 | # \ 37 | # / 38 | # + 39 | # ] 40 | # ] 41 | # | 42 | # / 43 | # Mp___ 44 | # `~0NNp, 45 | # __ggM' 46 | # g0M~"` 47 | # ]0M*- 48 | # 49 | # ___ 50 | # _g000M00g, 51 | # j0M~ ~M& 52 | # j0M" ~N, 53 | # j0P M& 54 | # jM 1 55 | # j0 ]1 56 | # .0P 0, 57 | # 00' M& 58 | # 0M ]0L 59 | # ]0f ___ M0 60 | # M0NN0M00MMM~"'M 0& 61 | # `~ ~0 ]0, 62 | # ]M ]0& 63 | # M& M0, 64 | # ____gp_ M& M0_ 65 | # __p0MPM8MM&_ M/ ^0&_ 66 | # gN"` M0N_j0, MM&__ 67 | # _gF `~M0P` __ M00g 68 | # g0' gM0&, ~M0& 69 | # _pM` 0, ]M1 "00& 70 | # _00 /g1MMgj01 ]0MI 71 | # _0F t"M,7MMM 00I 72 | # g0' _ N&j& 40' 73 | # g0' _p0Mq_ ' N0QQNM#g, 74 | # 0' _g0000000g__ ~M@MMM000g 75 | # f _jM00@` ~M0000Mgppg, "P00& 76 | # | g000~ `~M000000&_ ~0& 77 | # ]M _M00F "00MM` ~#& 78 | # `0L m000F #E "0f 79 | # 9r j000M` 40, 00 80 | # ]0g_ j00M` ^M0MNggp#gqpg M0& 81 | # ~MPM0f ~M000000000g_ ,_ygg&M00f 82 | # `~~~M00000000000000 83 | # `M0000000000f 84 | # ~@@@MF~` 85 | # 86 | # 87 | 88 | #= uuidtools.rb 89 | # 90 | # UUIDTools was designed to be a simple library for generating any 91 | # of the various types of UUIDs. It conforms to RFC 4122 whenever 92 | # possible. 93 | # 94 | #== Example 95 | # UUID.md5_create(UUID_DNS_NAMESPACE, "www.widgets.com") 96 | # => # 97 | # UUID.sha1_create(UUID_DNS_NAMESPACE, "www.widgets.com") 98 | # => # 99 | # UUID.timestamp_create 100 | # => # 101 | # UUID.random_create 102 | # => # 103 | class UUID 104 | @@mac_address = nil 105 | @@last_timestamp = nil 106 | @@last_node_id = nil 107 | @@last_clock_sequence = nil 108 | @@state_file = nil 109 | @@mutex = Mutex.new 110 | 111 | def initialize(time_low, time_mid, time_hi_and_version, 112 | clock_seq_hi_and_reserved, clock_seq_low, nodes) 113 | unless time_low >= 0 && time_low < 4294967296 114 | raise ArgumentError, 115 | "Expected unsigned 32-bit number for time_low, got #{time_low}." 116 | end 117 | unless time_mid >= 0 && time_mid < 65536 118 | raise ArgumentError, 119 | "Expected unsigned 16-bit number for time_mid, got #{time_mid}." 120 | end 121 | unless time_hi_and_version >= 0 && time_hi_and_version < 65536 122 | raise ArgumentError, 123 | "Expected unsigned 16-bit number for time_hi_and_version, " + 124 | "got #{time_hi_and_version}." 125 | end 126 | unless clock_seq_hi_and_reserved >= 0 && clock_seq_hi_and_reserved < 256 127 | raise ArgumentError, 128 | "Expected unsigned 8-bit number for clock_seq_hi_and_reserved, " + 129 | "got #{clock_seq_hi_and_reserved}." 130 | end 131 | unless clock_seq_low >= 0 && clock_seq_low < 256 132 | raise ArgumentError, 133 | "Expected unsigned 8-bit number for clock_seq_low, " + 134 | "got #{clock_seq_low}." 135 | end 136 | unless nodes.respond_to? :size 137 | raise ArgumentError, 138 | "Expected nodes to respond to :size." 139 | end 140 | unless nodes.size == 6 141 | raise ArgumentError, 142 | "Expected nodes to have size of 6." 143 | end 144 | for node in nodes 145 | unless node >= 0 && node < 256 146 | raise ArgumentError, 147 | "Expected unsigned 8-bit number for each node, " + 148 | "got #{node}." 149 | end 150 | end 151 | @time_low = time_low 152 | @time_mid = time_mid 153 | @time_hi_and_version = time_hi_and_version 154 | @clock_seq_hi_and_reserved = clock_seq_hi_and_reserved 155 | @clock_seq_low = clock_seq_low 156 | @nodes = nodes 157 | end 158 | 159 | attr_accessor :time_low 160 | attr_accessor :time_mid 161 | attr_accessor :time_hi_and_version 162 | attr_accessor :clock_seq_hi_and_reserved 163 | attr_accessor :clock_seq_low 164 | attr_accessor :nodes 165 | 166 | # Parses a UUID from a string. 167 | def UUID.parse(uuid_string) 168 | unless uuid_string.kind_of? String 169 | raise ArgumentError, 170 | "Expected String, got #{uuid_string.class.name} instead." 171 | end 172 | uuid_components = uuid_string.downcase.scan( 173 | Regexp.new("^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-" + 174 | "([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})$")).first 175 | raise ArgumentError, "Invalid UUID format." if uuid_components.nil? 176 | time_low = uuid_components[0].to_i(16) 177 | time_mid = uuid_components[1].to_i(16) 178 | time_hi_and_version = uuid_components[2].to_i(16) 179 | clock_seq_hi_and_reserved = uuid_components[3].to_i(16) 180 | clock_seq_low = uuid_components[4].to_i(16) 181 | nodes = [] 182 | for i in 0..5 183 | nodes << uuid_components[5][(i * 2)..(i * 2) + 1].to_i(16) 184 | end 185 | return UUID.new(time_low, time_mid, time_hi_and_version, 186 | clock_seq_hi_and_reserved, clock_seq_low, nodes) 187 | end 188 | 189 | # Parses a UUID from a raw byte string. 190 | def UUID.parse_raw(raw_string) 191 | unless raw_string.kind_of? String 192 | raise ArgumentError, 193 | "Expected String, got #{raw_string.class.name} instead." 194 | end 195 | integer = UUID.convert_byte_string_to_int(raw_string) 196 | 197 | time_low = (integer >> 96) & 0xFFFFFFFF 198 | time_mid = (integer >> 80) & 0xFFFF 199 | time_hi_and_version = (integer >> 64) & 0xFFFF 200 | clock_seq_hi_and_reserved = (integer >> 56) & 0xFF 201 | clock_seq_low = (integer >> 48) & 0xFF 202 | nodes = [] 203 | for i in 0..5 204 | nodes << ((integer >> (40 - (i * 8))) & 0xFF) 205 | end 206 | return UUID.new(time_low, time_mid, time_hi_and_version, 207 | clock_seq_hi_and_reserved, clock_seq_low, nodes) 208 | end 209 | 210 | # Creates a UUID from a random value. 211 | def UUID.random_create() 212 | new_uuid = UUID.parse_raw(UUID.true_random) 213 | new_uuid.time_hi_and_version &= 0x0FFF 214 | new_uuid.time_hi_and_version |= (4 << 12) 215 | new_uuid.clock_seq_hi_and_reserved &= 0x3F 216 | new_uuid.clock_seq_hi_and_reserved |= 0x80 217 | return new_uuid 218 | end 219 | 220 | # Creates a UUID from a timestamp. 221 | def UUID.timestamp_create(timestamp=nil) 222 | # We need a lock here to prevent two threads from ever 223 | # getting the same timestamp. 224 | @@mutex.synchronize do 225 | # Always use GMT to generate UUIDs. 226 | if timestamp.nil? 227 | gmt_timestamp = Time.now.gmtime 228 | else 229 | gmt_timestamp = timestamp.gmtime 230 | end 231 | # Convert to 100 nanosecond blocks 232 | gmt_timestamp_100_nanoseconds = (gmt_timestamp.tv_sec * 10000000) + 233 | (gmt_timestamp.tv_usec * 10) + 0x01B21DD213814000 234 | nodes = UUID.get_mac_address.split(":").collect do |octet| 235 | octet.to_i(16) 236 | end 237 | node_id = 0 238 | for i in 0..5 239 | node_id += (nodes[i] << (40 - (i * 8))) 240 | end 241 | clock_sequence = @@last_clock_sequence 242 | if clock_sequence.nil? 243 | clock_sequence = UUID.convert_byte_string_to_int(UUID.true_random) 244 | end 245 | if @@last_node_id != nil && @@last_node_id != node_id 246 | # The node id has changed. Change the clock id. 247 | clock_sequence = UUID.convert_byte_string_to_int(UUID.true_random) 248 | elsif @@last_timestamp != nil && 249 | gmt_timestamp_100_nanoseconds <= @@last_timestamp 250 | clock_sequence = clock_sequence + 1 251 | end 252 | @@last_timestamp = gmt_timestamp_100_nanoseconds 253 | @@last_node_id = node_id 254 | @@last_clock_sequence = clock_sequence 255 | 256 | time_low = gmt_timestamp_100_nanoseconds & 0xFFFFFFFF 257 | time_mid = ((gmt_timestamp_100_nanoseconds >> 32) & 0xFFFF) 258 | time_hi_and_version = ((gmt_timestamp_100_nanoseconds >> 48) & 0x0FFF) 259 | time_hi_and_version |= (1 << 12) 260 | clock_seq_low = clock_sequence & 0xFF; 261 | clock_seq_hi_and_reserved = (clock_sequence & 0x3F00) >> 8 262 | clock_seq_hi_and_reserved |= 0x80 263 | 264 | return UUID.new(time_low, time_mid, time_hi_and_version, 265 | clock_seq_hi_and_reserved, clock_seq_low, nodes) 266 | end 267 | end 268 | 269 | # Creates a UUID using the MD5 hash. (Version 3) 270 | def UUID.md5_create(namespace, name) 271 | return UUID.create_from_hash(Digest::MD5, namespace, name) 272 | end 273 | 274 | # Creates a UUID using the SHA1 hash. (Version 5) 275 | def UUID.sha1_create(namespace, name) 276 | return UUID.create_from_hash(Digest::SHA1, namespace, name) 277 | end 278 | 279 | # This method applies only to version 1 UUIDs. 280 | # Checks if the node ID was generated from a random number 281 | # or from an IEEE 802 address (MAC address). 282 | # Always returns false for UUIDs that aren't version 1. 283 | # This should not be confused with version 4 UUIDs where 284 | # more than just the node id is random. 285 | def random_node_id? 286 | return false if self.version != 1 287 | return ((self.nodes.first & 0x01) == 1) 288 | end 289 | 290 | # Returns true if this UUID is the 291 | # nil UUID (00000000-0000-0000-0000-000000000000). 292 | def nil_uuid? 293 | return false if self.time_low != 0 294 | return false if self.time_mid != 0 295 | return false if self.time_hi_and_version != 0 296 | return false if self.clock_seq_hi_and_reserved != 0 297 | return false if self.clock_seq_low != 0 298 | self.nodes.each do |node| 299 | return false if node != 0 300 | end 301 | return true 302 | end 303 | 304 | # Returns the UUID version type. 305 | # Possible values: 306 | # 1 - Time-based with unique or random host identifier 307 | # 2 - DCE Security version (with POSIX UIDs) 308 | # 3 - Name-based (MD5 hash) 309 | # 4 - Random 310 | # 5 - Name-based (SHA-1 hash) 311 | def version 312 | return (time_hi_and_version >> 12) 313 | end 314 | 315 | # Returns the UUID variant. 316 | # Possible values: 317 | # 0b000 - Reserved, NCS backward compatibility. 318 | # 0b100 - The variant specified in this document. 319 | # 0b110 - Reserved, Microsoft Corporation backward compatibility. 320 | # 0b111 - Reserved for future definition. 321 | def variant 322 | variant_raw = (clock_seq_hi_and_reserved >> 5) 323 | result = nil 324 | if (variant_raw >> 2) == 0 325 | result = 0x000 326 | elsif (variant_raw >> 1) == 2 327 | result = 0x100 328 | else 329 | result = variant_raw 330 | end 331 | return (result >> 6) 332 | end 333 | 334 | # Returns true if this UUID is valid. 335 | def valid? 336 | if [0b000, 0b100, 0b110, 0b111].include?(self.variant) && 337 | (1..5).include?(self.version) 338 | return true 339 | else 340 | return false 341 | end 342 | end 343 | 344 | # Returns the IEEE 802 address used to generate this UUID or 345 | # nil if a MAC address was not used. 346 | def mac_address 347 | return nil if self.version != 1 348 | return nil if self.random_node_id? 349 | return (self.nodes.collect do |node| 350 | sprintf("%2.2x", node) 351 | end).join(":") 352 | end 353 | 354 | # Returns the timestamp used to generate this UUID 355 | def timestamp 356 | return nil if self.version != 1 357 | gmt_timestamp_100_nanoseconds = 0 358 | gmt_timestamp_100_nanoseconds += 359 | ((self.time_hi_and_version & 0x0FFF) << 48) 360 | gmt_timestamp_100_nanoseconds += (self.time_mid << 32) 361 | gmt_timestamp_100_nanoseconds += self.time_low 362 | return Time.at( 363 | (gmt_timestamp_100_nanoseconds - 0x01B21DD213814000) / 10000000.0) 364 | end 365 | 366 | # Compares two UUIDs lexically 367 | def <=>(other_uuid) 368 | check = self.time_low <=> other_uuid.time_low 369 | return check if check != 0 370 | check = self.time_mid <=> other_uuid.time_mid 371 | return check if check != 0 372 | check = self.time_hi_and_version <=> other_uuid.time_hi_and_version 373 | return check if check != 0 374 | check = self.clock_seq_hi_and_reserved <=> 375 | other_uuid.clock_seq_hi_and_reserved 376 | return check if check != 0 377 | check = self.clock_seq_low <=> other_uuid.clock_seq_low 378 | return check if check != 0 379 | for i in 0..5 380 | if (self.nodes[i] < other_uuid.nodes[i]) 381 | return -1 382 | end 383 | if (self.nodes[i] > other_uuid.nodes[i]) 384 | return 1 385 | end 386 | end 387 | return 0 388 | end 389 | 390 | # Returns a representation of the object's state 391 | def inspect 392 | return "#" 393 | end 394 | 395 | # Returns the hex digest of the UUID object. 396 | def hexdigest 397 | return self.to_i.to_s(16) 398 | end 399 | 400 | # Returns the raw bytes that represent this UUID. 401 | def raw 402 | return UUID.convert_int_to_byte_string(self.to_i, 16) 403 | end 404 | 405 | # Returns a string representation for this UUID. 406 | def to_s 407 | result = sprintf("%8.8x-%4.4x-%4.4x-%2.2x%2.2x-", @time_low, @time_mid, 408 | @time_hi_and_version, @clock_seq_hi_and_reserved, @clock_seq_low); 409 | for i in 0..5 410 | result << sprintf("%2.2x", @nodes[i]) 411 | end 412 | return result 413 | end 414 | 415 | # Returns an integer representation for this UUID. 416 | def to_i 417 | bytes = (time_low << 96) + (time_mid << 80) + 418 | (time_hi_and_version << 64) + (clock_seq_hi_and_reserved << 56) + 419 | (clock_seq_low << 48) 420 | for i in 0..5 421 | bytes += (nodes[i] << (40 - (i * 8))) 422 | end 423 | return bytes 424 | end 425 | 426 | # Returns a URI for this UUID. 427 | def to_uri 428 | return URI.parse(self.to_uri_string) 429 | end 430 | 431 | # Returns a URI string for this UUID. 432 | def to_uri_string 433 | return "urn:uuid:#{self.to_s}" 434 | end 435 | 436 | def UUID.create_from_hash(hash_class, namespace, name) #:nodoc: 437 | if hash_class == Digest::MD5 438 | version = 3 439 | elsif hash_class == Digest::SHA1 440 | version = 5 441 | else 442 | raise ArgumentError, 443 | "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." 444 | end 445 | hash = hash_class.new 446 | hash.update(namespace.raw) 447 | hash.update(name) 448 | hash_string = hash.to_s[0..31] 449 | new_uuid = UUID.parse("#{hash_string[0..7]}-#{hash_string[8..11]}-" + 450 | "#{hash_string[12..15]}-#{hash_string[16..19]}-#{hash_string[20..31]}") 451 | 452 | new_uuid.time_hi_and_version &= 0x0FFF 453 | new_uuid.time_hi_and_version |= (version << 12) 454 | new_uuid.clock_seq_hi_and_reserved &= 0x3F 455 | new_uuid.clock_seq_hi_and_reserved |= 0x80 456 | return new_uuid 457 | end 458 | 459 | # Returns the MAC address of the current computer's network card. 460 | # Returns nil if a MAC address could not be found. 461 | def UUID.get_mac_address #:nodoc: 462 | if @@mac_address.nil? 463 | if RUBY_PLATFORM =~ /win/ && !(RUBY_PLATFORM =~ /darwin/) 464 | begin 465 | ifconfig_output = `ipconfig /all` 466 | mac_addresses = ifconfig_output.scan( 467 | Regexp.new("(#{(["[0-9a-fA-F]{2}"] * 6).join("-")})")) 468 | if mac_addresses.size > 0 469 | @@mac_address = mac_addresses.first.first.downcase.gsub(/-/, ":") 470 | end 471 | rescue 472 | end 473 | else 474 | begin 475 | ifconfig_output = `ifconfig` 476 | mac_addresses = ifconfig_output.scan( 477 | Regexp.new("ether (#{(["[0-9a-fA-F]{2}"] * 6).join(":")})")) 478 | if mac_addresses.size == 0 479 | ifconfig_output = `ifconfig | grep HWaddr | cut -c39-` 480 | mac_addresses = ifconfig_output.scan( 481 | Regexp.new("(#{(["[0-9a-fA-F]{2}"] * 6).join(":")})")) 482 | end 483 | if mac_addresses.size == 0 484 | ifconfig_output = `/sbin/ifconfig` 485 | mac_addresses = ifconfig_output.scan( 486 | Regexp.new("ether (#{(["[0-9a-fA-F]{2}"] * 6).join(":")})")) 487 | end 488 | if mac_addresses.size == 0 489 | ifconfig_output = `/sbin/ifconfig | grep HWaddr | cut -c39-` 490 | mac_addresses = ifconfig_output.scan( 491 | Regexp.new("(#{(["[0-9a-fA-F]{2}"] * 6).join(":")})")) 492 | end 493 | if mac_addresses.size > 0 494 | @@mac_address = mac_addresses.first.first 495 | end 496 | rescue 497 | end 498 | end 499 | end 500 | return @@mac_address 501 | end 502 | 503 | # Returns 128 bits of highly unpredictable data. 504 | # The random number generator isn't perfect, but it's 505 | # much, much better than the built-in pseudorandom number generators. 506 | def UUID.true_random #:nodoc: 507 | require 'benchmark' 508 | hash = Digest::SHA1.new 509 | performance = Benchmark.measure do 510 | hash.update(rand.to_s) 511 | hash.update(srand.to_s) 512 | hash.update(rand.to_s) 513 | hash.update(srand.to_s) 514 | hash.update(Time.now.to_s) 515 | hash.update(rand.to_s) 516 | hash.update(self.object_id.to_s) 517 | hash.update(rand.to_s) 518 | hash.update(hash.object_id.to_s) 519 | hash.update(self.methods.inspect) 520 | begin 521 | random_device = nil 522 | if File.exists? "/dev/urandom" 523 | random_device = File.open "/dev/urandom", "r" 524 | elsif File.exists? "/dev/random" 525 | random_device = File.open "/dev/random", "r" 526 | end 527 | hash.update(random_device.read(20)) if random_device != nil 528 | rescue 529 | end 530 | begin 531 | srand(hash.to_s.to_i(16) >> 128) 532 | rescue 533 | end 534 | hash.update(rand.to_s) 535 | hash.update(UUID.true_random) if (rand(2) == 0) 536 | end 537 | hash.update(performance.real.to_s) 538 | hash.update(performance.inspect) 539 | return UUID.convert_int_to_byte_string(hash.to_s[4..35].to_i(16), 16) 540 | end 541 | 542 | def UUID.convert_int_to_byte_string(integer, size) #:nodoc: 543 | byte_string = "" 544 | for i in 0..(size - 1) 545 | byte_string << ((integer >> (((size - 1) - i) * 8)) & 0xFF) 546 | end 547 | return byte_string 548 | end 549 | 550 | def UUID.convert_byte_string_to_int(byte_string) #:nodoc: 551 | integer = 0 552 | size = byte_string.size 553 | for i in 0..(size - 1) 554 | integer += (byte_string[i] << (((size - 1) - i) * 8)) 555 | end 556 | return integer 557 | end 558 | end 559 | 560 | UUID_DNS_NAMESPACE = UUID.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") 561 | UUID_URL_NAMESPACE = UUID.parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8") 562 | UUID_OID_NAMESPACE = UUID.parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8") 563 | UUID_X500_NAMESPACE = UUID.parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8") 564 | -------------------------------------------------------------------------------- /lib/feed_tools.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Copyright (c) 2005 Robert Aman 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | #++ 23 | 24 | if Object.const_defined?(:FEED_TOOLS_ENV) 25 | warn("FeedTools may have been loaded improperly. This may be caused " + 26 | "by the presence of the RUBYOPT environment variable or by using " + 27 | "load instead of require. This can also be caused by missing " + 28 | "the Iconv library, which is common on Windows.") 29 | end 30 | 31 | FEED_TOOLS_ENV = ENV['FEED_TOOLS_ENV'] || 32 | ENV['RAILS_ENV'] || 33 | 'production' # :nodoc: 34 | 35 | FEED_TOOLS_VERSION = "0.2.17" 36 | 37 | FEED_TOOLS_NAMESPACES = { 38 | "admin" => "http://webns.net/mvcb/", 39 | "ag" => "http://purl.org/rss/1.0/modules/aggregation/", 40 | "annotate" => "http://purl.org/rss/1.0/modules/annotate/", 41 | "atom" => "http://www.w3.org/2005/Atom", 42 | "atom03" => "http://purl.org/atom/ns#", 43 | "audio" => "http://media.tangent.org/rss/1.0/", 44 | "blogChannel" => "http://backend.userland.com/blogChannelModule", 45 | "cc" => "http://web.resource.org/cc/", 46 | "creativeCommons" => "http://backend.userland.com/creativeCommonsRssModule", 47 | "co" => "http://purl.org/rss/1.0/modules/company", 48 | "content" => "http://purl.org/rss/1.0/modules/content/", 49 | "cp" => "http://my.theinfo.org/changed/1.0/rss/", 50 | "dc" => "http://purl.org/dc/elements/1.1/", 51 | "dcterms" => "http://purl.org/dc/terms/", 52 | "email" => "http://purl.org/rss/1.0/modules/email/", 53 | "ev" => "http://purl.org/rss/1.0/modules/event/", 54 | "icbm" => "http://postneo.com/icbm/", 55 | "image" => "http://purl.org/rss/1.0/modules/image/", 56 | "feedburner" => "http://rssnamespace.org/feedburner/ext/1.0", 57 | "foaf" => "http://xmlns.com/foaf/0.1/", 58 | "fm" => "http://freshmeat.net/rss/fm/", 59 | "itunes" => "http://www.itunes.com/DTDs/Podcast-1.0.dtd", 60 | "l" => "http://purl.org/rss/1.0/modules/link/", 61 | "media" => "http://search.yahoo.com/mrss", 62 | "pingback" => "http://madskills.com/public/xml/rss/module/pingback/", 63 | "prism" => "http://prismstandard.org/namespaces/1.2/basic/", 64 | "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 65 | "rdfs" => "http://www.w3.org/2000/01/rdf-schema#", 66 | "ref" => "http://purl.org/rss/1.0/modules/reference/", 67 | "reqv" => "http://purl.org/rss/1.0/modules/richequiv/", 68 | "rss10" => "http://purl.org/rss/1.0/", 69 | "search" => "http://purl.org/rss/1.0/modules/search/", 70 | "slash" => "http://purl.org/rss/1.0/modules/slash/", 71 | "soap" => "http://schemas.xmlsoap.org/soap/envelope/", 72 | "ss" => "http://purl.org/rss/1.0/modules/servicestatus/", 73 | "str" => "http://hacks.benhammersley.com/rss/streaming/", 74 | "sub" => "http://purl.org/rss/1.0/modules/subscription/", 75 | "sy" => "http://purl.org/rss/1.0/modules/syndication/", 76 | "taxo" => "http://purl.org/rss/1.0/modules/taxonomy/", 77 | "thr" => "http://purl.org/rss/1.0/modules/threading/", 78 | "ti" => "http://purl.org/rss/1.0/modules/textinput/", 79 | "trackback" => "http://madskills.com/public/xml/rss/module/trackback/", 80 | "wfw" => "http://wellformedweb.org/CommentAPI/", 81 | "wiki" => "http://purl.org/rss/1.0/modules/wiki/", 82 | "xhtml" => "http://www.w3.org/1999/xhtml", 83 | "xml" => "http://www.w3.org/XML/1998/namespace" 84 | } 85 | 86 | begin 87 | require 'iconv' 88 | rescue LoadError 89 | warn("The Iconv library does not appear to be installed properly. " + 90 | "FeedTools cannot function properly without it.") 91 | raise 92 | end 93 | 94 | require 'builder' 95 | 96 | begin 97 | require 'tidy' 98 | rescue LoadError 99 | # Ignore the error for now. 100 | end 101 | 102 | require 'htree' 103 | 104 | require 'net/http' 105 | require 'net/https' 106 | require 'net/ftp' 107 | 108 | require 'rexml/document' 109 | 110 | require 'uri' 111 | require 'time' 112 | require 'cgi' 113 | require 'pp' 114 | require 'yaml' 115 | 116 | require 'feed_tools/feed' 117 | require 'feed_tools/feed_item' 118 | 119 | #= feed_tools.rb 120 | # 121 | # FeedTools was designed to be a simple XML feed parser, generator, and translator with a built-in 122 | # caching system. 123 | # 124 | #== Example 125 | # slashdot_feed = FeedTools::Feed.open('http://www.slashdot.org/index.rss') 126 | # slashdot_feed.title 127 | # => "Slashdot" 128 | # slashdot_feed.description 129 | # => "News for nerds, stuff that matters" 130 | # slashdot_feed.link 131 | # => "http://slashdot.org/" 132 | # slashdot_feed.items.first.find_node("slash:hitparade/text()").value 133 | # => "43,37,28,23,11,3,1" 134 | module FeedTools 135 | 136 | @force_tidy_enabled = true 137 | @tidy_enabled = false 138 | @feed_cache = nil 139 | @user_agent = "FeedTools/#{FEED_TOOLS_VERSION} " + 140 | "+http://www.sporkmonger.com/projects/feedtools/" 141 | @no_content_string = "[no description]" 142 | 143 | # Error raised when a feed cannot be retrieved 144 | class FeedAccessError < StandardError 145 | end 146 | 147 | # Returns the current caching mechanism. 148 | def FeedTools.feed_cache 149 | return @feed_cache 150 | end 151 | 152 | # Sets the current caching mechanism. If set to nil, disables caching. 153 | # Default is the DatabaseFeedCache class. 154 | # 155 | # Objects of this class must accept the following messages: 156 | # id 157 | # id= 158 | # url 159 | # url= 160 | # title 161 | # title= 162 | # link 163 | # link= 164 | # feed_data 165 | # feed_data= 166 | # feed_data_type 167 | # feed_data_type= 168 | # etag 169 | # etag= 170 | # last_modified 171 | # last_modified= 172 | # save 173 | # 174 | # Additionally, the class itself must accept the following messages: 175 | # find_by_id 176 | # find_by_url 177 | # initialize_cache 178 | # connected? 179 | def FeedTools.feed_cache=(new_feed_cache) 180 | # TODO: ensure that the feed cache class actually does those things. 181 | # ================================================================== 182 | @feed_cache = new_feed_cache 183 | end 184 | 185 | # Returns true if FeedTools.feed_cache is not nil and a connection with 186 | # the cache has been successfully established. Also returns false if an 187 | # error is raised while trying to determine the status of the cache. 188 | def FeedTools.feed_cache_connected? 189 | begin 190 | return false if FeedTools.feed_cache.nil? 191 | return FeedTools.feed_cache.connected? 192 | rescue 193 | return false 194 | end 195 | end 196 | 197 | # Returns the currently used user agent string. 198 | def FeedTools.user_agent 199 | return @user_agent 200 | end 201 | 202 | # Sets the user agent string to send in the http headers. 203 | def FeedTools.user_agent=(new_user_agent) 204 | @user_agent = new_user_agent 205 | end 206 | 207 | # Returns the currently used no content string. 208 | def FeedTools.no_content_string 209 | return @no_content_string 210 | end 211 | 212 | # Sets the no content string to use when a feed is missing a content element. 213 | # Used only for xml output. 214 | def FeedTools.no_content_string=(new_no_content_string) 215 | @no_content_string = new_no_content_string 216 | end 217 | 218 | # Returns true if the html tidy module can be used. 219 | # 220 | # Obviously, you need the tidy gem installed in order to run with html 221 | # tidy features turned on. 222 | # 223 | # This method does a fairly complicated, and probably unnecessarily 224 | # desperate search for the libtidy library. If you want this thing to 225 | # execute fast, the best thing to do is to set Tidy.path ahead of time. 226 | # If Tidy.path is set, this method doesn't do much. If it's not set, 227 | # it will do it's darnedest to find the libtidy library. If you set 228 | # the LIBTIDYPATH environment variable to the libtidy library, it should 229 | # be able to find it. 230 | # 231 | # Once the library is located, this method will run much faster. 232 | def FeedTools.tidy_enabled? 233 | # This is an override variable to keep tidy from being used even if it 234 | # is available. 235 | if @force_tidy_enabled == false 236 | return false 237 | end 238 | if @tidy_enabled.nil? || @tidy_enabled == false 239 | @tidy_enabled = false 240 | begin 241 | require 'tidy' 242 | if Tidy.path.nil? 243 | # *Shrug*, just brute force it, I guess. There's a lot of places 244 | # this thing might be hiding in, depending on platform and general 245 | # sanity of the person who installed the thing. Most of these are 246 | # probably unlikely, but it's not like checking unlikely locations 247 | # hurts. Much. Especially if you actually find it. 248 | libtidy_locations = [ 249 | '/usr/local/lib/libtidy.dylib', 250 | '/opt/local/lib/libtidy.dylib', 251 | '/usr/lib/libtidy.dylib', 252 | '/usr/local/lib/tidylib.dylib', 253 | '/opt/local/lib/tidylib.dylib', 254 | '/usr/lib/tidylib.dylib', 255 | '/usr/local/lib/tidy.dylib', 256 | '/opt/local/lib/tidy.dylib', 257 | '/usr/lib/tidy.dylib', 258 | '/usr/local/lib/libtidy.so', 259 | '/opt/local/lib/libtidy.so', 260 | '/usr/lib/libtidy.so', 261 | '/usr/local/lib/tidylib.so', 262 | '/opt/local/lib/tidylib.so', 263 | '/usr/lib/tidylib.so', 264 | '/usr/local/lib/tidy.so', 265 | '/opt/local/lib/tidy.so', 266 | '/usr/lib/tidy.so', 267 | 'C:\Program Files\Tidy\tidy.dll', 268 | 'C:\Tidy\tidy.dll', 269 | 'C:\Ruby\bin\tidy.dll', 270 | 'C:\Ruby\tidy.dll', 271 | '/usr/local/lib', 272 | '/opt/local/lib', 273 | '/usr/lib' 274 | ] 275 | # We just made this thing up, but if someone sets it, we'll 276 | # go ahead and check it 277 | unless ENV['LIBTIDYPATH'].nil? 278 | libtidy_locations = 279 | libtidy_locations.reverse.push(ENV['LIBTIDYPATH']) 280 | end 281 | for path in libtidy_locations 282 | if File.exists? path 283 | if File.ftype(path) == "file" 284 | Tidy.path = path 285 | @tidy_enabled = true 286 | break 287 | elsif File.ftype(path) == "directory" 288 | # Ok, now perhaps we're getting a bit more desperate 289 | lib_paths = 290 | `find #{path} -name '*tidy*' | grep '\\.\\(so\\|dylib\\)$'` 291 | # If there's more than one, grab the first one and 292 | # hope for the best, and if it doesn't work, then blame the 293 | # user for not specifying more accurately. 294 | tidy_path = lib_paths.split("\n").first 295 | unless tidy_path.nil? 296 | Tidy.path = tidy_path 297 | @tidy_enabled = true 298 | break 299 | end 300 | end 301 | end 302 | end 303 | # Still couldn't find it. 304 | unless @tidy_enabled 305 | @tidy_enabled = false 306 | end 307 | else 308 | @tidy_enabled = true 309 | end 310 | rescue LoadError 311 | # Tidy not installed, disable features that rely on tidy. 312 | @tidy_enabled = false 313 | end 314 | end 315 | return @tidy_enabled 316 | end 317 | 318 | # Turns html tidy support on or off. Be aware, that setting this to true 319 | # does not mean tidy will be enabled. It simply means that tidy will be 320 | # enabled if it is available to be enabled. 321 | def FeedTools.tidy_enabled=(new_tidy_enabled) 322 | @force_tidy_enabled = new_tidy_enabled 323 | end 324 | 325 | # Attempts to ensures that the passed url is valid and sane. Accepts very, very ugly urls 326 | # and makes every effort to figure out what it was supposed to be. Also translates from 327 | # the feed: and rss: pseudo-protocols to the http: protocol. 328 | def FeedTools.normalize_url(url) 329 | if url.nil? || url == "" 330 | return nil 331 | end 332 | normalized_url = url.strip 333 | 334 | # if a url begins with the '/' character, it only makes sense that they 335 | # meant to be using a file:// url. Fix it for them. 336 | if normalized_url.length > 0 && normalized_url[0..0] == "/" 337 | normalized_url = "file://" + normalized_url 338 | end 339 | 340 | # if a url begins with a drive letter followed by a colon, we're looking at 341 | # a file:// url. Fix it for them. 342 | if normalized_url.length > 0 && 343 | normalized_url.scan(/^[a-zA-Z]:[\\\/]/).size > 0 344 | normalized_url = "file:///" + normalized_url 345 | end 346 | 347 | # if a url begins with javascript:, it's quite possibly an attempt at 348 | # doing something malicious. Let's keep that from getting anywhere, 349 | # shall we? 350 | if (normalized_url.downcase =~ /javascript:/) != nil 351 | return "#" 352 | end 353 | 354 | # deal with all of the many ugly possibilities involved in the rss: 355 | # and feed: pseudo-protocols (incidentally, whose crazy idea was this 356 | # mess?) 357 | normalized_url.gsub!(/^http:\/*(feed:\/*)?/, "http://") 358 | normalized_url.gsub!(/^http:\/*(rss:\/*)?/, "http://") 359 | normalized_url.gsub!(/^feed:\/*(http:\/*)?/, "http://") 360 | normalized_url.gsub!(/^rss:\/*(http:\/*)?/, "http://") 361 | normalized_url.gsub!(/^file:\/*/, "file:///") 362 | normalized_url.gsub!(/^https:\/*/, "https://") 363 | # fix (very) bad urls (usually of the user-entered sort) 364 | normalized_url.gsub!(/^http:\/*(http:\/*)*/, "http://") 365 | 366 | if (normalized_url =~ /^file:/) == 0 367 | # Adjust windows-style urls 368 | normalized_url.gsub!(/^file:\/\/\/([a-zA-Z])\|/, 'file:///\1:') 369 | normalized_url.gsub!(/\\/, '/') 370 | else 371 | if (normalized_url =~ /https?:\/\//) == nil 372 | normalized_url = "http://" + normalized_url 373 | end 374 | if normalized_url == "http://" 375 | return nil 376 | end 377 | begin 378 | feed_uri = URI.parse(normalized_url) 379 | if feed_uri.scheme == nil 380 | feed_uri.scheme = "http" 381 | end 382 | if feed_uri.path == nil || feed_uri.path == "" 383 | feed_uri.path = "/" 384 | end 385 | if (feed_uri.path =~ /^[\/]+/) == 0 386 | feed_uri.path.gsub!(/^[\/]+/, "/") 387 | end 388 | feed_uri.host.downcase! 389 | normalized_url = feed_uri.to_s 390 | rescue URI::InvalidURIError 391 | end 392 | end 393 | 394 | # We can't do a proper set of escaping, so this will 395 | # have to do. 396 | normalized_url.gsub!(/%20/, " ") 397 | normalized_url.gsub!(/ /, "%20") 398 | 399 | return normalized_url 400 | end 401 | 402 | # Converts a url into a tag uri 403 | def FeedTools.build_tag_uri(url, date) 404 | unless url.kind_of? String 405 | raise ArgumentError, "Expected String, got #{url.class.name}" 406 | end 407 | unless date.kind_of? Time 408 | raise ArgumentError, "Expected Time, got #{date.class.name}" 409 | end 410 | tag_uri = normalize_url(url) 411 | unless FeedTools.is_uri?(tag_uri) 412 | raise ArgumentError, "Must supply a valid URL." 413 | end 414 | host = URI.parse(tag_uri).host 415 | tag_uri.gsub!(/^(http|ftp|file):\/*/, "") 416 | tag_uri.gsub!(/#/, "/") 417 | tag_uri = "tag:#{host},#{date.strftime('%Y-%m-%d')}:" + 418 | "#{tag_uri[(tag_uri.index(host) + host.size)..-1]}" 419 | return tag_uri 420 | end 421 | 422 | # Converts a url into a urn:uuid: uri 423 | def FeedTools.build_urn_uri(url) 424 | unless url.kind_of? String 425 | raise ArgumentError, "Expected String, got #{url.class.name}" 426 | end 427 | normalized_url = normalize_url(url) 428 | require 'uuidtools' 429 | return UUID.sha1_create(UUID_URL_NAMESPACE, normalized_url).to_uri_string 430 | end 431 | 432 | # Returns true if the parameter appears to be a valid uri 433 | def FeedTools.is_uri?(url) 434 | return false if url.nil? 435 | begin 436 | uri = URI.parse(url) 437 | if uri.scheme.nil? || uri.scheme == "" 438 | return false 439 | end 440 | rescue URI::InvalidURIError 441 | return false 442 | end 443 | return true 444 | end 445 | 446 | # Escapes all html entities 447 | def FeedTools.escape_entities(html) 448 | return nil if html.nil? 449 | escaped_html = CGI.escapeHTML(html) 450 | unescaped_html.gsub!(/'/, "'") 451 | unescaped_html.gsub!(/"/, """) 452 | return escaped_html 453 | end 454 | 455 | # Unescapes all html entities 456 | def FeedTools.unescape_entities(html) 457 | return nil if html.nil? 458 | unescaped_html = html 459 | unescaped_html.gsub!(/&/, "&") 460 | unescaped_html.gsub!(/&/, "&") 461 | unescaped_html = CGI.unescapeHTML(unescaped_html) 462 | unescaped_html.gsub!(/'/, "'") 463 | unescaped_html.gsub!(/"/, "\"") 464 | return unescaped_html 465 | end 466 | 467 | # Removes all html tags from the html formatted text. 468 | def FeedTools.strip_html(html) 469 | return nil if html.nil? 470 | # TODO: do this properly 471 | # ====================== 472 | stripped_html = html.gsub(/<\/?[^>]+>/, "") 473 | return stripped_html 474 | end 475 | 476 | # Tidys up the html 477 | def FeedTools.tidy_html(html, options = {}) 478 | return nil if html.nil? 479 | if FeedTools.tidy_enabled? 480 | is_fragment = true 481 | html.gsub!(/<!'/, "&lt;!'") 482 | if (html.strip =~ /(.|\n)*/) != nil || 483 | (html.strip =~ /<\/body>(.|\n)*<\/html>$/) != nil 484 | is_fragment = false 485 | end 486 | if (html.strip =~ /<\?xml(.|\n)*\?>/) != nil 487 | is_fragment = false 488 | end 489 | tidy_html = Tidy.open(:show_warnings=>false) do |tidy| 490 | tidy.options.output_xml = true 491 | tidy.options.numeric_entities = true 492 | tidy.options.markup = true 493 | tidy.options.indent = false 494 | tidy.options.wrap = 0 495 | tidy.options.logical_emphasis = true 496 | # TODO: Make this match the actual encoding of the feed 497 | # ===================================================== 498 | tidy.options.input_encoding = "utf8" 499 | tidy.options.output_encoding = "ascii" 500 | tidy.options.ascii_chars = false 501 | tidy.options.doctype = "omit" 502 | xml = tidy.clean(html) 503 | xml 504 | end 505 | if is_fragment 506 | # Tidy sticks ...[our html]... in. 507 | # We don't want this. 508 | tidy_html.strip! 509 | tidy_html.gsub!(/^(.|\n)*/, "") 510 | tidy_html.gsub!(/<\/body>(.|\n)*<\/html>$/, "") 511 | tidy_html.strip! 512 | end 513 | tidy_html.gsub!(/&/, "&") 514 | tidy_html.gsub!(/&/, "&") 515 | tidy_html.gsub!(/\320\262\320\202\342\204\242/, "\342\200\231") 516 | 517 | else 518 | tidy_html = html 519 | end 520 | return tidy_html 521 | end 522 | 523 | # Removes all dangerous html tags from the html formatted text. 524 | # If mode is set to :escape, dangerous and unknown elements will 525 | # be escaped. If mode is set to :strip, dangerous and unknown 526 | # elements and all children will be removed entirely. 527 | # Dangerous or unknown attributes are always removed. 528 | def FeedTools.sanitize_html(html, mode=:strip) 529 | return nil if html.nil? 530 | 531 | # Lists borrowed from Mark Pilgrim's feedparser 532 | acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 533 | 'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite', 534 | 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 535 | 'dt', 'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 536 | 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 537 | 'li', 'map', 'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 538 | 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 539 | 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 540 | 'u', 'ul', 'var'] 541 | 542 | acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', 543 | 'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 544 | 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class', 545 | 'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime', 546 | 'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height', 547 | 'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang', 548 | 'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name', 549 | 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev', 550 | 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 551 | 'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 552 | 'type', 'usemap', 'valign', 'value', 'vspace', 'width'] 553 | 554 | # Replace with appropriate named entities 555 | html.gsub!(/&/, "&") 556 | html.gsub!(/&/, "&") 557 | html.gsub!(/<!'/, "&lt;!'") 558 | 559 | # Hackity hack. But it works, and it seems plenty fast enough. 560 | html_doc = HTree.parse_xml("" + html + "").to_rexml 561 | 562 | sanitize_node = lambda do |html_node| 563 | if html_node.respond_to? :children 564 | for child in html_node.children 565 | if child.kind_of? REXML::Element 566 | unless acceptable_elements.include? child.name 567 | if mode == :strip 568 | html_node.delete_element(child) 569 | else 570 | new_child = REXML::Text.new(CGI.escapeHTML(child.to_s)) 571 | html_node.insert_after(child, new_child) 572 | html_node.delete_element(child) 573 | end 574 | end 575 | for attribute in child.attributes.keys 576 | unless acceptable_attributes.include? attribute 577 | child.delete_attribute(attribute) 578 | end 579 | end 580 | end 581 | sanitize_node.call(child) 582 | end 583 | end 584 | html_node 585 | end 586 | sanitize_node.call(html_doc.root) 587 | html = html_doc.root.inner_xml 588 | return html 589 | end 590 | 591 | # Creates a merged "planet" feed from a set of urls. 592 | def FeedTools.build_merged_feed(url_array) 593 | return nil if url_array.nil? 594 | merged_feed = FeedTools::Feed.new 595 | retrieved_feeds = [] 596 | feed_threads = [] 597 | url_array.each do |feed_url| 598 | feed_threads << Thread.new do 599 | feed = Feed.open(feed_url) 600 | retrieved_feeds << feed 601 | end 602 | end 603 | feed_threads.each do |thread| 604 | thread.join 605 | end 606 | retrieved_feeds.each do |feed| 607 | merged_feed.entries.concat( 608 | feed.entries.collect do |entry| 609 | entry.title = "#{feed.title}: #{entry.title}" 610 | entry 611 | end ) 612 | end 613 | return merged_feed 614 | end 615 | end 616 | 617 | module REXML # :nodoc: 618 | class Element # :nodoc: 619 | unless REXML::Element.public_instance_methods.include? :inner_xml 620 | def inner_xml # :nodoc: 621 | result = "" 622 | self.each_child do |child| 623 | result << child.to_s 624 | end 625 | return result 626 | end 627 | end 628 | 629 | unless REXML::Element.public_instance_methods.include? :base_uri 630 | def base_uri # :nodoc: 631 | if not attribute('xml:base') 632 | return parent.base_uri 633 | elsif parent 634 | return URI.join(parent.base_uri, attribute('xml:base').value).to_s 635 | else 636 | return (attribute('xml:base').value or '') 637 | end 638 | end 639 | end 640 | end 641 | end 642 | 643 | begin 644 | unless FeedTools.feed_cache.nil? 645 | FeedTools.feed_cache.initialize_cache 646 | end 647 | rescue 648 | end -------------------------------------------------------------------------------- /lib/redcloth.rb: -------------------------------------------------------------------------------- 1 | # vim:ts=4:sw=4: 2 | # = RedCloth - Textile and Markdown Hybrid for Ruby 3 | # 4 | # Homepage:: http://whytheluckystiff.net/ruby/redcloth/ 5 | # Author:: why the lucky stiff (http://whytheluckystiff.net/) 6 | # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.) 7 | # License:: BSD 8 | # 9 | # (see http://hobix.com/textile/ for a Textile Reference.) 10 | # 11 | # Based on (and also inspired by) both: 12 | # 13 | # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt 14 | # Textism for PHP: http://www.textism.com/tools/textile/ 15 | # 16 | # 17 | 18 | # = RedCloth 19 | # 20 | # RedCloth is a Ruby library for converting Textile and/or Markdown 21 | # into HTML. You can use either format, intermingled or separately. 22 | # You can also extend RedCloth to honor your own custom text stylings. 23 | # 24 | # RedCloth users are encouraged to use Textile if they are generating 25 | # HTML and to use Markdown if others will be viewing the plain text. 26 | # 27 | # == What is Textile? 28 | # 29 | # Textile is a simple formatting style for text 30 | # documents, loosely based on some HTML conventions. 31 | # 32 | # == Sample Textile Text 33 | # 34 | # h2. This is a title 35 | # 36 | # h3. This is a subhead 37 | # 38 | # This is a bit of paragraph. 39 | # 40 | # bq. This is a blockquote. 41 | # 42 | # = Writing Textile 43 | # 44 | # A Textile document consists of paragraphs. Paragraphs 45 | # can be specially formatted by adding a small instruction 46 | # to the beginning of the paragraph. 47 | # 48 | # h[n]. Header of size [n]. 49 | # bq. Blockquote. 50 | # # Numeric list. 51 | # * Bulleted list. 52 | # 53 | # == Quick Phrase Modifiers 54 | # 55 | # Quick phrase modifiers are also included, to allow formatting 56 | # of small portions of text within a paragraph. 57 | # 58 | # \_emphasis\_ 59 | # \_\_italicized\_\_ 60 | # \*strong\* 61 | # \*\*bold\*\* 62 | # ??citation?? 63 | # -deleted text- 64 | # +inserted text+ 65 | # ^superscript^ 66 | # ~subscript~ 67 | # @code@ 68 | # %(classname)span% 69 | # 70 | # ==notextile== (leave text alone) 71 | # 72 | # == Links 73 | # 74 | # To make a hypertext link, put the link text in "quotation 75 | # marks" followed immediately by a colon and the URL of the link. 76 | # 77 | # Optional: text in (parentheses) following the link text, 78 | # but before the closing quotation mark, will become a Title 79 | # attribute for the link, visible as a tool tip when a cursor is above it. 80 | # 81 | # Example: 82 | # 83 | # "This is a link (This is a title) ":http://www.textism.com 84 | # 85 | # Will become: 86 | # 87 | # This is a link 88 | # 89 | # == Images 90 | # 91 | # To insert an image, put the URL for the image inside exclamation marks. 92 | # 93 | # Optional: text that immediately follows the URL in (parentheses) will 94 | # be used as the Alt text for the image. Images on the web should always 95 | # have descriptive Alt text for the benefit of readers using non-graphical 96 | # browsers. 97 | # 98 | # Optional: place a colon followed by a URL immediately after the 99 | # closing ! to make the image into a link. 100 | # 101 | # Example: 102 | # 103 | # !http://www.textism.com/common/textist.gif(Textist)! 104 | # 105 | # Will become: 106 | # 107 | # Textist 108 | # 109 | # With a link: 110 | # 111 | # !/common/textist.gif(Textist)!:http://textism.com 112 | # 113 | # Will become: 114 | # 115 | # Textist 116 | # 117 | # == Defining Acronyms 118 | # 119 | # HTML allows authors to define acronyms via the tag. The definition appears as a 120 | # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing, 121 | # this should be used at least once for each acronym in documents where they appear. 122 | # 123 | # To quickly define an acronym in Textile, place the full text in (parentheses) 124 | # immediately following the acronym. 125 | # 126 | # Example: 127 | # 128 | # ACLU(American Civil Liberties Union) 129 | # 130 | # Will become: 131 | # 132 | # ACLU 133 | # 134 | # == Adding Tables 135 | # 136 | # In Textile, simple tables can be added by seperating each column by 137 | # a pipe. 138 | # 139 | # |a|simple|table|row| 140 | # |And|Another|table|row| 141 | # 142 | # Attributes are defined by style definitions in parentheses. 143 | # 144 | # table(border:1px solid black). 145 | # (background:#ddd;color:red). |{}| | | | 146 | # 147 | # == Using RedCloth 148 | # 149 | # RedCloth is simply an extension of the String class, which can handle 150 | # Textile formatting. Use it like a String and output HTML with its 151 | # RedCloth#to_html method. 152 | # 153 | # doc = RedCloth.new " 154 | # 155 | # h2. Test document 156 | # 157 | # Just a simple test." 158 | # 159 | # puts doc.to_html 160 | # 161 | # By default, RedCloth uses both Textile and Markdown formatting, with 162 | # Textile formatting taking precedence. If you want to turn off Markdown 163 | # formatting, to boost speed and limit the processor: 164 | # 165 | # class RedCloth::Textile.new( str ) 166 | 167 | class RedCloth < String 168 | 169 | VERSION = '3.0.4' 170 | DEFAULT_RULES = [:textile, :markdown] 171 | 172 | # 173 | # Two accessor for setting security restrictions. 174 | # 175 | # This is a nice thing if you're using RedCloth for 176 | # formatting in public places (e.g. Wikis) where you 177 | # don't want users to abuse HTML for bad things. 178 | # 179 | # If +:filter_html+ is set, HTML which wasn't 180 | # created by the Textile processor will be escaped. 181 | # 182 | # If +:filter_styles+ is set, it will also disable 183 | # the style markup specifier. ('{color: red}') 184 | # 185 | attr_accessor :filter_html, :filter_styles 186 | 187 | # 188 | # Accessor for toggling hard breaks. 189 | # 190 | # If +:hard_breaks+ is set, single newlines will 191 | # be converted to HTML break tags. This is the 192 | # default behavior for traditional RedCloth. 193 | # 194 | attr_accessor :hard_breaks 195 | 196 | # Accessor for toggling lite mode. 197 | # 198 | # In lite mode, block-level rules are ignored. This means 199 | # that tables, paragraphs, lists, and such aren't available. 200 | # Only the inline markup for bold, italics, entities and so on. 201 | # 202 | attr_accessor :lite_mode 203 | 204 | # 205 | # Accessor for toggling span caps. 206 | # 207 | # Textile places `span' tags around capitalized 208 | # words by default, but this wreaks havoc on Wikis. 209 | # If +:no_span_caps+ is set, this will be 210 | # suppressed. 211 | # 212 | attr_accessor :no_span_caps 213 | 214 | # 215 | # Establishes the markup predence. Available rules include: 216 | # 217 | # == Textile Rules 218 | # 219 | # The following textile rules can be set individually. Or add the complete 220 | # set of rules with the single :textile rule, which supplies the rule set in 221 | # the following precedence: 222 | # 223 | # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/) 224 | # block_textile_table:: Textile table block structures 225 | # block_textile_lists:: Textile list structures 226 | # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.) 227 | # inline_textile_image:: Textile inline images 228 | # inline_textile_link:: Textile inline links 229 | # inline_textile_span:: Textile inline spans 230 | # inline_textile_glyphs:: Textile entities (such as em-dashes and smart quotes) 231 | # 232 | # == Markdown 233 | # 234 | # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/) 235 | # block_markdown_setext:: Markdown setext headers 236 | # block_markdown_atx:: Markdown atx headers 237 | # block_markdown_rule:: Markdown horizontal rules 238 | # block_markdown_bq:: Markdown blockquotes 239 | # block_markdown_lists:: Markdown lists 240 | # inline_markdown_link:: Markdown links 241 | attr_accessor :rules 242 | 243 | # Returns a new RedCloth object, based on _string_ and 244 | # enforcing all the included _restrictions_. 245 | # 246 | # r = RedCloth.new( "h1. A bold man", [:filter_html] ) 247 | # r.to_html 248 | # #=>"

A <b>bold</b> man

" 249 | # 250 | def initialize( string, restrictions = [] ) 251 | restrictions.each { |r| method( "#{ r }=" ).call( true ) } 252 | super( string ) 253 | end 254 | 255 | # 256 | # Generates HTML from the Textile contents. 257 | # 258 | # r = RedCloth.new( "And then? She *fell*!" ) 259 | # r.to_html( true ) 260 | # #=>"And then? She fell!" 261 | # 262 | def to_html( *rules ) 263 | rules = DEFAULT_RULES if rules.empty? 264 | # make our working copy 265 | text = self.dup 266 | 267 | @urlrefs = {} 268 | @shelf = [] 269 | textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists, 270 | :block_textile_prefix, :inline_textile_image, :inline_textile_link, 271 | :inline_textile_code, :inline_textile_glyphs, :inline_textile_span] 272 | markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule, 273 | :block_markdown_bq, :block_markdown_lists, 274 | :inline_markdown_reflink, :inline_markdown_link] 275 | @rules = rules.collect do |rule| 276 | case rule 277 | when :markdown 278 | markdown_rules 279 | when :textile 280 | textile_rules 281 | else 282 | rule 283 | end 284 | end.flatten 285 | 286 | # standard clean up 287 | incoming_entities text 288 | clean_white_space text 289 | no_textile text 290 | 291 | # start processor 292 | @pre_list = [] 293 | rip_offtags text 294 | hard_break text 295 | unless @lite_mode 296 | refs text 297 | blocks text 298 | end 299 | inline text 300 | smooth_offtags text 301 | 302 | retrieve text 303 | 304 | text.gsub!( /<\/?notextile>/, '' ) 305 | text.gsub!( /x%x%/, '&' ) 306 | clean_html text if filter_html 307 | text.strip! 308 | text 309 | 310 | end 311 | 312 | ####### 313 | private 314 | ####### 315 | # 316 | # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. 317 | # (from PyTextile) 318 | # 319 | TEXTILE_TAGS = 320 | 321 | [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], 322 | [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], 323 | [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], 324 | [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], 325 | [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. 326 | 327 | collect! do |a, b| 328 | [a.chr, ( b.zero? and "" or "&#{ b };" )] 329 | end 330 | 331 | # 332 | # Regular expressions to convert to HTML. 333 | # 334 | A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ 335 | A_VLGN = /[\-^~]/ 336 | C_CLAS = '(?:\([^)]+\))' 337 | C_LNGE = '(?:\[[^\]]+\])' 338 | C_STYL = '(?:\{[^}]+\})' 339 | S_CSPN = '(?:\\\\\d+)' 340 | S_RSPN = '(?:/\d+)' 341 | A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" 342 | S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" 343 | C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" 344 | # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) 345 | PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) 346 | HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)' 347 | 348 | # Text markup tags, don't conflict with block tags 349 | SIMPLE_HTML_TAGS = [ 350 | 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code', 351 | 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br', 352 | 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo' 353 | ] 354 | 355 | # Elements to handle 356 | GLYPHS = [ 357 | # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing 358 | [ /([^\s\[{(>])\'/, '\1’' ], # single closing 359 | [ /\'(?=\s|s\b|[#{PUNCT}])/, '’' ], # single closing 360 | [ /\'/, '‘' ], # single opening 361 | # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing 362 | [ /([^\s\[{(>])"/, '\1”' ], # double closing 363 | [ /"(?=\s|[#{PUNCT}])/, '”' ], # double closing 364 | [ /"/, '“' ], # double opening 365 | [ /\b( )?\.{3}/, '\1…' ], # ellipsis 366 | [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym 367 | [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]{2,})([^\2\3', :no_span_caps ], # 3+ uppercase caps 368 | [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash 369 | [ /\s->\s/, ' → ' ], # right arrow 370 | [ /\s-\s/, ' – ' ], # en dash 371 | [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign 372 | [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark 373 | [ /\b ?[(\[]R[\])]/i, '®' ], # registered 374 | [ /\b ?[(\[]C[\])]/i, '©' ] # copyright 375 | ] 376 | 377 | H_ALGN_VALS = { 378 | '<' => 'left', 379 | '=' => 'center', 380 | '>' => 'right', 381 | '<>' => 'justify' 382 | } 383 | 384 | V_ALGN_VALS = { 385 | '^' => 'top', 386 | '-' => 'middle', 387 | '~' => 'bottom' 388 | } 389 | 390 | QTAGS = [ 391 | ['**', 'b'], 392 | ['*', 'strong'], 393 | ['??', 'cite', :limit], 394 | ['-', 'del', :limit], 395 | ['__', 'i'], 396 | ['_', 'em', :limit], 397 | ['%', 'span', :limit], 398 | ['+', 'ins', :limit], 399 | ['^', 'sup'], 400 | ['~', 'sub'] 401 | ] 402 | QTAGS.collect! do |rc, ht, rtype| 403 | rcq = Regexp::quote rc 404 | re = 405 | case rtype 406 | when :limit 407 | /(\W) 408 | (#{rcq}) 409 | (#{C}) 410 | (?::(\S+?))? 411 | (.+?) 412 | #{rcq} 413 | (?=\W)/x 414 | else 415 | /(#{rcq}) 416 | (#{C}) 417 | (?::(\S+?))? 418 | (.+?) 419 | #{rcq}/xm 420 | end 421 | [rc, ht, re, rtype] 422 | end 423 | 424 | # 425 | # Flexible HTML escaping 426 | # 427 | def htmlesc( str, mode ) 428 | str.gsub!( '&', '&' ) 429 | str.gsub!( '"', '"' ) if mode != :NoQuotes 430 | str.gsub!( "'", ''' ) if mode == :Quotes 431 | str.gsub!( '<', '<') 432 | str.gsub!( '>', '>') 433 | end 434 | 435 | # Search and replace for Textile glyphs (quotes, dashes, other symbols) 436 | def pgl( text ) 437 | GLYPHS.each do |re, resub, tog| 438 | next if tog and method( tog ).call 439 | text.gsub! re, resub 440 | end 441 | end 442 | 443 | # Parses Textile attribute lists and builds an HTML attribute string 444 | def pba( text_in, element = "" ) 445 | 446 | return '' unless text_in 447 | 448 | style = [] 449 | text = text_in.dup 450 | if element == 'td' 451 | colspan = $1 if text =~ /\\(\d+)/ 452 | rowspan = $1 if text =~ /\/(\d+)/ 453 | style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN 454 | end 455 | 456 | style << "#{ $1 };" if not filter_styles and 457 | text.sub!( /\{([^}]*)\}/, '' ) 458 | 459 | lang = $1 if 460 | text.sub!( /\[([^)]+?)\]/, '' ) 461 | 462 | cls = $1 if 463 | text.sub!( /\(([^()]+?)\)/, '' ) 464 | 465 | style << "padding-left:#{ $1.length }em;" if 466 | text.sub!( /([(]+)/, '' ) 467 | 468 | style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) 469 | 470 | style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN 471 | 472 | cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ 473 | 474 | atts = '' 475 | atts << " style=\"#{ style.join }\"" unless style.empty? 476 | atts << " class=\"#{ cls }\"" unless cls.to_s.empty? 477 | atts << " lang=\"#{ lang }\"" if lang 478 | atts << " id=\"#{ id }\"" if id 479 | atts << " colspan=\"#{ colspan }\"" if colspan 480 | atts << " rowspan=\"#{ rowspan }\"" if rowspan 481 | 482 | atts 483 | end 484 | 485 | TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m 486 | 487 | # Parses a Textile table block, building HTML from the result. 488 | def block_textile_table( text ) 489 | text.gsub!( TABLE_RE ) do |matches| 490 | 491 | tatts, fullrow = $~[1..2] 492 | tatts = pba( tatts, 'table' ) 493 | tatts = shelve( tatts ) if tatts 494 | rows = [] 495 | 496 | fullrow. 497 | split( /\|$/m ). 498 | delete_if { |x| x.empty? }. 499 | each do |row| 500 | 501 | ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m 502 | 503 | cells = [] 504 | row.split( '|' ).each do |cell| 505 | ctyp = 'd' 506 | ctyp = 'h' if cell =~ /^_/ 507 | 508 | catts = '' 509 | catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/ 510 | 511 | unless cell.strip.empty? 512 | catts = shelve( catts ) if catts 513 | cells << "\t\t\t#{ cell }" 514 | end 515 | end 516 | ratts = shelve( ratts ) if ratts 517 | rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" 518 | end 519 | "\t\n#{ rows.join( "\n" ) }\n\t\n\n" 520 | end 521 | end 522 | 523 | LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m 524 | LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m 525 | 526 | # Parses Textile lists and generates HTML 527 | def block_textile_lists( text ) 528 | text.gsub!( LISTS_RE ) do |match| 529 | lines = match.split( /\n/ ) 530 | last_line = -1 531 | depth = [] 532 | lines.each_with_index do |line, line_id| 533 | if line =~ LISTS_CONTENT_RE 534 | tl,atts,content = $~[1..3] 535 | if depth.last 536 | if depth.last.length > tl.length 537 | (depth.length - 1).downto(0) do |i| 538 | break if depth[i].length == tl.length 539 | lines[line_id - 1] << "\n\t\n\t" 540 | depth.pop 541 | end 542 | end 543 | if depth.last and depth.last.length == tl.length 544 | lines[line_id - 1] << '' 545 | end 546 | end 547 | unless depth.last == tl 548 | depth << tl 549 | atts = pba( atts ) 550 | atts = shelve( atts ) if atts 551 | lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t
  • #{ content }" 552 | else 553 | lines[line_id] = "\t\t
  • #{ content }" 554 | end 555 | last_line = line_id 556 | 557 | else 558 | last_line = line_id 559 | end 560 | if line_id - last_line > 1 or line_id == lines.length - 1 561 | depth.delete_if do |v| 562 | lines[last_line] << "
  • \n\t" 563 | end 564 | end 565 | end 566 | lines.join( "\n" ) 567 | end 568 | end 569 | 570 | CODE_RE = /(\W) 571 | @ 572 | (?:\|(\w+?)\|)? 573 | (.+?) 574 | @ 575 | (?=\W)/x 576 | 577 | def inline_textile_code( text ) 578 | text.gsub!( CODE_RE ) do |m| 579 | before,lang,code,after = $~[1..4] 580 | lang = " lang=\"#{ lang }\"" if lang 581 | rip_offtags( "#{ before }#{ code }#{ after }" ) 582 | end 583 | end 584 | 585 | def lT( text ) 586 | text =~ /\#$/ ? 'o' : 'u' 587 | end 588 | 589 | def hard_break( text ) 590 | text.gsub!( /(.)\n(?! *[#*\s|]|$)/, "\\1
    " ) if hard_breaks 591 | end 592 | 593 | BLOCKS_GROUP_RE = /\n{2,}(?! )/m 594 | 595 | def blocks( text, deep_code = false ) 596 | text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk| 597 | plain = blk !~ /\A[#*> ]/ 598 | 599 | # skip blocks that are complex HTML 600 | if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1 601 | blk 602 | else 603 | # search for indentation levels 604 | blk.strip! 605 | if blk.empty? 606 | blk 607 | else 608 | code_blk = nil 609 | blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk| 610 | flush_left iblk 611 | blocks iblk, plain 612 | iblk.gsub( /^(\S)/, "\t\\1" ) 613 | if plain 614 | code_blk = iblk; "" 615 | else 616 | iblk 617 | end 618 | end 619 | 620 | block_applied = 0 621 | @rules.each do |rule_name| 622 | block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) ) 623 | end 624 | if block_applied.zero? 625 | if deep_code 626 | blk = "\t
    #{ blk }
    " 627 | else 628 | blk = "\t

    #{ blk }

    " 629 | end 630 | end 631 | # hard_break blk 632 | blk + "\n#{ code_blk }" 633 | end 634 | end 635 | 636 | end.join( "\n\n" ) ) 637 | end 638 | 639 | def textile_bq( tag, atts, cite, content ) 640 | cite, cite_title = check_refs( cite ) 641 | cite = " cite=\"#{ cite }\"" if cite 642 | atts = shelve( atts ) if atts 643 | "\t\n\t\t#{ content }

    \n\t" 644 | end 645 | 646 | def textile_p( tag, atts, cite, content ) 647 | atts = shelve( atts ) if atts 648 | "\t<#{ tag }#{ atts }>#{ content }" 649 | end 650 | 651 | alias textile_h1 textile_p 652 | alias textile_h2 textile_p 653 | alias textile_h3 textile_p 654 | alias textile_h4 textile_p 655 | alias textile_h5 textile_p 656 | alias textile_h6 textile_p 657 | 658 | def textile_fn_( tag, num, atts, cite, content ) 659 | atts << " id=\"fn#{ num }\"" 660 | content = "#{ num } #{ content }" 661 | atts = shelve( atts ) if atts 662 | "\t#{ content }

    " 663 | end 664 | 665 | BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m 666 | 667 | def block_textile_prefix( text ) 668 | if text =~ BLOCK_RE 669 | tag,tagpre,num,atts,cite,content = $~[1..6] 670 | atts = pba( atts ) 671 | 672 | # pass to prefix handler 673 | if respond_to? "textile_#{ tag }", true 674 | text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) ) 675 | elsif respond_to? "textile_#{ tagpre }_", true 676 | text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) ) 677 | end 678 | end 679 | end 680 | 681 | SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m 682 | def block_markdown_setext( text ) 683 | if text =~ SETEXT_RE 684 | tag = if $2 == "="; "h1"; else; "h2"; end 685 | blk, cont = "<#{ tag }>#{ $1 }", $' 686 | blocks cont 687 | text.replace( blk + cont ) 688 | end 689 | end 690 | 691 | ATX_RE = /\A(\#{1,6}) # $1 = string of #'s 692 | [ ]* 693 | (.+?) # $2 = Header text 694 | [ ]* 695 | \#* # optional closing #'s (not counted) 696 | $/x 697 | def block_markdown_atx( text ) 698 | if text =~ ATX_RE 699 | tag = "h#{ $1.length }" 700 | blk, cont = "<#{ tag }>#{ $2 }\n\n", $' 701 | blocks cont 702 | text.replace( blk + cont ) 703 | end 704 | end 705 | 706 | MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m 707 | 708 | def block_markdown_bq( text ) 709 | text.gsub!( MARKDOWN_BQ_RE ) do |blk| 710 | blk.gsub!( /^ *> ?/, '' ) 711 | flush_left blk 712 | blocks blk 713 | blk.gsub!( /^(\S)/, "\t\\1" ) 714 | "
    \n#{ blk }\n
    \n\n" 715 | end 716 | end 717 | 718 | MARKDOWN_RULE_RE = /^#{ 719 | ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' ) 720 | }$/ 721 | 722 | def block_markdown_rule( text ) 723 | text.gsub!( MARKDOWN_RULE_RE ) do |blk| 724 | "
    " 725 | end 726 | end 727 | 728 | # XXX TODO XXX 729 | def block_markdown_lists( text ) 730 | end 731 | 732 | def inline_markdown_link( text ) 733 | end 734 | 735 | def inline_textile_span( text ) 736 | QTAGS.each do |qtag_rc, ht, qtag_re, rtype| 737 | text.gsub!( qtag_re ) do |m| 738 | 739 | case rtype 740 | when :limit 741 | sta,qtag,atts,cite,content = $~[1..5] 742 | else 743 | qtag,atts,cite,content = $~[1..4] 744 | sta = '' 745 | end 746 | atts = pba( atts ) 747 | atts << " cite=\"#{ cite }\"" if cite 748 | atts = shelve( atts ) if atts 749 | 750 | "#{ sta }<#{ ht }#{ atts }>#{ content }" 751 | 752 | end 753 | end 754 | end 755 | 756 | LINK_RE = / 757 | ([\s\[{(]|[#{PUNCT}])? # $pre 758 | " # start 759 | (#{C}) # $atts 760 | ([^"]+?) # $text 761 | \s? 762 | (?:\(([^)]+?)\)(?="))? # $title 763 | ": 764 | (\S+?) # $url 765 | (\/)? # $slash 766 | ([^\w\/;]*?) # $post 767 | (?=<|\s|$) 768 | /x 769 | 770 | def inline_textile_link( text ) 771 | text.gsub!( LINK_RE ) do |m| 772 | pre,atts,text,title,url,slash,post = $~[1..7] 773 | 774 | url, url_title = check_refs( url ) 775 | title ||= url_title 776 | 777 | atts = pba( atts ) 778 | atts = " href=\"#{ url }#{ slash }\"#{ atts }" 779 | atts << " title=\"#{ title }\"" if title 780 | atts = shelve( atts ) if atts 781 | 782 | "#{ pre }#{ text }#{ post }" 783 | end 784 | end 785 | 786 | MARKDOWN_REFLINK_RE = / 787 | \[([^\[\]]+)\] # $text 788 | [ ]? # opt. space 789 | (?:\n[ ]*)? # one optional newline followed by spaces 790 | \[(.*?)\] # $id 791 | /x 792 | 793 | def inline_markdown_reflink( text ) 794 | text.gsub!( MARKDOWN_REFLINK_RE ) do |m| 795 | text, id = $~[1..2] 796 | 797 | if id.empty? 798 | url, title = check_refs( text ) 799 | else 800 | url, title = check_refs( id ) 801 | end 802 | 803 | atts = " href=\"#{ url }\"" 804 | atts << " title=\"#{ title }\"" if title 805 | atts = shelve( atts ) 806 | 807 | "#{ text }" 808 | end 809 | end 810 | 811 | MARKDOWN_LINK_RE = / 812 | \[([^\[\]]+)\] # $text 813 | \( # open paren 814 | [ \t]* # opt space 815 | ? # $href 816 | [ \t]* # opt space 817 | (?: # whole title 818 | (['"]) # $quote 819 | (.*?) # $title 820 | \3 # matching quote 821 | )? # title is optional 822 | \) 823 | /x 824 | 825 | def inline_markdown_link( text ) 826 | text.gsub!( MARKDOWN_LINK_RE ) do |m| 827 | text, url, quote, title = $~[1..4] 828 | 829 | atts = " href=\"#{ url }\"" 830 | atts << " title=\"#{ title }\"" if title 831 | atts = shelve( atts ) 832 | 833 | "#{ text }" 834 | end 835 | end 836 | 837 | TEXTILE_REFS_RE = /(^ *)\[([^\n]+?)\](#{HYPERLINK})(?=\s|$)/ 838 | MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m 839 | 840 | def refs( text ) 841 | @rules.each do |rule_name| 842 | method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/ 843 | end 844 | end 845 | 846 | def refs_textile( text ) 847 | text.gsub!( TEXTILE_REFS_RE ) do |m| 848 | flag, url = $~[2..3] 849 | @urlrefs[flag.downcase] = [url, nil] 850 | nil 851 | end 852 | end 853 | 854 | def refs_markdown( text ) 855 | text.gsub!( MARKDOWN_REFS_RE ) do |m| 856 | flag, url = $~[2..3] 857 | title = $~[6] 858 | @urlrefs[flag.downcase] = [url, title] 859 | nil 860 | end 861 | end 862 | 863 | def check_refs( text ) 864 | ret = @urlrefs[text.downcase] if text 865 | ret || [text, nil] 866 | end 867 | 868 | IMAGE_RE = / 869 | (

    |.|^) # start of line? 870 | \! # opening 871 | (\<|\=|\>)? # optional alignment atts 872 | (#{C}) # optional style,class atts 873 | (?:\. )? # optional dot-space 874 | ([^\s(!]+?) # presume this is the src 875 | \s? # optional space 876 | (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title 877 | \! # closing 878 | (?::#{ HYPERLINK })? # optional href 879 | /x 880 | 881 | def inline_textile_image( text ) 882 | text.gsub!( IMAGE_RE ) do |m| 883 | stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8] 884 | atts = pba( atts ) 885 | atts = " src=\"#{ url }\"#{ atts }" 886 | atts << " title=\"#{ title }\"" if title 887 | atts << " alt=\"#{ title }\"" 888 | # size = @getimagesize($url); 889 | # if($size) $atts.= " $size[3]"; 890 | 891 | href, alt_title = check_refs( href ) if href 892 | url, url_title = check_refs( url ) 893 | 894 | out = '' 895 | out << "" if href 896 | out << "" 897 | out << "#{ href_a1 }#{ href_a2 }" if href 898 | 899 | if algn 900 | algn = h_align( algn ) 901 | if stln == "

    " 902 | out = "

    #{ out }" 903 | else 904 | out = "#{ stln }

    #{ out }
    " 905 | end 906 | else 907 | out = stln + out 908 | end 909 | 910 | out 911 | end 912 | end 913 | 914 | def shelve( val ) 915 | @shelf << val 916 | " <#{ @shelf.length }>" 917 | end 918 | 919 | def retrieve( text ) 920 | @shelf.each_with_index do |r, i| 921 | text.gsub!( " <#{ i + 1 }>", r ) 922 | end 923 | end 924 | 925 | def incoming_entities( text ) 926 | ## turn any incoming ampersands into a dummy character for now. 927 | ## This uses a negative lookahead for alphanumerics followed by a semicolon, 928 | ## implying an incoming html entity, to be skipped 929 | 930 | text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) 931 | end 932 | 933 | def no_textile( text ) 934 | text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/, 935 | '\1\2\3' ) 936 | text.gsub!( /^ *==([^=]+.*?)==/m, 937 | '\1\2\3' ) 938 | end 939 | 940 | def clean_white_space( text ) 941 | # normalize line breaks 942 | text.gsub!( /\r\n/, "\n" ) 943 | text.gsub!( /\r/, "\n" ) 944 | text.gsub!( /\t/, ' ' ) 945 | text.gsub!( /^ +$/, '' ) 946 | text.gsub!( /\n{3,}/, "\n\n" ) 947 | text.gsub!( /"$/, "\" " ) 948 | 949 | # if entire document is indented, flush 950 | # to the left side 951 | flush_left text 952 | end 953 | 954 | def flush_left( text ) 955 | indt = 0 956 | if text =~ /^ / 957 | while text !~ /^ {#{indt}}\S/ 958 | indt += 1 959 | end unless text.empty? 960 | if indt.nonzero? 961 | text.gsub!( /^ {#{indt}}/, '' ) 962 | end 963 | end 964 | end 965 | 966 | def footnote_ref( text ) 967 | text.gsub!( /\b\[([0-9]+?)\](\s)?/, 968 | '\1\2' ) 969 | end 970 | 971 | OFFTAGS = /(code|pre|kbd|notextile)/ 972 | OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi 973 | OFFTAG_OPEN = /<#{ OFFTAGS }/ 974 | OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/ 975 | HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m 976 | ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m 977 | 978 | def inline_textile_glyphs( text, level = 0 ) 979 | if text !~ HASTAG_MATCH 980 | pgl text 981 | footnote_ref text 982 | else 983 | codepre = 0 984 | text.gsub!( ALLTAG_MATCH ) do |line| 985 | ## matches are off if we're between ,
     etc.
     986 |                 if $1
     987 |                     if line =~ OFFTAG_OPEN
     988 |                         codepre += 1
     989 |                     elsif line =~ OFFTAG_CLOSE
     990 |                         codepre -= 1
     991 |                         codepre = 0 if codepre < 0
     992 |                     end 
     993 |                 elsif codepre.zero?
     994 |                     inline_textile_glyphs( line, level + 1 )
     995 |                 else
     996 |                     htmlesc( line, :NoQuotes )
     997 |                 end
     998 |                 ## p [level, codepre, orig_line, line]
     999 | 
    1000 |                 line
    1001 |             end
    1002 |         end
    1003 |     end
    1004 | 
    1005 |     def rip_offtags( text )
    1006 |         if text =~ /<.*>/
    1007 |             ## strip and encode 
     content
    1008 |             codepre, used_offtags = 0, {}
    1009 |             text.gsub!( OFFTAG_MATCH ) do |line|
    1010 |                 if $3
    1011 |                     offtag, aftertag = $4, $5
    1012 |                     codepre += 1
    1013 |                     used_offtags[offtag] = true
    1014 |                     if codepre - used_offtags.length > 0
    1015 |                         htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    1016 |                         @pre_list.last << line
    1017 |                         line = ""
    1018 |                     else
    1019 |                         htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
    1020 |                         line = ""
    1021 |                         @pre_list << "#{ $3 }#{ aftertag }"
    1022 |                     end
    1023 |                 elsif $1 and codepre > 0
    1024 |                     if codepre - used_offtags.length > 0
    1025 |                         htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    1026 |                         @pre_list.last << line
    1027 |                         line = ""
    1028 |                     end
    1029 |                     codepre -= 1 unless codepre.zero?
    1030 |                     used_offtags = {} if codepre.zero?
    1031 |                 end 
    1032 |                 line
    1033 |             end
    1034 |         end
    1035 |         text
    1036 |     end
    1037 | 
    1038 |     def smooth_offtags( text )
    1039 |         unless @pre_list.empty?
    1040 |             ## replace 
     content
    1041 |             text.gsub!( // ) { @pre_list[$1.to_i] }
    1042 |         end
    1043 |     end
    1044 | 
    1045 |     def inline( text ) 
    1046 |         @rules.each do |rule_name|
    1047 |             method( rule_name ).call( text ) if rule_name.to_s.match /^inline_/
    1048 |         end
    1049 |     end
    1050 | 
    1051 |     def h_align( text ) 
    1052 |         H_ALGN_VALS[text]
    1053 |     end
    1054 | 
    1055 |     def v_align( text ) 
    1056 |         V_ALGN_VALS[text]
    1057 |     end
    1058 | 
    1059 |     def textile_popup_help( name, windowW, windowH )
    1060 |         ' ' + name + '
    ' 1061 | end 1062 | 1063 | # HTML cleansing stuff 1064 | BASIC_TAGS = { 1065 | 'a' => ['href', 'title'], 1066 | 'img' => ['src', 'alt', 'title'], 1067 | 'br' => [], 1068 | 'i' => nil, 1069 | 'u' => nil, 1070 | 'b' => nil, 1071 | 'pre' => nil, 1072 | 'kbd' => nil, 1073 | 'code' => ['lang'], 1074 | 'cite' => nil, 1075 | 'strong' => nil, 1076 | 'em' => nil, 1077 | 'ins' => nil, 1078 | 'sup' => nil, 1079 | 'sub' => nil, 1080 | 'del' => nil, 1081 | 'table' => nil, 1082 | 'tr' => nil, 1083 | 'td' => ['colspan', 'rowspan'], 1084 | 'th' => nil, 1085 | 'ol' => nil, 1086 | 'ul' => nil, 1087 | 'li' => nil, 1088 | 'p' => nil, 1089 | 'h1' => nil, 1090 | 'h2' => nil, 1091 | 'h3' => nil, 1092 | 'h4' => nil, 1093 | 'h5' => nil, 1094 | 'h6' => nil, 1095 | 'blockquote' => ['cite'] 1096 | } 1097 | 1098 | def clean_html( text, tags = BASIC_TAGS ) 1099 | text.gsub!( /]*)>/ ) do 1101 | raw = $~ 1102 | tag = raw[2].downcase 1103 | if tags.has_key? tag 1104 | pcs = [tag] 1105 | tags[tag].each do |prop| 1106 | ['"', "'", ''].each do |q| 1107 | q2 = ( q != '' ? q : '\s' ) 1108 | if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i 1109 | attrv = $1 1110 | next if prop == 'src' and attrv !~ /^http/ 1111 | pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\"" 1112 | break 1113 | end 1114 | end 1115 | end if tags[tag] 1116 | "<#{raw[1]}#{pcs.join " "}>" 1117 | else 1118 | " " 1119 | end 1120 | end 1121 | end 1122 | end 1123 | 1124 | --------------------------------------------------------------------------------