wunderbar 0.8.5 → 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest CHANGED
@@ -1,6 +1,8 @@
1
1
  COPYING
2
2
  README
3
3
  Rakefile
4
+ demo/wiki.html
5
+ demo/wiki.rb
4
6
  lib/wunderbar.rb
5
7
  lib/wunderbar/builder.rb
6
8
  lib/wunderbar/cgi-methods.rb
@@ -13,5 +15,5 @@ lib/wunderbar/version.rb
13
15
  test/test_builder.rb
14
16
  test/test_html_markup.rb
15
17
  test/test_logger.rb
16
- wunderbar.gemspec
18
+ tools/web2script.rb
17
19
  Manifest
data/README CHANGED
@@ -47,7 +47,6 @@ number of other convenience methods are defined:
47
47
  * $cgi - Common Gateway Interface
48
48
  * $param - Access to parameters (read-only OpenStruct like interface)
49
49
  * $env - Access to environment variables (read-only OpenStruct like interface)
50
- * $x - XmlBuilder instance
51
50
  * $USER - Host user id
52
51
  * $HOME - Home directory
53
52
  * $SERVER- Server name
data/demo/wiki.html ADDED
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title>Wunderbar Wiki</title>
6
+ </head>
7
+ <body>
8
+ <h2>Wunderbar Wiki</h2>
9
+
10
+ <p>Note: demo code requires Ruby 1.9.2 or later. Wunderbar itself works with
11
+ Ruby 1.8.7 or later.</p>
12
+
13
+ <p>Prereqs:</p>
14
+
15
+ <ul>
16
+ <li><code>gem install wunderbar rdiscount</code></li>
17
+ <li><code>apt-get install git</code></li>
18
+ <li><a href="http://jquery.com/">jQuery</a></li>
19
+ <li><a href="http://www.showdown.im/">Showdown</a></li>
20
+ </ul>
21
+
22
+
23
+ <p>Create a data directory (one per wiki):</p>
24
+
25
+ <ul>
26
+ <li>Must be writeable by your web server</li>
27
+ <li>Should not be in the Document Root</li>
28
+ </ul>
29
+
30
+
31
+ <p>Configure git:</p>
32
+
33
+ <ul>
34
+ <li>Follow <a
35
+ href="http://help.github.com/set-your-user-name-email-and-github-token/]">Step
36
+ 1</a>.</li>
37
+ <li>Note that this needs to be done for the user that the webserver runs the
38
+ script under. If you are not running <a
39
+ href="http://httpd.apache.org/docs/2.0/suexec.html">suexec</a>, this is
40
+ typically <code>www-data</code> on Unix or <code>www</code> on Mac OS/X.</li>
41
+ </ul>
42
+
43
+
44
+ <p>Configure Apache:</p>
45
+
46
+ <pre><code> Options +ExecCGI +MultiViews
47
+ MultiViewsMatch Any
48
+ AddHandler cgi-script .cgi
49
+ </code></pre>
50
+
51
+ <p>Install scripts:</p>
52
+
53
+ <ul>
54
+ <li>Copy <code>jquery-min.js</code> and <code>showdown.js</code> into your
55
+ document root</li>
56
+ </ul>
57
+
58
+
59
+ <p>Install wiki (one per wiki):</p>
60
+
61
+ <pre><code> ruby wiki.rb --install=/var/www/wikiname.cgi --WIKIDATA="/full/path/to/data/directory"
62
+ </code></pre>
63
+
64
+ <p>Access your wiki:</p>
65
+
66
+ <pre><code> http://localhost/wikiname/
67
+ </code>
68
+ </body>
69
+ </html>
data/demo/wiki.rb ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/ruby
2
+ require 'wunderbar'
3
+ require 'rdiscount'
4
+ require 'shellwords'
5
+ require 'digest/md5'
6
+
7
+ Dir.chdir WIKIDATA
8
+
9
+ # parse request
10
+ %r{/(?<file>.\w+)((?<flag>/)(?<rev>\w*))?$} =~ $env.PATH_INFO
11
+ flag ||= '?' if $env.REQUEST_URI.include? '?'
12
+ file ||= 'index'
13
+
14
+ Wunderbar.html do
15
+ _head do
16
+ _title file
17
+ _style %{
18
+ body {background-color: #{(flag=='?') ? '#E0D8D8' : '#FFF'}}
19
+ .status {border: 2px solid #000; border-radius: 1em; background: #FAFAD2; padding: 0.5em}
20
+ .input, .output {border: 1px solid #888; position: relative; width: 47.5%; height: 400px; overflow: auto}
21
+ .input {float: left; left: 1.5%}
22
+ .output {float: right; right: 1.5%; background-color: #6C6666; color: #FFF}
23
+ .buttons {clear: both; text-align: center; padding-top: 0.5em}
24
+ .message {position: fixed; left: 2%; color: #9400d3}
25
+ h1 {text-align: center; margin: 0}
26
+ form {clear: both}
27
+ .buttons form {display: inline}
28
+ ._stdin:before {content: '$ '}
29
+ ._stdin {color: #9400D3; margin-bottom: 0}
30
+ ._stdout {margin: 0}
31
+ ._stderr {margin: 0; color: red}
32
+ }
33
+ _script src: '/showdown.js'
34
+ _script src: '/jquery.min.js'
35
+ end
36
+
37
+ _body? do
38
+
39
+ # determine markup
40
+ if _.post? and @markup
41
+ _header class: 'status' do
42
+ _h1 'Status'
43
+ if File.exist?(file) and Digest::MD5.hexdigest(File.read(file)) != @hash
44
+ _p 'Write conflict'
45
+ else
46
+ File.open(file, 'w') {|fh| fh.write @markup}
47
+ _.system 'git init' unless Dir.exist? '.git'
48
+ if `git status --porcelain #{file}`.empty?
49
+ _p 'Nothing changed'
50
+ else
51
+ _.system "git add #{file}"
52
+ _.system "git commit -m #{@comment.shellescape} #{file}"
53
+ end
54
+ end
55
+ end
56
+
57
+ elsif File.exist? file
58
+ # existing file
59
+ if !rev or rev.empty?
60
+ @markup = File.read(file)
61
+ else
62
+ @markup = `git show #{rev}:#{file}`
63
+ flag = nil
64
+ end
65
+
66
+ else
67
+ # new file: go directly into edit mode
68
+ @markup = "#{file}\n#{'-'*file.length}\n\nEnter your text here..."
69
+ flag = '?'
70
+ end
71
+
72
+ # produce HTML
73
+ if file == '_index'
74
+
75
+ # index
76
+ index = Hash[`git ls-tree HEAD --name-only`.scan(/(\w+)()/)].
77
+ merge Hash[*`git status --porcelain`.scan(/(..) (\w+)/).flatten.reverse]
78
+ _table do
79
+ _tbody do
80
+ index.sort.each do |name, status|
81
+ _tr do
82
+ _td status
83
+ _td {_a name, href: name}
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ elsif flag == '?'
90
+
91
+ # edit mode
92
+ _header do
93
+ _h1 "~~~ #{file} ~~~"
94
+ _span 'Input', style: 'float: left; margin-left: 2em'
95
+ _span 'Output', style: 'float: right; margin-right: 2em'
96
+ end
97
+
98
+ _form action: file, method: 'post' do
99
+ _textarea @markup, name: 'markup', class: 'input'
100
+ _input type: 'hidden', name: 'hash',
101
+ value: Digest::MD5.hexdigest(@markup)
102
+ _div class: 'output' do
103
+ _ << RDiscount.new(@markup).to_html
104
+ end
105
+
106
+ _div class: 'buttons' do
107
+ _span class: 'message'
108
+ _input name: 'comment', placeholder: 'commit message'
109
+ _input type: 'submit', value: 'save'
110
+ end
111
+ end
112
+
113
+ elsif flag == '/'
114
+
115
+ # revision history
116
+ _h2 "Revision history for #{file}"
117
+ _ul do
118
+ `git log --format="%H|%ai|%an|%s" #{file}`.lines.each do |line|
119
+ hash, date, author, subject = line.split('|')
120
+ _li! {_a date, href: hash; _ " #{subject} by #{author}"}
121
+ end
122
+ end
123
+
124
+ else
125
+
126
+ #display
127
+ _ << RDiscount.new(@markup).to_html
128
+ _div class: 'buttons' do
129
+ if !rev or rev.empty?
130
+ _form action: "#{file}?", method: 'post' do
131
+ _input type: 'submit', value: 'edit'
132
+ end
133
+ end
134
+ _form action: "#{file}/" do
135
+ _input type: 'submit', value: 'history'
136
+ end
137
+ end
138
+ end
139
+
140
+ _script %{
141
+ // autosave every 10 seconds
142
+ var dirty = false;
143
+ setInterval(function() {
144
+ if (!dirty) return;
145
+ dirty = false;
146
+
147
+ var params = {
148
+ markup: $('textarea[name=markup]').val(),
149
+ hash: $('input[name=hash]').val()
150
+ };
151
+
152
+ $.getJSON("#{SELF}", params, function(_) {
153
+ $('input[name=hash]').val(_.hash);
154
+ if (_.time) {
155
+ var time = new Date(_.time).toLocaleTimeString();
156
+ $('.message').text("Autosaved at " + time).show().fadeOut(5000);
157
+ } else {
158
+ $('.input').val(_.markup).attr('readonly', 'readonly');
159
+ $('.message').css({'font-weight': 'bold'}).text(_.error).show();
160
+ }
161
+ });
162
+ }, 10000);
163
+
164
+ // regenerate output every 0.5 seconds
165
+ var updated = false;
166
+ setInterval(function() {
167
+ if (!updated) return;
168
+ updated = false;
169
+ $('.output').html(converter.makeHtml($('.input').val()));
170
+ }, 500);
171
+
172
+ // update output pane and mark dirty whenever input changes
173
+ var converter = new Showdown.converter();
174
+ $('.input').bind('input', function() {
175
+ updated = dirty = true;
176
+ }).trigger('input');
177
+
178
+ // resize based on window size
179
+ var reserve = $('header').height() * 3 + $('.buttons').height();
180
+ $(window).resize(function() {
181
+ $('.input,.output').height($(window).height()-reserve);
182
+ }).trigger('resize');
183
+ }
184
+ end
185
+ end
186
+
187
+ # process autosave requests
188
+ Wunderbar.json do
189
+ hash = Digest::MD5.hexdigest(@markup)
190
+ if File.exist?(file) and Digest::MD5.hexdigest(File.read(file)) != @hash
191
+ {error: "Write conflict", markup: File.read(file), hash: hash}
192
+ else
193
+ File.open(file, 'w') {|fh| fh.write @markup} unless @hash == hash
194
+ {time: Time.now.to_i*1000, hash: hash}
195
+ end
196
+ end
197
+
198
+ __END__
199
+ # Customize where the wiki data is stored
200
+ WIKIDATA = '/full/path/to/data/directory'
@@ -1,107 +1,149 @@
1
- # add indented_text!, indented_data! and traceback! methods to builder
2
- module Builder
3
- class XmlMarkup
4
- unless method_defined? :indented_text!
5
- def indented_text!(text)
6
- indented_data!(text) {|data| text! data}
7
- end
1
+ module Wunderbar
2
+ # XmlMarkup handles indentation of elements beautifully, this class extends
3
+ # that support to text, data, and spacing between elements
4
+ class SpacedMarkup < Builder::XmlMarkup
5
+ def indented_text!(text)
6
+ indented_data!(text) {|data| text! data}
8
7
  end
9
8
 
10
- unless method_defined? :indented_data!
11
- def indented_data!(data, &block)
12
- return if data.strip.length == 0
9
+ def indented_data!(data, &block)
10
+ return if data.strip.length == 0
13
11
 
14
- if @indent > 0
15
- data.sub! /\n\s*\Z/, ''
16
- data.sub! /\A\s*\n/, ''
12
+ if @indent > 0
13
+ data.sub! /\n\s*\Z/, ''
14
+ data.sub! /\A\s*\n/, ''
17
15
 
18
- unindent = data.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min || 1
16
+ unindent = data.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min || 1
19
17
 
20
- before = ::Regexp.new('^'.ljust(unindent))
21
- after = " " * (@level * @indent)
22
- data.gsub! before, after
23
- end
18
+ before = ::Regexp.new('^'.ljust(unindent))
19
+ after = " " * (@level * @indent)
20
+ data.gsub! before, after
24
21
 
25
- if block
26
- block.call(data)
27
- else
28
- self << data
29
- end
22
+ _newline if @pending_newline and not @first_tag
23
+ @pending_newline = @pending_margin
24
+ @first_tag = @pending_margin = false
25
+ end
30
26
 
31
- _newline unless data =~ /\n\Z/
27
+ if block
28
+ block.call(data)
29
+ else
30
+ self << data
32
31
  end
32
+
33
+ _newline unless data =~ /\n\Z/
33
34
  end
34
35
 
35
- unless method_defined? :traceback!
36
- def traceback!(exception=$!, klass='traceback')
37
- pre :class=>klass do
38
- text! exception.inspect
39
- _newline
40
- exception.backtrace.each {|frame| text!((' '*@indent)+frame + "\n")}
41
- end
42
- end
36
+ def disable_indendation!(&block)
37
+ indent, level = @indent, @level
38
+ @indent = @level = 0
39
+ text! " "*indent*level
40
+ block.call
41
+ ensure
42
+ text! "\n"
43
+ @indent, @level = indent, level
43
44
  end
44
- end
45
- end
46
45
 
47
- # monkey patch to ensure that tags are closed
48
- test =
49
- Builder::XmlMarkup.new.html do |x|
50
- x.body do
51
- begin
52
- x.p do
53
- raise Exception.new('boom')
54
- end
55
- rescue Exception => e
56
- x.pre e
57
- end
46
+ def _margin
47
+ _newline unless @first_tag
48
+ @pending_newline = false
49
+ @pending_margin = true
50
+ end
51
+
52
+ def _nested_structures(*args)
53
+ pending_newline = @pending_newline
54
+ @pending_newline = false
55
+ @first_tag = true
56
+ super
57
+ @first_tag = @pending_margin = false
58
+ @pending_newline = pending_newline
59
+ end
60
+
61
+ def tag!(*args)
62
+ _newline if @pending_newline
63
+ @pending_newline = @pending_margin
64
+ @first_tag = @pending_margin = false
65
+ super
58
66
  end
59
67
  end
60
68
 
61
- if test.index('<p>') and !test.index('</p>')
62
- module Builder
63
- class XmlMarkup
64
- def method_missing(sym, *args, &block)
65
- text = nil
66
- attrs = nil
67
- sym = "#{sym}:#{args.shift}" if args.first.kind_of?(Symbol)
68
- args.each do |arg|
69
- case arg
70
- when Hash
71
- attrs ||= {}
72
- attrs.merge!(arg)
73
- else
74
- text ||= ''
75
- text << arg.to_s
76
- end
77
- end
78
- if block
79
- unless text.nil?
80
- raise ArgumentError, "XmlMarkup cannot mix a text argument with a block"
69
+ class XmlMarkup
70
+ def initialize(*args)
71
+ @x = SpacedMarkup.new(*args)
72
+ end
73
+
74
+ # forward to either Wunderbar or XmlMarkup
75
+ def method_missing(method, *args, &block)
76
+ if Wunderbar.respond_to? method
77
+ Wunderbar.send method, *args, &block
78
+ elsif SpacedMarkup.public_instance_methods.include? method
79
+ @x.__send__ method, *args, &block
80
+ elsif SpacedMarkup.public_instance_methods.include? method.to_s
81
+ @x.__send__ method, *args, &block
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ # avoid method_missing overhead for the most common case
88
+ def tag!(*args, &block)
89
+ @x.tag! *args, &block
90
+ end
91
+
92
+ # execute a system command, echoing stdin, stdout, and stderr
93
+ def system(command, opts={})
94
+ require 'open3'
95
+ tag = opts[:tag] || 'pre'
96
+ output_class = opts[:class] || {}
97
+ stdin = output_class[:stdin] || '_stdin'
98
+ stdout = output_class[:stdout] || '_stdout'
99
+ stderr = output_class[:stderr] || '_stderr'
100
+
101
+ @x.tag! tag, command, :class=>stdin unless opts[:echo] == false
102
+
103
+ require 'thread'
104
+ semaphore = Mutex.new
105
+ Open3.popen3(command) do |pin, pout, perr|
106
+ [
107
+ Thread.new do
108
+ until pout.eof?
109
+ out_line = pout.readline.chomp
110
+ semaphore.synchronize { @x.tag! tag, out_line, :class=>stdout }
111
+ end
112
+ end,
113
+
114
+ Thread.new do
115
+ until perr.eof?
116
+ err_line = perr.readline.chomp
117
+ semaphore.synchronize { @x.tag! tag, err_line, :class=>stderr }
118
+ end
119
+ end,
120
+
121
+ Thread.new do
122
+ if opts[:stdin].respond_to? :read
123
+ require 'fileutils'
124
+ FileUtils.copy_stream opts[:stdin], pin
125
+ elsif opts[:stdin]
126
+ pin.write opts[:stdin].to_s
127
+ end
128
+ pin.close
81
129
  end
82
- _indent
83
- _start_tag(sym, attrs)
84
- _newline
85
- begin ### Added
86
- _nested_structures(block)
87
- ensure ### Added
88
- _indent
89
- _end_tag(sym)
90
- _newline
91
- end ### Added
92
- elsif text.nil?
93
- _indent
94
- _start_tag(sym, attrs, true)
95
- _newline
96
- else
97
- _indent
98
- _start_tag(sym, attrs)
99
- text! text
100
- _end_tag(sym)
101
- _newline
102
- end
103
- @target
130
+ ].each {|thread| thread.join}
104
131
  end
105
132
  end
133
+
134
+ # declaration (DOCTYPE, etc)
135
+ def declare(*args)
136
+ @x.declare!(*args)
137
+ end
138
+
139
+ # comment
140
+ def comment(*args)
141
+ @x.comment! *args
142
+ end
143
+
144
+ # was this invoked via HTTP POST?
145
+ def post?
146
+ $HTTP_POST
147
+ end
106
148
  end
107
149
  end
@@ -83,28 +83,23 @@ def $cgi.out?(headers, &block)
83
83
  end
84
84
 
85
85
  # produce html/xhtml
86
- def $cgi.html(&block)
86
+ def $cgi.html(*args, &block)
87
87
  return if $XHR_JSON or $TEXT
88
+ args.push {} if args.empty?
89
+ args.first[:xmlns] ||= 'http://www.w3.org/1999/xhtml' if Hash === args.first
90
+ mimetype = ($XHTML ? 'application/xhtml+xml' : 'text/html')
88
91
  x = HtmlMarkup.new
89
- if $XHTML
90
- $cgi.out? 'type' => 'application/xhtml+xml', 'charset' => 'UTF-8' do
91
- x._! "\xEF\xBB\xBF"
92
- x.declare! :DOCTYPE, :html
93
- x.html :xmlns => 'http://www.w3.org/1999/xhtml', &block
94
- end
95
- else
96
- $cgi.out? 'type' => 'text/html', 'charset' => 'UTF-8' do
97
- x._! "\xEF\xBB\xBF"
98
- x.declare! :DOCTYPE, :html
99
- x.html &block
100
- end
92
+ $cgi.out? 'type' => mimetype, 'charset' => 'UTF-8' do
93
+ x._! "\xEF\xBB\xBF"
94
+ x._.declare :DOCTYPE, :html
95
+ x.html *args, &block
101
96
  end
102
97
  end
103
98
 
104
99
  # produce html and quit
105
- def $cgi.html! &block
100
+ def $cgi.html! *args, &block
106
101
  return if $XHR_JSON or $TEXT
107
- html(&block)
102
+ html(*args, &block)
108
103
  Process.exit
109
104
  end
110
105
 
@@ -12,7 +12,6 @@ ARGV.delete('--prompt') or ARGV.delete('--offline')
12
12
  # standard objects
13
13
  $cgi = CGI.new
14
14
  $param = $cgi.params
15
- $x = Builder::XmlMarkup.new :indent => 2
16
15
 
17
16
  # implied request types
18
17
  $HTTP_GET ||= ($cgi.request_method == 'GET')
@@ -1,49 +1,3 @@
1
- # execute a system command, echoing stdin, stdout, and stderr
2
- def $x.system(command, opts={})
3
- ::Kernel.require 'open3'
4
- output_class = opts[:class] || {}
5
- stdin = output_class[:stdin] || '_stdin'
6
- stdout = output_class[:stdout] || '_stdout'
7
- stderr = output_class[:stderr] || '_stderr'
8
-
9
- $x.pre command, :class=>stdin unless opts[:echo] == false
10
-
11
- ::Kernel.require 'thread'
12
- semaphore = ::Mutex.new
13
- ::Open3.popen3(command) do |pin, pout, perr|
14
- [
15
- ::Thread.new do
16
- until pout.eof?
17
- out_line = pout.readline.chomp
18
- semaphore.synchronize { $x.pre out_line, :class=>stdout }
19
- end
20
- end,
21
-
22
- ::Thread.new do
23
- until perr.eof?
24
- err_line = perr.readline.chomp
25
- semaphore.synchronize { $x.pre err_line, :class=>stderr }
26
- end
27
- end,
28
-
29
- ::Thread.new do
30
- if opts[:stdin].respond_to? :read
31
- require 'fileutils'
32
- FileUtils.copy_stream opts[:stdin], pin
33
- elsif opts[:stdin]
34
- pin.write opts[:stdin].to_s
35
- end
36
- pin.close
37
- end
38
- ].each {|thread| thread.join}
39
- end
40
- end
41
-
42
- # was this invoked via HTTP POST?
43
- def $x.post?
44
- $HTTP_POST
45
- end
46
-
47
1
  # Wrapper class that understands HTML
48
2
  class HtmlMarkup
49
3
  VOID = %w(
@@ -52,14 +6,11 @@ class HtmlMarkup
52
6
  )
53
7
 
54
8
  def initialize(*args, &block)
55
- # as a migration aide, use the global variable, but consider that
56
- # to be deprecated.
57
- $x ||= Builder::XmlMarkup.new :indent => 2
58
- @x = $x
9
+ @x = Wunderbar::XmlMarkup.new :indent => 2
59
10
  end
60
11
 
61
12
  def html(*args, &block)
62
- @x.html(*args) do
13
+ @x.tag! :html, *args do
63
14
  $param.each do |key,value|
64
15
  instance_variable_set "@#{key}", value.first if key =~ /^\w+$/
65
16
  end
@@ -76,6 +27,10 @@ class HtmlMarkup
76
27
  raise error
77
28
  end
78
29
 
30
+ if name.sub!(/_$/,'')
31
+ @x._margin
32
+ end
33
+
79
34
  if flag != '!'
80
35
  if %w(script style).include?(name)
81
36
  if String === args.first and not block
@@ -104,15 +59,8 @@ class HtmlMarkup
104
59
  end
105
60
 
106
61
  if flag == '!'
107
- # turn off indentation
108
- indent, level = @x.instance_eval { [@indent, @level] }
109
- begin
110
- @x.instance_eval { [@indent=0, @level=0] }
111
- @x.text! " "*indent*level
62
+ @x.disable_indendation! do
112
63
  @x.tag! name, *args, &block
113
- ensure
114
- @x.text! "\n"
115
- @x.instance_eval { [@indent=indent, @level=level] }
116
64
  end
117
65
  elsif flag == '?'
118
66
  # capture exceptions, produce filtered tracebacks
@@ -135,9 +83,9 @@ class HtmlMarkup
135
83
  end
136
84
 
137
85
  if traceback_class
138
- $x.pre text, :class=>traceback_class
86
+ @x.tag! :pre, text, :class=>traceback_class
139
87
  else
140
- $x.pre text, :style=>traceback_style
88
+ @x.tag! :pre, text, :style=>traceback_style
141
89
  end
142
90
  end
143
91
  end
@@ -148,11 +96,25 @@ class HtmlMarkup
148
96
 
149
97
  def _head(*args, &block)
150
98
  @x.tag!('head', *args) do
151
- @x.meta :charset => 'utf-8' unless $XHTML
99
+ @x.tag! :meta, :charset => 'utf-8' unless $XHTML
152
100
  block.call if block
153
101
  end
154
102
  end
155
103
 
104
+ def _svg(*args, &block)
105
+ args << {} if args.empty?
106
+ args.first['xmlns'] = 'http://www.w3.org/2000/svg' if Hash === args.first
107
+ @x.tag! :svg, *args, &block
108
+ end
109
+
110
+ def _math(*args, &block)
111
+ args << {} if args.empty?
112
+ if Hash === args.first
113
+ args.first['xmlns'] = 'http://www.w3.org/1998/Math/MathML'
114
+ end
115
+ @x.tag! :math, *args, &block
116
+ end
117
+
156
118
  def _(text=nil)
157
119
  @x.indented_text! text if text
158
120
  @x
@@ -163,10 +125,6 @@ class HtmlMarkup
163
125
  @x
164
126
  end
165
127
 
166
- def declare!(*args)
167
- @x.declare!(*args)
168
- end
169
-
170
128
  def _coffeescript(text)
171
129
  require 'coffee-script'
172
130
  _script CoffeeScript.compile(text)
@@ -2,7 +2,7 @@ module Wunderbar
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 8
5
- TINY = 5
5
+ TINY = 6
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/test/test_builder.rb CHANGED
@@ -4,20 +4,32 @@ require 'wunderbar'
4
4
 
5
5
  class BuilderTest < Test::Unit::TestCase
6
6
  def test_empty
7
- x = Builder::XmlMarkup.new :indent => 2
8
- x.script { x.indented_text! '' }
7
+ x = Wunderbar::XmlMarkup.new :indent => 2
8
+ x.tag!(:script) { x.indented_text! '' }
9
9
  assert_equal %{<script>\n</script>\n}, x.target!
10
10
  end
11
11
 
12
12
  def test_unindented_input
13
- x = Builder::XmlMarkup.new :indent => 2
14
- x.script { x.indented_text! "{\n x: 1\n}" }
13
+ x = Wunderbar::XmlMarkup.new :indent => 2
14
+ x.tag!(:script) { x.indented_text! "{\n x: 1\n}" }
15
15
  assert_equal %{<script>\n {\n x: 1\n }\n</script>\n}, x.target!
16
16
  end
17
17
 
18
18
  def test_indented_input
19
- x = Builder::XmlMarkup.new :indent => 2
20
- x.script { x.indented_text! " alert('danger');" }
19
+ x = Wunderbar::XmlMarkup.new :indent => 2
20
+ x.tag!(:script) { x.indented_text! " alert('danger');" }
21
21
  assert_equal %{<script>\n alert('danger');\n</script>\n}, x.target!
22
22
  end
23
+
24
+ def test_exception
25
+ x = Wunderbar::XmlMarkup.new :indent => 2
26
+ x.tag!(:body) do
27
+ begin
28
+ x.tag!(:p) { raise Exception.new('boom') }
29
+ rescue Exception => e
30
+ x.tag!(:pre, e)
31
+ end
32
+ end
33
+ assert x.target!.include? '<p>' and x.target!.include? '</p>'
34
+ end
23
35
  end
@@ -4,7 +4,6 @@ require 'wunderbar'
4
4
 
5
5
  class HtmlMarkupTest < Test::Unit::TestCase
6
6
  def setup
7
- $x = nil # until this hack is removed html-methods.rb
8
7
  @original_log_level = Wunderbar.logger.level
9
8
  Wunderbar.log_level = :fatal
10
9
  end
@@ -75,6 +74,20 @@ class HtmlMarkupTest < Test::Unit::TestCase
75
74
  assert_match %r[<div>one <strong>two</strong> three</div>], x.target!
76
75
  end
77
76
 
77
+ def test_spaced_embedded
78
+ x = HtmlMarkup.new
79
+ x.html {_div {_p 'one'; _hr_; _p 'two'}}
80
+ assert_match %r[<div>\n +<p>one</p>\n\n +<hr/>\n\n +<p>two</p>\n +</div>],
81
+ x.target!
82
+ end
83
+
84
+ def test_spaced_collapsed
85
+ x = HtmlMarkup.new
86
+ x.html {_div {_p_ 'one'; _hr_; _p_ 'two'}}
87
+ assert_match %r[<div>\n +<p>one</p>\n\n +<hr/>\n\n +<p>two</p>\n +</div>],
88
+ x.target!
89
+ end
90
+
78
91
  def test_traceback
79
92
  x = HtmlMarkup.new
80
93
  x.html {_body? {boom}}
@@ -120,10 +133,28 @@ class HtmlMarkupTest < Test::Unit::TestCase
120
133
 
121
134
  def test_declare
122
135
  x = HtmlMarkup.new
123
- x.declare! :DOCTYPE, 'html'
136
+ x._.declare :DOCTYPE, 'html'
124
137
  assert_equal %{<!DOCTYPE "html">\n}, x.target!
125
138
  end
126
139
 
140
+ def test_comment
141
+ x = HtmlMarkup.new
142
+ x._.comment 'foo'
143
+ assert_equal %{<!-- foo -->\n}, x.target!
144
+ end
145
+
146
+ def test_svg
147
+ x = HtmlMarkup.new
148
+ x.html {_svg}
149
+ assert_match %r[^ <svg xmlns="http://www.w3.org/2000/svg"/>], x.target!
150
+ end
151
+
152
+ def test_math
153
+ x = HtmlMarkup.new
154
+ x.html {_math}
155
+ assert_match %r[^ <math xmlns="http://www.w3.org/1998/Math/MathML"/>], x.target!
156
+ end
157
+
127
158
  begin
128
159
  require 'coffee-script'
129
160
 
@@ -0,0 +1,213 @@
1
+ require 'net/http'
2
+ require 'rubygems'
3
+ require 'nokogiri'
4
+ require 'optparse'
5
+
6
+ # Convert a webpage to a Wunderbar script
7
+
8
+ OptionParser.new { |opts|
9
+ opts.banner = "#{File.basename(__FILE__)} [-o output] [-w width] URLs..."
10
+ opts.on '-o', '--output FILE', 'Send Output to FILE' do |file|
11
+ $stdout = File.open(file, 'w')
12
+ end
13
+ opts.on '-w', '--width WIDTH', Integer, 'Set line width' do |width|
14
+ $width = width
15
+ end
16
+ opts.on '-g', '--group lines', Integer,
17
+ 'Insert blanks lines around blocks larger than this value' do |group|
18
+ $group = group
19
+ end
20
+ if ''.respond_to? 'encoding'
21
+ opts.on '-a', '--ascii', Integer, 'Escape non-ASCII characters' do
22
+ $ascii = true
23
+ end
24
+ end
25
+ }.parse!
26
+
27
+ # Method to "enquote" a string
28
+ class String
29
+ def enquote
30
+ if $ascii
31
+ inspect.gsub(/[^\x20-\x7f]/) { |c| '\u' + c.ord.to_s(16).rjust(4,'0') }
32
+ else
33
+ inspect
34
+ end
35
+ end
36
+ end
37
+
38
+ # queue of lines to be output
39
+ $q = []
40
+ def q line
41
+ $q << line
42
+ end
43
+
44
+ def flow_text(line, join)
45
+ while $width and line.length>$width
46
+ line.sub! /(.{1,#{$width-4}})(\s+|\Z)/, "\\1 #{join}"
47
+ break unless line.include? "\n"
48
+ q line.split("\n").first
49
+ line = line[/\n(.*)/,1]
50
+ end
51
+ q line
52
+ end
53
+
54
+ def flow_attrs(line, attributes, indent)
55
+ attributes.each do |attribute|
56
+ line += ','
57
+ if $width and (line+attribute).length > $width-1
58
+ q line
59
+ line = "#{indent} "
60
+ end
61
+ line += attribute
62
+ end
63
+ q line
64
+ end
65
+
66
+ def code(element, indent='')
67
+ attributes = []
68
+ element.attributes.keys.each do |key|
69
+ value = element[key]
70
+
71
+ # resolve relative links
72
+ if %w(a img link).include? element.name and %w(href src).include? key
73
+ value = ($uri + value).to_s rescue nil
74
+ end
75
+
76
+ if key =~ /^\w+$/
77
+ if key == 'xmlns' and %w(html svg mathml).include? element.name
78
+ # drop xmlns attributes from these elements
79
+ elsif key == 'type' and element.name == 'style' and value == 'text/css'
80
+ # drop type attributes from script elements
81
+ elsif key == 'type' and element.name == 'script' and value == 'text/javascript'
82
+ # drop type attributes from script elements
83
+ elsif RUBY_VERSION =~ /^1\.8/
84
+ attributes << " :#{key} => #{value.enquote}"
85
+ else
86
+ attributes << " #{key}: #{value.enquote}"
87
+ end
88
+ else
89
+ attributes << " #{key.enquote} => #{value.enquote}"
90
+ end
91
+ end
92
+
93
+ # restore namespaces that Nokogiri::HTML dropped
94
+ element_name = element.name
95
+ if $namespaced[element.name]
96
+ element_name = $namespaced[element.name]
97
+ element_name += ',' unless attributes.empty?
98
+ end
99
+
100
+ line = "#{indent}_#{element_name}#{attributes.join(',')}"
101
+ line.sub! /^_/, 'Wunderbar.' if element_name == 'html' and indent.empty?
102
+
103
+ if element.children.empty?
104
+ flow_attrs "#{indent}_#{element_name}#{attributes.pop}", attributes, indent
105
+
106
+ # element has children
107
+ elsif element.children.any? {|child| child.element?}
108
+ # do any of the text nodes need special processing to preserve spacing?
109
+ flatten = false
110
+ space = true
111
+ if element.children.any? {|child| child.text? and !child.text.strip.empty?}
112
+ element.children.each do |child|
113
+ if child.text? or child.element?
114
+ next if child.text == ''
115
+ flatten = true if not space and not child.text =~ /\A\s/
116
+ space = (child.text =~ /\s\Z/)
117
+ end
118
+ end
119
+ end
120
+ line.sub!(/(\w)( |$)/, '\1!\2') if flatten
121
+
122
+ q "#{line} do"
123
+
124
+ start = $q.length
125
+ blank = false
126
+ first = true
127
+
128
+ # recursively process children
129
+ element.children.each do |child|
130
+ if child.text?
131
+ text = child.text.gsub(/\s+/, ' ')
132
+ text = text.strip unless flatten
133
+ next if text.empty?
134
+ flow_text "#{indent} _ #{text.enquote}", "\" +\n #{indent}\""
135
+ elsif child.comment?
136
+ flow_text "#{indent} _.comment #{child.text.strip.enquote}",
137
+ "\" +\n #{indent}\""
138
+ else
139
+ code(child, indent + ' ')
140
+ end
141
+
142
+ # insert a blank line if either this or the previous block was large
143
+ if $group and start + $group < $q.length
144
+ $q[start].sub! /^(\s+_\w+) /, '\1_ \2'
145
+ $q.insert(start,'') if not first
146
+ blank = true
147
+ else
148
+ $q.insert(start,'') if blank
149
+ blank = false
150
+ end
151
+ start = $q.length
152
+ first = false
153
+ end
154
+ q indent + "end"
155
+
156
+ elsif element.name == 'pre' and element.text.include? "\n"
157
+ data = element.text.sub(/\A\n/,'').sub(/\s+\Z/,'')
158
+
159
+ unindent = data.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min || 1
160
+ before = Regexp.new('^'.ljust(unindent))
161
+ after = "#{indent} "
162
+ data.gsub! before, after
163
+
164
+ flow_attrs "#{indent}_pre <<-EOD.gsub(/^\\s{#{after.length}}/,'')",
165
+ attributes, indent
166
+ data.split("\n").each { |line| q line }
167
+ q "#{indent}EOD"
168
+
169
+ # element has text but no attributes or children
170
+ elsif attributes.empty?
171
+ if %w(script style).include? element.name and element.text.include? "\n"
172
+ script = element.text.sub(/\A\n/,'').sub(/\s+\Z/,'')
173
+
174
+ unindent = script.sub(/s+\Z/,'').scan(/^ *\S/).map(&:length).min || 1
175
+ before = Regexp.new('^'.ljust(unindent))
176
+ after = "#{indent} "
177
+ script.gsub! before, after
178
+
179
+ q "#{line} %{"
180
+ script.split("\n").each { |line| q line }
181
+ q "#{indent}}"
182
+ else
183
+ flow_text "#{line} #{element.text.enquote}", "\" +\n #{indent}\""
184
+ end
185
+
186
+ # element has text and attributes but no children
187
+ else
188
+ flow_attrs "#{indent}_#{element_name} #{element.text.enquote}",
189
+ attributes, indent
190
+ end
191
+ end
192
+
193
+ # fetch and convert each web page
194
+ ARGV.each do |arg|
195
+ $uri = URI.parse arg
196
+ doc = Net::HTTP.get($uri)
197
+ $namespaced = Hash[doc.scan(/<\/(\w+):(\w+)>/).uniq.
198
+ map {|p,n| [n, "#{p} :#{n}"]}]
199
+ $namespaced.delete_if {|name, value| doc =~ /<#{name}[ >]/}
200
+ code Nokogiri::HTML(doc).root
201
+ end
202
+
203
+ # headers
204
+ if ''.respond_to? 'encoding'
205
+ puts '# encoding: utf-8' if $q.any? {|line| line.match /[^\x20-\x7f]/}
206
+ else
207
+ puts "require 'rubygems'"
208
+ end
209
+
210
+ puts "require 'wunderbar'\n\n"
211
+
212
+ # main output
213
+ puts $q.join("\n")
data/wunderbar.gemspec CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "wunderbar"
5
- s.version = "0.8.5"
5
+ s.version = "0.8.6"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Sam Ruby"]
9
- s.date = "2012-02-18"
9
+ s.date = "2012-03-17"
10
10
  s.description = " Provides a number of globals, helper methods, and monkey patches which\n simplify the generation of HTML and the development of CGI scripts.\n"
11
11
  s.email = "rubys@intertwingly.net"
12
12
  s.extra_rdoc_files = ["COPYING", "README", "lib/wunderbar.rb", "lib/wunderbar/builder.rb", "lib/wunderbar/cgi-methods.rb", "lib/wunderbar/environment.rb", "lib/wunderbar/html-methods.rb", "lib/wunderbar/installation.rb", "lib/wunderbar/job-control.rb", "lib/wunderbar/logger.rb", "lib/wunderbar/version.rb"]
13
- s.files = ["COPYING", "README", "Rakefile", "lib/wunderbar.rb", "lib/wunderbar/builder.rb", "lib/wunderbar/cgi-methods.rb", "lib/wunderbar/environment.rb", "lib/wunderbar/html-methods.rb", "lib/wunderbar/installation.rb", "lib/wunderbar/job-control.rb", "lib/wunderbar/logger.rb", "lib/wunderbar/version.rb", "test/test_builder.rb", "test/test_html_markup.rb", "test/test_logger.rb", "wunderbar.gemspec", "Manifest"]
13
+ s.files = ["COPYING", "README", "Rakefile", "demo/wiki.html", "demo/wiki.rb", "lib/wunderbar.rb", "lib/wunderbar/builder.rb", "lib/wunderbar/cgi-methods.rb", "lib/wunderbar/environment.rb", "lib/wunderbar/html-methods.rb", "lib/wunderbar/installation.rb", "lib/wunderbar/job-control.rb", "lib/wunderbar/logger.rb", "lib/wunderbar/version.rb", "test/test_builder.rb", "test/test_html_markup.rb", "test/test_logger.rb", "tools/web2script.rb", "Manifest", "wunderbar.gemspec"]
14
14
  s.homepage = "http://github.com/rubys/wunderbar"
15
15
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Wunderbar", "--main", "README"]
16
16
  s.require_paths = ["lib"]
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wunderbar
3
3
  version: !ruby/object:Gem::Version
4
- hash: 53
4
+ hash: 51
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 8
9
- - 5
10
- version: 0.8.5
9
+ - 6
10
+ version: 0.8.6
11
11
  platform: ruby
12
12
  authors:
13
13
  - Sam Ruby
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-02-18 00:00:00 Z
18
+ date: 2012-03-17 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: builder
@@ -67,6 +67,8 @@ files:
67
67
  - COPYING
68
68
  - README
69
69
  - Rakefile
70
+ - demo/wiki.html
71
+ - demo/wiki.rb
70
72
  - lib/wunderbar.rb
71
73
  - lib/wunderbar/builder.rb
72
74
  - lib/wunderbar/cgi-methods.rb
@@ -79,8 +81,9 @@ files:
79
81
  - test/test_builder.rb
80
82
  - test/test_html_markup.rb
81
83
  - test/test_logger.rb
82
- - wunderbar.gemspec
84
+ - tools/web2script.rb
83
85
  - Manifest
86
+ - wunderbar.gemspec
84
87
  homepage: http://github.com/rubys/wunderbar
85
88
  licenses: []
86
89