├── .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 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 | #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("") 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/, '
108 | #
109 | # With a link:
110 | #
111 | # !/common/textist.gif(Textist)!:http://textism.com
112 | #
113 | # Will become:
114 | #
115 | #
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 | # #=>"#{ 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#{ 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" 644 | end 645 | 646 | def textile_p( tag, atts, cite, content ) 647 | atts = shelve( atts ) if atts 648 | "\t<#{ tag }#{ atts }>#{ content }#{ tag }>" 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 }
\n\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 }#{ tag }>", $' 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 }#{ tag }>\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 | "
|.|^) # 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 }
, 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 |
--------------------------------------------------------------------------------