showoff 0.4.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +133 -39
- data/bin/showoff +12 -3
- data/lib/commandline_parser.rb +67 -0
- data/lib/showoff.rb +155 -54
- data/lib/showoff_utils.rb +30 -16
- data/public/css/showoff.css +74 -21
- data/public/js/coffee-script.js +8 -0
- data/public/js/fg.menu.js +0 -0
- data/public/js/sh_lang/sh_coffeescript.min.js +1 -0
- data/public/js/sh_lang/sh_erlang.min.js +1 -0
- data/public/js/showoff.js +60 -21
- data/views/index.erb +9 -4
- data/views/onepage.erb +0 -1
- metadata +30 -14
data/README.rdoc
CHANGED
@@ -7,7 +7,7 @@ hatred in my heart for Keynote and yet it is by far the best in the field.
|
|
7
7
|
|
8
8
|
The idea is that you setup your markdown slide files in section subdirectories
|
9
9
|
and then startup the showoff server in that directory. It will read in your
|
10
|
-
showoff.json file for which sections go in which order and then will give
|
10
|
+
<tt>showoff.json</tt> file for which sections go in which order and then will give
|
11
11
|
you a URL to present from.
|
12
12
|
|
13
13
|
It can:
|
@@ -18,7 +18,7 @@ It can:
|
|
18
18
|
* bullets with incremental advancing
|
19
19
|
* re-enact command line interactions
|
20
20
|
* call up a menu of sections/slides at any time to jump around
|
21
|
-
* execute
|
21
|
+
* execute Javascript, Coffeescript or Ruby live and display results
|
22
22
|
* do simple transitions (instant, fade, slide in)
|
23
23
|
* show a pre-show slideshow while you wait to start
|
24
24
|
|
@@ -26,13 +26,14 @@ It might will can:
|
|
26
26
|
|
27
27
|
* show a timer - elapsed / remaining
|
28
28
|
* perform simple animations of images moving between keyframes
|
29
|
-
* show
|
29
|
+
* show synchronized, hidden notes on another browser (like an iphone)
|
30
30
|
* show audience questions / comments (twitter or direct)
|
31
31
|
* let audience members go back / catch up as you talk
|
32
32
|
* let audience members vote on sections (?)
|
33
33
|
* broadcast itself on Bonjour
|
34
34
|
* let audience members download slides, code samples or other supplementary material
|
35
35
|
* let you write on the slide with your mouse, madden-style via canvas
|
36
|
+
* automatically resize text to fit screen [see Alex's shrink.js]
|
36
37
|
|
37
38
|
Some of the nice things are that you can easily version control it, you
|
38
39
|
can easily move sections between presentations, and you can rearrange or
|
@@ -40,21 +41,28 @@ remove sections easily.
|
|
40
41
|
|
41
42
|
= Usage
|
42
43
|
|
43
|
-
ShowOff is meant to be run in a ShowOff formatted repository - that means that
|
44
|
+
ShowOff is meant to be run in a ShowOff formatted repository - that means that
|
45
|
+
it has a <tt>showoff.json</tt> file and a number of sections (subdirectories) with
|
46
|
+
markdown files for the slides you're presenting.
|
44
47
|
|
45
48
|
$ gem install showoff
|
46
49
|
$ git clone (showoff-repo)
|
47
50
|
$ cd (showoff-repo)
|
48
51
|
$ showoff serve
|
49
52
|
|
50
|
-
If you run 'showoff' in the example subdirectory of ShowOff itself, it will
|
53
|
+
If you run 'showoff' in the example subdirectory of ShowOff itself, it will
|
51
54
|
show an example presentation, so you can see what it's like.
|
52
55
|
|
56
|
+
You can also run 'showoff serve' inside a section subdirectory. If there is no
|
57
|
+
<tt>showoff.json</tt> file then it will make its best guess, creating a presentation
|
58
|
+
from all `.md` files in alphabetical order in the given (or current)
|
59
|
+
directory.
|
60
|
+
|
53
61
|
= Slide Format
|
54
62
|
|
55
63
|
You can break your slides up into sections of however many subdirectories deep
|
56
64
|
you need. ShowOff will recursively check all the directories mentioned in
|
57
|
-
your showoff.json file for any markdown files (.md). Each markdown file can
|
65
|
+
your <tt>showoff.json</tt> file for any markdown files (.md). Each markdown file can
|
58
66
|
have any number of slides in it, separating each slide with the '!SLIDE'
|
59
67
|
keyword and optional slide styles.
|
60
68
|
|
@@ -77,17 +85,31 @@ the following contents:
|
|
77
85
|
That represents two slides, the first contains just a large title, and the
|
78
86
|
second is faded into view showing the title and three bullets that are then
|
79
87
|
incrementally shown. In order for ShowOff to see those slides, your
|
80
|
-
showoff.json file needs to look something like this:
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
88
|
+
<tt>showoff.json</tt> file needs to look something like this:
|
89
|
+
|
90
|
+
{
|
91
|
+
"name": "Something",
|
92
|
+
"description": "Example Presentation",
|
93
|
+
"sections": [
|
94
|
+
{"section":"one"}
|
95
|
+
]
|
96
|
+
}
|
85
97
|
|
86
98
|
If you have multiple sections in your talk, you can make this json array
|
87
99
|
include all the sections you want to show in which order you want to show
|
88
100
|
them.
|
89
101
|
|
90
|
-
|
102
|
+
Instead of a hash, you can use a plain string as an entry in the `sections`
|
103
|
+
section of `showoff.json`.
|
104
|
+
|
105
|
+
And if that plain string starts with '#' then it is interpreted not as a
|
106
|
+
filename, but as markdown. This is used for inserting interstitial slides
|
107
|
+
or notes -- for instance, Alex Chaffee's
|
108
|
+
[Ruby Notes](http://github.com/alexch/ruby_notes)
|
109
|
+
uses it to insert lab instructions between lecture slide sections, which may
|
110
|
+
vary from venue to venue.
|
111
|
+
|
112
|
+
If you want to keep the ability to emit an HTML document from your
|
91
113
|
Markdown source file -- say, for a TextMate preview or a GitHub rendering
|
92
114
|
-- you can use angle brackets around the `!SLIDE` keyword and styles, e.g.
|
93
115
|
|
@@ -106,7 +128,7 @@ Some useful styles for each slide are:
|
|
106
128
|
* incremental - can be used with 'bullets' and 'commandline' styles, will incrementally update elements on arrow key rather than switch slides
|
107
129
|
* small - make all slide text 80%
|
108
130
|
* smaller - make all slide text 70%
|
109
|
-
* execute - on
|
131
|
+
* execute - on Javascript, Coffeescript and Ruby highlighted code slides, you can click on the code to execute it and display the results on the slide
|
110
132
|
|
111
133
|
Check out the example directory included to see examples of most of these.
|
112
134
|
|
@@ -147,13 +169,20 @@ The transitions are provided by jQuery Cycle plugin. See http://www.malsup.com/j
|
|
147
169
|
You can manage the presentation with the following keys:
|
148
170
|
|
149
171
|
* space, cursor right: next slide
|
150
|
-
* cursor left: previous slide
|
172
|
+
* shift-space, cursor left: previous slide
|
151
173
|
* d: debug mode
|
152
|
-
* c: table of contents (vi)
|
174
|
+
* c, t: table of contents (vi)
|
153
175
|
* f: toggle footer
|
154
|
-
* z
|
176
|
+
* z, ?: toggle help
|
155
177
|
* p: toggle preshow
|
156
178
|
|
179
|
+
= Showing plain old markdown
|
180
|
+
|
181
|
+
If a markdown file has no !SLIDE keywords, then showoff will treat every line
|
182
|
+
beginning with a single hash -- i.e. every H1 -- as a new slide in "bullets"
|
183
|
+
style. Remember that you can't specify classes or transitions in this mode,
|
184
|
+
and as soon as you add one !SLIDE you need them everywhere.
|
185
|
+
|
157
186
|
= Preshow
|
158
187
|
|
159
188
|
If you want to show a slideshow while you wait to speak, you can run a preshow. Add a +_preshow+ directory
|
@@ -166,7 +195,7 @@ audience in the meantime. Press 'p' again to stop, or wait until the timer runs
|
|
166
195
|
|
167
196
|
To insert custom JavaScript into your presentation you can either place it into
|
168
197
|
a file (with extension .js) into the root directory of your presentation or you
|
169
|
-
can embed a
|
198
|
+
can embed a <+script+> element directly into your slides. This JavaScript will be
|
170
199
|
executed—as usually—as soon as it is loaded.
|
171
200
|
|
172
201
|
If you want to trigger some JavaScript as soon as a certain page is shown or
|
@@ -222,8 +251,8 @@ The same applies to the *showoff:prev* event, of course.
|
|
222
251
|
= Custom Stylesheets
|
223
252
|
|
224
253
|
To insert custom Stylesheets into your presentation you can either place it into
|
225
|
-
a file (with extension .css)
|
226
|
-
you can embed a <+link
|
254
|
+
a file (with extension .css) into the root directory of your presentation or
|
255
|
+
you can embed a <+link+> element directly into your slides. This stylesheet will
|
227
256
|
be applied as soon as it is loaded.
|
228
257
|
|
229
258
|
The content generated by the slide is wrapped with a +div+ with the class .+content+ like this.
|
@@ -257,14 +286,31 @@ Note that the example above uses CSS3 styling with ::+after+ and the +content+
|
|
257
286
|
= Language highlighting
|
258
287
|
|
259
288
|
Showoff uses {shjs}[http://shjs.sourceforge.net/] to highlight code blocks.
|
260
|
-
If you begin a code block with three @-signs followed by a
|
261
|
-
{programming language name}[http://shjs.sourceforge.net/doc/documentation.html],
|
289
|
+
If you begin a code block with three @-signs followed by a
|
290
|
+
{programming language name}[http://shjs.sourceforge.net/doc/documentation.html],
|
262
291
|
that line will be stripped and the rest of the block will become sparkly
|
263
292
|
and colorful.
|
264
293
|
|
265
294
|
@@@ ruby
|
266
295
|
10.times { puts "Whee!" }
|
267
296
|
|
297
|
+
= Custom Ruby Files
|
298
|
+
|
299
|
+
If you want to have executable Ruby code on your slides you must set the
|
300
|
+
environment variable ENV['SHOWOFF_EVAL_RUBY']. This can be done with
|
301
|
+
|
302
|
+
export SHOWOFF_EVAL_RUBY=1
|
303
|
+
|
304
|
+
or
|
305
|
+
|
306
|
+
# On Heroku
|
307
|
+
heroku config:add SHOWOFF_EVAL_RUBY=1
|
308
|
+
|
309
|
+
|
310
|
+
If you need supporting libraries when you evaluate the code. You can do this by
|
311
|
+
putting Ruby files (*.rb) into the root directory of the presentation then they
|
312
|
+
will be required when the presentation loads.
|
313
|
+
|
268
314
|
= Editor integration
|
269
315
|
|
270
316
|
The "add slide" feature can allow you to add the necessary boilerplate from your editor. If you are using vim, you can
|
@@ -294,19 +340,24 @@ added where your cursor was. Binding this to a keybinding can allow you to add
|
|
294
340
|
[<tt>help</tt>] Shows list of commands or help for one command
|
295
341
|
[<tt>heroku</tt>] Setup your presentation to serve on Heroku
|
296
342
|
[<tt>github</tt>] Setup your presentation to serve on GitHub Pages
|
297
|
-
[<tt>serve</tt>] Serves the showoff presentation in the current directory
|
343
|
+
[<tt>serve</tt>] Serves the showoff presentation in the current directory (or a given dir)
|
298
344
|
[<tt>static</tt>] Generate static version of presentation
|
299
345
|
|
300
|
-
|
346
|
+
|
347
|
+
== <tt>showoff add [title]</tt>
|
301
348
|
|
302
349
|
Add a new slide at the end in a given dir
|
303
350
|
|
304
351
|
*Aliases*
|
305
352
|
* <tt><b>new</b></tt>
|
306
353
|
|
307
|
-
Outputs or creates a new slide.
|
354
|
+
Outputs or creates a new slide. With -d and -n, a new slide is created in the given dir, numbered to appear
|
355
|
+
as the last slide in that dir (use -u to avoid numbering). Without those, outputs the slide markdown to
|
356
|
+
stdout (useful for shelling out from your editor). You may also specify a source file to use for a code
|
357
|
+
slide.
|
358
|
+
|
359
|
+
=== options for add
|
308
360
|
|
309
|
-
==== Options
|
310
361
|
These options are specified *after* the command.
|
311
362
|
|
312
363
|
[<tt>-d, --dir=dir</tt>] Slide dir (where to put a new slide file)
|
@@ -314,7 +365,9 @@ These options are specified *after* the command.
|
|
314
365
|
[<tt>-s, --source=path to file</tt>] Include code from the given file as the slide body
|
315
366
|
[<tt>-t, --style, --type=valid showoff style/type</tt>] Slide Type/Style <i>( default: <tt>title</tt>)</i>
|
316
367
|
[<tt>-u, --nonumber</tt>] Dont number the slide, use the given name verbatim
|
317
|
-
|
368
|
+
|
369
|
+
|
370
|
+
== <tt>showoff create dir_name</tt>
|
318
371
|
|
319
372
|
Create new showoff presentation
|
320
373
|
|
@@ -323,57 +376,92 @@ Create new showoff presentation
|
|
323
376
|
|
324
377
|
This command helps start a new showoff presentation by setting up the proper directory structure for you. It takes the directory name you would like showoff to create for you.
|
325
378
|
|
326
|
-
|
379
|
+
=== options for create
|
380
|
+
|
327
381
|
These options are specified *after* the command.
|
328
382
|
|
329
383
|
[<tt>-d, --slidedir=arg</tt>] sample slide directory name <i>( default: <tt>one</tt>)</i>
|
330
384
|
[<tt>-n, --nosamples</tt>] Dont create sample slides
|
331
|
-
|
385
|
+
|
386
|
+
|
387
|
+
== <tt>showoff help [command]</tt>
|
332
388
|
|
333
389
|
Shows list of commands or help for one command
|
334
390
|
|
335
|
-
|
391
|
+
|
392
|
+
== <tt>showoff heroku heroku_name</tt>
|
336
393
|
|
337
394
|
Setup your presentation to serve on Heroku
|
338
395
|
|
339
396
|
Creates the Gemfile and config.ru file needed to push a showoff pres to heroku. It will then run heroku create for you to register the new project on heroku and add the remote for you. Then all you need to do is commit the new created files and run git push heroku to deploy.
|
340
397
|
|
341
|
-
|
398
|
+
|
399
|
+
== <tt>showoff github</tt>
|
342
400
|
|
343
401
|
Generates a static version of your site and puts it in a gh-pages branch for static serving on GitHub.
|
344
402
|
|
345
|
-
|
403
|
+
=== options for github
|
346
404
|
These options are specified *after* the command.
|
347
405
|
|
348
406
|
[<tt>-f, --force</tt>] force overwrite of existing Gemfile/.gems and config.ru files if they exist
|
349
407
|
[<tt>-g, --dotgems</tt>] Use older-style .gems file instead of bundler-style Gemfile
|
350
408
|
[<tt>-p, --password=arg</tt>] add password protection to your heroku site
|
351
|
-
=== <tt>serve </tt>
|
352
409
|
|
353
|
-
Serves the showoff presentation in the current directory
|
354
410
|
|
411
|
+
== <tt>showoff serve </tt>
|
355
412
|
|
413
|
+
Serves the showoff presentation in the current directory
|
356
414
|
|
357
|
-
====
|
415
|
+
==== options for serve
|
358
416
|
These options are specified *after* the command.
|
359
417
|
|
418
|
+
[<tt>-f, --pres_file=arg</tt>] Presentation file <i>(default: <tt>showoff.json</tt>)</i>
|
360
419
|
[<tt>-h, --host=arg</tt>] Host or ip to run on <i>( default: <tt>localhost</tt>)</i>
|
361
420
|
[<tt>-p, --port=arg</tt>] Port on which to run <i>( default: <tt>9090</tt>)</i>
|
362
|
-
|
421
|
+
|
422
|
+
|
423
|
+
== <tt>showoff static name</tt>
|
363
424
|
|
364
425
|
Generate static version of presentation
|
365
426
|
|
366
|
-
|
367
|
-
|
368
|
-
|
427
|
+
= PDF Output
|
428
|
+
|
429
|
+
Showoff can produce a PDF version of your presentation. To do this, you must install a few things first:
|
430
|
+
|
431
|
+
gem install pdfkit
|
432
|
+
|
433
|
+
You'll then need to install a version of wkhtmltopdf available at the {wkhtmltopdf repo}[http://code.google.com/p/wkhtmltopdf/wiki/compilation] (or brew install wkhtmltopdf on a mac) and make sure that +wkhtmltopdf+ is in your path:
|
369
434
|
|
370
|
-
|
435
|
+
export $PATH="/location/to/my/wkhtmltopdf/0.9.9:$PATH"
|
436
|
+
|
437
|
+
|
438
|
+
|
439
|
+
Then restart showoff, and navigate to <tt>/pdf</tt> (e.g. http://localhost/pdf) of your presentation and a PDF will be generated with the browser.
|
440
|
+
|
441
|
+
= Completion
|
442
|
+
|
443
|
+
== ZSH completion
|
371
444
|
You can complete commands and options in ZSH, by installing a script:
|
372
445
|
|
373
446
|
mkdir -p $HOME/.zsh/Completion
|
374
447
|
cp script/_showoff $HOME/.zsh/Completion
|
375
448
|
echo 'fpath=(~/.zsh/Completion $fpath)' >> $HOME/.zshrc
|
376
449
|
|
450
|
+
== <tt>bash</tt> completion
|
451
|
+
|
452
|
+
You can complete commands for showoff by putting the following in your <tt>.bashrc</tt> (or whatever
|
453
|
+
you use when starting <tt>bash</tt>):
|
454
|
+
|
455
|
+
complete -F get_showoff_commands
|
456
|
+
function get_showoff_commands()
|
457
|
+
{
|
458
|
+
if [ -z $2 ] ; then
|
459
|
+
COMPREPLY=(`showoff help -c`)
|
460
|
+
else
|
461
|
+
COMPREPLY=(`showoff help -c $2`)
|
462
|
+
fi
|
463
|
+
}
|
464
|
+
|
377
465
|
= Real World Usage
|
378
466
|
|
379
467
|
So far, ShowOff has been used in the following presentations (and many others):
|
@@ -407,6 +495,12 @@ So far, ShowOff has been used in the following presentations (and many others):
|
|
407
495
|
http://github.com/nono/Presentations/tree/master/20100703_25_promising_projects_in_50_minutes/
|
408
496
|
* 11th Libre Software Meeting 2010 - Ruby 1.9, The future of Ruby? - Bruno Michel
|
409
497
|
http://github.com/nono/Presentations/tree/master/20100708_RMLL_Ruby_1.9/
|
498
|
+
* Lone Star PHP 2011 - Drupal - Chris Christensen
|
499
|
+
https://github.com/christianchristensen/Presentations/tree/master/20110611-lonestarphp-drupal
|
500
|
+
* Railsbridge Open Workshops - Sarah Allen, Sarah Mei, and Alex Chaffee
|
501
|
+
http://github.com/alexch/workshop
|
502
|
+
* SDRuby Lightning Talk - Readable Regexps - Ian Young
|
503
|
+
https://github.com/iangreenleaf/sdruby-lightningtalk-tregexp
|
410
504
|
|
411
505
|
|
412
506
|
If you use it for something, please let me know so I can add it.
|
data/bin/showoff
CHANGED
@@ -56,19 +56,28 @@ command :heroku do |c|
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
desc 'Serves the showoff presentation in the current directory'
|
59
|
+
desc 'Serves the showoff presentation in the specified (or current) directory'
|
60
|
+
arg_name "[pres_dir]"
|
61
|
+
default_value "."
|
60
62
|
command :serve do |c|
|
61
63
|
|
64
|
+
c.desc 'Show verbose messaging'
|
65
|
+
c.switch :verbose
|
66
|
+
|
62
67
|
c.desc 'Port on which to run'
|
63
68
|
c.default_value "9090"
|
64
69
|
c.flag [:p,:port]
|
65
70
|
|
66
71
|
c.desc 'Host or ip to run on'
|
67
72
|
c.default_value "localhost"
|
68
|
-
c.flag [:h,:host]
|
73
|
+
c.flag [:h,:host]
|
74
|
+
|
75
|
+
c.desc 'JSON file used to describe presentation'
|
76
|
+
c.default_value "showoff.json"
|
77
|
+
c.flag [:f, :pres_file]
|
69
78
|
|
70
79
|
c.action do |global_options,options,args|
|
71
|
-
ShowOff.run! :host => options[:h], :port => options[:p].to_i
|
80
|
+
ShowOff.run! :host => options[:h], :port => options[:p].to_i, :pres_file => options[:f], :pres_dir => args[0], :verbose => options[:verbose]
|
72
81
|
end
|
73
82
|
end
|
74
83
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
# For parsing commandline slide content.
|
4
|
+
class CommandlineParser < Parslet::Parser
|
5
|
+
|
6
|
+
rule(:prompt) do
|
7
|
+
str('$') | str('#')
|
8
|
+
end
|
9
|
+
|
10
|
+
rule(:text) do
|
11
|
+
match['[:print:]'].repeat
|
12
|
+
end
|
13
|
+
|
14
|
+
rule(:singleline_input) do
|
15
|
+
(str("\\\n").absent? >> match['[:print:]']).repeat
|
16
|
+
end
|
17
|
+
|
18
|
+
rule(:input) do
|
19
|
+
multiline_input | singleline_input
|
20
|
+
end
|
21
|
+
|
22
|
+
rule(:multiline_input) do
|
23
|
+
|
24
|
+
# some command \
|
25
|
+
# continued \
|
26
|
+
# \
|
27
|
+
# and stop
|
28
|
+
( singleline_input >> str('\\') >> newline ).repeat(1) >> singleline_input
|
29
|
+
end
|
30
|
+
|
31
|
+
rule(:command) do
|
32
|
+
|
33
|
+
# $ some command
|
34
|
+
# some output
|
35
|
+
( prompt.as(:prompt) >> space? >> input.as(:input) >> output? ).as(:command)
|
36
|
+
end
|
37
|
+
|
38
|
+
rule(:output) do
|
39
|
+
|
40
|
+
# output
|
41
|
+
prompt.absent? >> text
|
42
|
+
end
|
43
|
+
|
44
|
+
rule(:output?) do
|
45
|
+
|
46
|
+
#
|
47
|
+
# some text
|
48
|
+
# some text
|
49
|
+
#
|
50
|
+
# some text
|
51
|
+
( newline >> ( ( output >> newline ).repeat >> output.maybe ).as(:output) ).maybe
|
52
|
+
end
|
53
|
+
|
54
|
+
rule(:commands) do
|
55
|
+
command.repeat
|
56
|
+
end
|
57
|
+
|
58
|
+
rule(:newline) do
|
59
|
+
str("\n") | str("\r\n")
|
60
|
+
end
|
61
|
+
|
62
|
+
rule(:space?) do
|
63
|
+
match['[:space:]'].repeat
|
64
|
+
end
|
65
|
+
|
66
|
+
root(:commands)
|
67
|
+
end
|
data/lib/showoff.rb
CHANGED
@@ -3,10 +3,12 @@ require 'sinatra/base'
|
|
3
3
|
require 'json'
|
4
4
|
require 'nokogiri'
|
5
5
|
require 'fileutils'
|
6
|
+
require 'logger'
|
6
7
|
|
7
8
|
here = File.expand_path(File.dirname(__FILE__))
|
8
9
|
require "#{here}/showoff_utils"
|
9
10
|
require "#{here}/princely"
|
11
|
+
require "#{here}/commandline_parser"
|
10
12
|
|
11
13
|
begin
|
12
14
|
require 'RMagick'
|
@@ -27,38 +29,58 @@ rescue LoadError
|
|
27
29
|
Object.send(:remove_const,:Markdown)
|
28
30
|
Markdown = BlueCloth
|
29
31
|
end
|
30
|
-
require 'pp'
|
31
32
|
|
32
33
|
class ShowOff < Sinatra::Application
|
33
34
|
|
34
|
-
Version = VERSION = '0.
|
35
|
+
Version = VERSION = '0.6.0'
|
35
36
|
|
36
37
|
attr_reader :cached_image_size
|
37
38
|
|
38
39
|
set :views, File.dirname(__FILE__) + '/../views'
|
39
40
|
set :public, File.dirname(__FILE__) + '/../public'
|
40
|
-
set :pres_dir, 'example'
|
41
41
|
|
42
42
|
def initialize(app=nil)
|
43
43
|
super(app)
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
@logger = Logger.new(STDOUT)
|
45
|
+
@logger.formatter = proc { |severity,datetime,progname,msg| "#{progname} #{msg}\n" }
|
46
|
+
@logger.level = options.verbose ? Logger::DEBUG : Logger::WARN
|
47
|
+
|
48
|
+
dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
49
|
+
@logger.debug(dir)
|
50
|
+
|
51
|
+
showoff_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
52
|
+
if Dir.pwd == showoff_dir
|
53
|
+
options.pres_dir = "#{showoff_dir}/example"
|
47
54
|
@root_path = "."
|
48
55
|
else
|
49
|
-
options.pres_dir
|
56
|
+
options.pres_dir ||= Dir.pwd
|
50
57
|
@root_path = ".."
|
51
58
|
end
|
59
|
+
options.pres_dir = File.expand_path(options.pres_dir)
|
60
|
+
if (options.pres_file)
|
61
|
+
puts "Using #{options.pres_file}"
|
62
|
+
ShowOffUtils.presentation_config_file = options.pres_file
|
63
|
+
end
|
64
|
+
puts "Serving presentation from #{options.pres_dir}"
|
52
65
|
@cached_image_size = {}
|
53
|
-
|
66
|
+
@logger.debug options.pres_dir
|
54
67
|
@pres_name = options.pres_dir.split('/').pop
|
68
|
+
require_ruby_files
|
69
|
+
end
|
70
|
+
|
71
|
+
def require_ruby_files
|
72
|
+
Dir.glob("#{options.pres_dir}/*.rb").map { |path| require path }
|
55
73
|
end
|
56
74
|
|
57
75
|
helpers do
|
58
76
|
def load_section_files(section)
|
59
77
|
section = File.join(options.pres_dir, section)
|
60
|
-
files =
|
61
|
-
|
78
|
+
files = if File.directory? section
|
79
|
+
Dir.glob("#{section}/**/*").sort
|
80
|
+
else
|
81
|
+
[section]
|
82
|
+
end
|
83
|
+
@logger.debug files
|
62
84
|
files
|
63
85
|
end
|
64
86
|
|
@@ -70,34 +92,68 @@ class ShowOff < Sinatra::Application
|
|
70
92
|
Dir.glob("#{options.pres_dir}/*.js").map { |path| File.basename(path) }
|
71
93
|
end
|
72
94
|
|
95
|
+
|
73
96
|
def preshow_files
|
74
97
|
Dir.glob("#{options.pres_dir}/_preshow/*").map { |path| File.basename(path) }.to_json
|
75
98
|
end
|
76
99
|
|
77
|
-
|
78
|
-
|
79
|
-
|
100
|
+
# todo: move more behavior into this class
|
101
|
+
class Slide
|
102
|
+
attr_reader :classes, :text
|
103
|
+
def initialize classes = ""
|
104
|
+
@classes = ["content"] + classes.strip.chomp('>').split
|
105
|
+
@text = ""
|
106
|
+
end
|
107
|
+
def <<(s)
|
108
|
+
@text << s
|
109
|
+
@text << "\n"
|
110
|
+
end
|
111
|
+
def empty?
|
112
|
+
@text.strip == ""
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def process_markdown(name, content, static=false, pdf=false)
|
118
|
+
|
119
|
+
# if there are no !SLIDE markers, then make every H1 define a new slide
|
120
|
+
unless content =~ /^\<?!SLIDE/m
|
121
|
+
content = content.gsub(/^# /m, "<!SLIDE>\n# ")
|
122
|
+
end
|
123
|
+
|
124
|
+
# todo: unit test
|
125
|
+
lines = content.split("\n")
|
126
|
+
puts "#{name}: #{lines.length} lines"
|
127
|
+
slides = []
|
128
|
+
slides << (slide = Slide.new)
|
129
|
+
until lines.empty?
|
130
|
+
line = lines.shift
|
131
|
+
if line =~ /^<?!SLIDE(.*)>?/
|
132
|
+
slides << (slide = Slide.new($1))
|
133
|
+
else
|
134
|
+
slide << line
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
slides.delete_if {|slide| slide.empty? }
|
139
|
+
|
80
140
|
final = ''
|
81
141
|
if slides.size > 1
|
82
142
|
seq = 1
|
83
143
|
end
|
84
144
|
slides.each do |slide|
|
85
145
|
md = ''
|
86
|
-
|
87
|
-
|
88
|
-
content_classes = lines.shift.strip.chomp('>').split rescue []
|
89
|
-
slide = lines.join("\n")
|
90
|
-
# add content class too
|
91
|
-
content_classes.unshift "content"
|
146
|
+
content_classes = slide.classes
|
147
|
+
|
92
148
|
# extract transition, defaulting to none
|
93
149
|
transition = 'none'
|
94
150
|
content_classes.delete_if { |x| x =~ /^transition=(.+)/ && transition = $1 }
|
95
151
|
# extract id, defaulting to none
|
96
152
|
id = nil
|
97
153
|
content_classes.delete_if { |x| x =~ /^#([\w-]+)/ && id = $1 }
|
98
|
-
|
99
|
-
|
100
|
-
|
154
|
+
@logger.debug "id: #{id}" if id
|
155
|
+
@logger.debug "classes: #{content_classes.inspect}"
|
156
|
+
@logger.debug "transition: #{transition}"
|
101
157
|
# create html
|
102
158
|
md += "<div"
|
103
159
|
md += " id=\"#{id}\"" if id
|
@@ -108,8 +164,8 @@ class ShowOff < Sinatra::Application
|
|
108
164
|
else
|
109
165
|
md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}\">\n"
|
110
166
|
end
|
111
|
-
sl = Markdown.new(slide).to_html
|
112
|
-
sl = update_image_paths(name, sl, static)
|
167
|
+
sl = Markdown.new(slide.text).to_html
|
168
|
+
sl = update_image_paths(name, sl, static, pdf)
|
113
169
|
md += sl
|
114
170
|
md += "</div>\n"
|
115
171
|
md += "</div>\n"
|
@@ -124,14 +180,14 @@ class ShowOff < Sinatra::Application
|
|
124
180
|
markdown.gsub(/<p>\.(.*?) /, '<p class="\1">')
|
125
181
|
end
|
126
182
|
|
127
|
-
def update_image_paths(path, slide, static=false)
|
183
|
+
def update_image_paths(path, slide, static=false, pdf=false)
|
128
184
|
paths = path.split('/')
|
129
185
|
paths.pop
|
130
186
|
path = paths.join('/')
|
131
187
|
replacement_prefix = static ?
|
132
|
-
%(img src="file://#{options.pres_dir}/#{path}) :
|
188
|
+
( pdf ? %(img src="file://#{options.pres_dir}/#{path}) : %(img src="./file/#{path}) ) :
|
133
189
|
%(img src="/image/#{path})
|
134
|
-
slide.gsub(/img src=\"(.*?)\"/) do |s|
|
190
|
+
slide.gsub(/img src=\"([^\/].*?)\"/) do |s|
|
135
191
|
img_path = File.join(path, $1)
|
136
192
|
w, h = get_image_size(img_path)
|
137
193
|
src = %(#{replacement_prefix}/#{$1}")
|
@@ -146,7 +202,12 @@ class ShowOff < Sinatra::Application
|
|
146
202
|
def get_image_size(path)
|
147
203
|
if !cached_image_size.key?(path)
|
148
204
|
img = Magick::Image.ping(path).first
|
149
|
-
|
205
|
+
# don't set a size for svgs so they can expand to fit their container
|
206
|
+
if img.mime_type == 'image/svg+xml'
|
207
|
+
cached_image_size[path] = [nil, nil]
|
208
|
+
else
|
209
|
+
cached_image_size[path] = [img.columns, img.rows]
|
210
|
+
end
|
150
211
|
end
|
151
212
|
cached_image_size[path]
|
152
213
|
end
|
@@ -157,14 +218,15 @@ class ShowOff < Sinatra::Application
|
|
157
218
|
|
158
219
|
def update_commandline_code(slide)
|
159
220
|
html = Nokogiri::XML.parse(slide)
|
221
|
+
parser = CommandlineParser.new
|
160
222
|
|
161
223
|
html.css('pre').each do |pre|
|
162
224
|
pre.css('code').each do |code|
|
163
225
|
out = code.text
|
164
226
|
lines = out.split("\n")
|
165
|
-
if lines.first[0, 3] == '@@@'
|
227
|
+
if lines.first.strip[0, 3] == '@@@'
|
166
228
|
lang = lines.shift.gsub('@@@', '').strip
|
167
|
-
pre.set_attribute('class', 'sh_' + lang)
|
229
|
+
pre.set_attribute('class', 'sh_' + lang.downcase)
|
168
230
|
code.content = lines.join("\n")
|
169
231
|
end
|
170
232
|
end
|
@@ -172,38 +234,53 @@ class ShowOff < Sinatra::Application
|
|
172
234
|
|
173
235
|
html.css('.commandline > pre > code').each do |code|
|
174
236
|
out = code.text
|
175
|
-
lines = out.split(/^\$(.*?)$/)
|
176
|
-
lines.delete('')
|
177
237
|
code.content = ''
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
238
|
+
tree = parser.parse(out)
|
239
|
+
transform = Parslet::Transform.new do
|
240
|
+
rule(:prompt => simple(:prompt), :input => simple(:input), :output => simple(:output)) do
|
241
|
+
command = Nokogiri::XML::Node.new('code', html)
|
242
|
+
command.set_attribute('class', 'command')
|
243
|
+
command.content = "#{prompt} #{input}"
|
244
|
+
code << command
|
245
|
+
|
246
|
+
# Add newline after the input so that users can
|
247
|
+
# advance faster than the typewriter effect
|
248
|
+
# and still keep inputs on separate lines.
|
249
|
+
code << "\n"
|
250
|
+
|
251
|
+
unless output.to_s.empty?
|
252
|
+
|
253
|
+
result = Nokogiri::XML::Node.new('code', html)
|
254
|
+
result.set_attribute('class', 'result')
|
255
|
+
result.content = output
|
256
|
+
code << result
|
257
|
+
end
|
258
|
+
end
|
189
259
|
end
|
260
|
+
transform.apply(tree)
|
190
261
|
end
|
191
262
|
html.root.to_s
|
192
263
|
end
|
193
264
|
|
194
|
-
def get_slides_html(static=false)
|
195
|
-
sections = ShowOffUtils.showoff_sections(options.pres_dir)
|
265
|
+
def get_slides_html(static=false, pdf=false)
|
266
|
+
sections = ShowOffUtils.showoff_sections(options.pres_dir, @logger)
|
196
267
|
files = []
|
197
268
|
if sections
|
198
|
-
sections.each do |section|
|
199
|
-
files << load_section_files(section)
|
200
|
-
end
|
201
|
-
files = files.flatten
|
202
|
-
files = files.select { |f| f =~ /.md/ }
|
203
269
|
data = ''
|
204
|
-
|
205
|
-
|
206
|
-
|
270
|
+
sections.each do |section|
|
271
|
+
if section =~ /^#/
|
272
|
+
name = section.each_line.first.gsub(/^#*/,'').strip
|
273
|
+
data << process_markdown(name, "<!SLIDE subsection>\n" + section, static, pdf)
|
274
|
+
else
|
275
|
+
files = []
|
276
|
+
files << load_section_files(section)
|
277
|
+
files = files.flatten
|
278
|
+
files = files.select { |f| f =~ /.md/ }
|
279
|
+
files.each do |f|
|
280
|
+
fname = f.gsub(options.pres_dir + '/', '').gsub('.md', '')
|
281
|
+
data << process_markdown(fname, File.read(f), static, pdf)
|
282
|
+
end
|
283
|
+
end
|
207
284
|
end
|
208
285
|
end
|
209
286
|
data
|
@@ -297,7 +374,7 @@ class ShowOff < Sinatra::Application
|
|
297
374
|
end
|
298
375
|
|
299
376
|
def pdf(static=true)
|
300
|
-
@slides = get_slides_html(static)
|
377
|
+
@slides = get_slides_html(static, true)
|
301
378
|
@no_js = false
|
302
379
|
html = erb :onepage
|
303
380
|
# TODO make a random filename
|
@@ -363,9 +440,33 @@ class ShowOff < Sinatra::Application
|
|
363
440
|
FileUtils.makedirs(File.join(file_dir, dir))
|
364
441
|
FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
|
365
442
|
end
|
443
|
+
# copy images from css too
|
444
|
+
Dir.glob("#{pres_dir}/*.css").each do |css_path|
|
445
|
+
File.open(css_path) do |file|
|
446
|
+
data = file.read
|
447
|
+
data.scan(/url\((.*)\)/).flatten.each do |path|
|
448
|
+
@logger.debug path
|
449
|
+
dir = File.dirname(path)
|
450
|
+
FileUtils.makedirs(File.join(file_dir, dir))
|
451
|
+
FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
366
455
|
end
|
367
456
|
end
|
368
457
|
|
458
|
+
def eval_ruby code
|
459
|
+
eval(code).to_s
|
460
|
+
rescue => e
|
461
|
+
e.message
|
462
|
+
end
|
463
|
+
|
464
|
+
get '/eval_ruby' do
|
465
|
+
return eval_ruby(params[:code]) if ENV['SHOWOFF_EVAL_RUBY']
|
466
|
+
|
467
|
+
return "Ruby Evaluation is off. To turn it on set ENV['SHOWOFF_EVAL_RUBY']"
|
468
|
+
end
|
469
|
+
|
369
470
|
get %r{(?:image|file)/(.*)} do
|
370
471
|
path = params[:captures].first
|
371
472
|
full_path = File.join(options.pres_dir, path)
|