The wiki demo is considerably larger than what we have seen before, and so all we will cover here is selected portions that highlight additional Wunderbar features. While this demo isn't completely polished, it is functional. In fact, I have used it frequently to enable ad-hoc collaboration or even simple note taking. This wiki does implement some features that aren't commonly present in other wiki systems. Features like live preview, autosave, and watch.
The core idea is that with Markdown as a wiki syntax and with git as a versioned backing store, all that remains is a user interface.
Running commands and presenting the output is a common feature of scripts.
Wunderbar provides a _.system
method to help. It will echo the command you
provide and will turn each line of output into a separate <pre>
elements that
you can style however you like:
_html do
_style %{
._stdin:before {content: '$ '}
._stdin {color: #9400D3; margin-bottom: 0}
._stdout {margin: 0}
._stderr {margin: 0; color: red}
}
_.system "git add #{file}"
end
Shell escaping of arguments is a common requirement, and will
be handled automatically by Wunderbar if the argument to _.system
is an
array:
commit = %w(git commit)
commit << '--author' << $EMAIL if defined? $EMAIL and $EMAIL
commit << '--message' << @comment
commit << file
_.system commit
If the escape Ruby gem is included, it will be used instead of Shellwords as it tends to produce more readable output.
One feature not shown here is that arrays may be nested, and if this is done the nested content is not echoed back. This is useful for passwords:
_.system ['svn', 'commit', '--username', $USER, ['--password', secret]]
Markdown is supported by an optional plugin:
require 'wunderbar/markdown'
_html do
_markdown 'text'
end
Use of the Markdown feature requires kramdown, nokogiri, and
This wiki will autosave changes every 5 seconds using Ajax calls back to the same page on the server:
require 'ruby2js/filter/functions'
_html do
@uri = env['REQUEST_URI']
_script do
dirty = false
setInterval 5000 do
return unless dirty
dirty = false
params = {
markup: ~'textarea[name=markup]'.val,
hash: ~'input[name=hash]'.val
}
$$.post(@uri, params) do |response|
# ...
end
end
~'.input'.on(:input) do
dirty = true
end
end
end
What this code does is set dirty
to true
every time input occurs. Every
5000
miliseconds, the dirty
flag is checked, and when set, an Ajax POST
request is sent to the server with the current values of the textarea
and
the (hidden) hash input
field. Along the way, the dirty
flag is reset,
preventing additional Ajax requests until the input field changes again.
The functions
filter provides some useful transformations of the Ruby script
that affects the JavaScript output. In this case, we make use of the
transformation which allows setInterval
to be passed a block. Without this
transformation, the block would still be converted into a function
, but
would be passed as the _final parameter on the call. This transformation
will reorder the parameters.
The AJAX requests are to the same page, so the wiki script will be invoked
again. Wunderbar will detect that the request is an Ajax request, and will
execute the _json
block instead of the _html
one:
# process autosave requests
_json do
hash = Digest::MD5.hexdigest(@markup)
if File.exist?(file) and Digest::MD5.hexdigest(File.read(file)) != @hash
_error "Write conflict"
_markup File.read(file)
else
File.open(file, 'w') {|fh| fh.write @markup} unless @hash == hash
_time Time.now.to_i*1000
end
_hash hash
end
Once again, the parameters passed in are placed into instance variables for easy access.
Output from _json
is, as you might expect, is expressed as JSON, and
typically as a hash (a.k.a., key/value pairs). _json
accumulates these keys
and values by capturing method calls that start with an underscore. So in
this case, the response will either contain three pairs (error, markup, and
hash) or two pairs (time and hash), depending on whether a write conflict
occurred.
Note that times in Ruby are in seconds, and times in JavaScript are in milliseconds, and therefore the Time is multiplied by 1000 before being passed to the client.
Now lets look at the client processing of this response:
$$.post(@uri, params) do |response|
~'input[name=hash]'.val = response.hash
if response.time
time = Date.new(response.time).toLocaleTimeString()
~'#message'.text("Autosaved at #{time}").show.fadeOut(5000)
else
~'.input'.val(response.markup).readonly = true
~'#message'.css(fontWeight: 'bold').text(response.error).show
end
~'#save'.disabled = false if ~'#comment'.val != ''
end
The hash
is stashed into a hidden input variable. The time
, if present,
is converted to the current locale, and is used to replace the contents of the
HTML element with an id of message. This area is then shown (it previously
was hidden) and fades out over 5 seconds. If the time is not present, the
input field is replaced with the servers view of what the latest markup is and
set to readonly. The message field is set to bold, the text is set to the
response, and finally the message field itself is shown.
This excerpt highlights the nearly seamless information flow made possible by
Wunderbar. @uri
was computed on the server in response to the original HTML
request, the params
are computed on the client, sent back to the server,
which returns back response.time
, which is placed back on the page with
the help of jQuery.
In addition to json, Wunderbar support plain text. The wiki demo uses this to return the original markdown source.
# allow the raw markdown to be fetched
_text do
_ File.read(file) if File.exist?(file)
end
The script itself concludes with:
__END__
# Customize where the wiki data is stored
WIKIDATA = '/full/path/to/data/directory'
This enables one to create a new wiki using a CGI wrapper via the following command:
ruby demo/wiki.rb --install=/var/www/mywiki.cgi --wikidata=/var/data/mywiki
A final note: the end data section also includes:
# Width to wrap lines in output HTML produced (remove to disable wrapping)
$WIDTH = 80
And this value is referenced in the html itself:
_html _width: $WIDTH do
#...
end
When this is enabled, Wunderbar will make an attempt to keep each line of output within the specified width. Admittedly, not many people will view source on the HTML generated by a Wiki, but those that do will be pleasantly surprised. Inline scripts and styles will be properly intended, and line breaks will be added to content when doing so doesn't change what will be displayed.
Producing meticulous and lovingly crafted output is a design goal for Wunderbar.
Next up, a discussion of various techniques to introduce modularity into your Wunderbar application.