wunderbar 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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