showoff 0.7.0 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.rdoc +53 -475
  3. data/Rakefile +17 -18
  4. data/bin/showoff +29 -7
  5. data/lib/commandline_parser.rb +1 -1
  6. data/lib/showoff/version.rb +3 -0
  7. data/lib/showoff.rb +600 -91
  8. data/lib/showoff_utils.rb +110 -4
  9. data/public/css/disconnected-large.png +0 -0
  10. data/public/css/disconnected.png +0 -0
  11. data/public/css/fast.png +0 -0
  12. data/public/css/grippy-close.png +0 -0
  13. data/public/css/grippy.png +0 -0
  14. data/public/css/onepage.css +6 -0
  15. data/public/css/pace.png +0 -0
  16. data/public/css/paceMarker.png +0 -0
  17. data/public/css/presenter.css +333 -43
  18. data/public/css/sh_style.css +15 -0
  19. data/public/css/showoff.css +373 -48
  20. data/public/css/slow.png +0 -0
  21. data/public/css/spinner.gif +0 -0
  22. data/public/css/tipsy.css +26 -0
  23. data/public/favicon.ico +0 -0
  24. data/public/js/jquery.parsequery.min.js +2 -0
  25. data/public/js/jquery.tipsy.js +260 -0
  26. data/public/js/onepage.js +2 -3
  27. data/public/js/presenter.js +384 -33
  28. data/public/js/sh_lang/sh_gherkin.js +112 -0
  29. data/public/js/sh_lang/sh_gherkin.min.js +1 -0
  30. data/public/js/sh_lang/sh_ini.js +87 -0
  31. data/public/js/sh_lang/sh_ini.min.js +87 -0
  32. data/public/js/sh_lang/sh_puppet.js +182 -0
  33. data/public/js/sh_lang/sh_puppet.min.js +182 -0
  34. data/public/js/sh_lang/sh_puppet_output.js +22 -0
  35. data/public/js/sh_lang/sh_puppet_output.min.js +22 -0
  36. data/public/js/sh_lang/sh_shell.min.js +1 -0
  37. data/public/js/showoff.js +423 -51
  38. data/views/404.erb +19 -0
  39. data/views/download.erb +36 -0
  40. data/views/header.erb +35 -25
  41. data/views/header_mini.erb +22 -0
  42. data/views/index.erb +46 -1
  43. data/views/onepage.erb +35 -14
  44. data/views/presenter.erb +63 -21
  45. data/views/stats.erb +73 -0
  46. metadata +170 -131
  47. data/public/css/960.css +0 -653
  48. data/public/css/pdf.css +0 -12
data/lib/showoff.rb CHANGED
@@ -4,6 +4,8 @@ require 'json'
4
4
  require 'nokogiri'
5
5
  require 'fileutils'
6
6
  require 'logger'
7
+ require 'htmlentities'
8
+ require 'sinatra-websocket'
7
9
 
8
10
  here = File.expand_path(File.dirname(__FILE__))
9
11
  require "#{here}/showoff_utils"
@@ -12,57 +14,98 @@ require "#{here}/commandline_parser"
12
14
  begin
13
15
  require 'RMagick'
14
16
  rescue LoadError
15
- $stderr.puts 'image sizing disabled - install rmagick'
17
+ $stderr.puts 'WARN: image sizing disabled - install rmagick'
16
18
  end
17
19
 
18
20
  begin
19
21
  require 'pdfkit'
20
22
  rescue LoadError
21
- $stderr.puts 'pdf generation disabled - install pdfkit'
23
+ $stderr.puts 'WARN: pdf generation disabled - install pdfkit'
22
24
  end
23
25
 
24
- begin
25
- require 'rdiscount'
26
- rescue LoadError
27
- require 'bluecloth'
28
- Object.send(:remove_const,:Markdown)
29
- Markdown = BlueCloth
30
- end
26
+ require 'tilt'
31
27
 
32
28
  class ShowOff < Sinatra::Application
33
29
 
34
- Version = VERSION = '0.7.0'
35
-
36
30
  attr_reader :cached_image_size
37
31
 
32
+ # Set up application variables
33
+
38
34
  set :views, File.dirname(__FILE__) + '/../views'
39
- set :public, File.dirname(__FILE__) + '/../public'
35
+ set :public_folder, File.dirname(__FILE__) + '/../public'
36
+
37
+ set :statsdir, "stats"
38
+ set :viewstats, "viewstats.json"
39
+ set :feedback, "feedback.json"
40
+
41
+ set :server, 'thin'
42
+ set :sockets, []
43
+ set :presenters, []
40
44
 
41
45
  set :verbose, false
42
46
  set :pres_dir, '.'
43
47
  set :pres_file, 'showoff.json'
48
+ set :page_size, "Letter"
49
+ set :pres_template, nil
50
+ set :showoff_config, {}
51
+ set :encoding, nil
52
+
53
+ FileUtils.mkdir settings.statsdir unless File.directory? settings.statsdir
54
+
55
+ # Page view time accumulator. Tracks how often slides are viewed by the audience
56
+ begin
57
+ @@counter = JSON.parse(File.read("#{settings.statsdir}/#{settings.viewstats}"))
58
+ rescue
59
+ @@counter = Hash.new
60
+ end
61
+
62
+ @@downloads = Hash.new # Track downloadable files
63
+ @@cookie = nil # presenter cookie. Identifies the presenter for control messages
64
+ @@current = Hash.new # The current slide that the presenter is viewing
44
65
 
45
66
  def initialize(app=nil)
46
67
  super(app)
47
68
  @logger = Logger.new(STDOUT)
48
69
  @logger.formatter = proc { |severity,datetime,progname,msg| "#{progname} #{msg}\n" }
49
- @logger.level = options.verbose ? Logger::DEBUG : Logger::WARN
70
+ @logger.level = settings.verbose ? Logger::DEBUG : Logger::WARN
50
71
 
51
72
  dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
52
73
  @logger.debug(dir)
53
74
 
54
75
  showoff_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
55
- options.pres_dir ||= Dir.pwd
76
+ settings.pres_dir ||= Dir.pwd
56
77
  @root_path = "."
57
78
 
58
- options.pres_dir = File.expand_path(options.pres_dir)
59
- if (options.pres_file)
60
- ShowOffUtils.presentation_config_file = options.pres_file
79
+ settings.pres_dir = File.expand_path(settings.pres_dir)
80
+ if (settings.pres_file)
81
+ ShowOffUtils.presentation_config_file = settings.pres_file
82
+ end
83
+
84
+ # Load configuration for page size and template from the
85
+ # configuration JSON file
86
+ if File.exists?(ShowOffUtils.presentation_config_file)
87
+ showoff_json = JSON.parse(File.read(ShowOffUtils.presentation_config_file))
88
+ settings.showoff_config = showoff_json
89
+
90
+ # Set options for encoding, template and page size
91
+ settings.encoding = showoff_json["encoding"]
92
+ settings.page_size = showoff_json["page-size"] || "Letter"
93
+ settings.pres_template = showoff_json["templates"]
61
94
  end
95
+
96
+ @logger.debug settings.pres_template
97
+
62
98
  @cached_image_size = {}
63
- @logger.debug options.pres_dir
64
- @pres_name = options.pres_dir.split('/').pop
99
+ @logger.debug settings.pres_dir
100
+ @pres_name = settings.pres_dir.split('/').pop
65
101
  require_ruby_files
102
+
103
+ # Default asset path
104
+ @asset_path = "./"
105
+
106
+
107
+ # Initialize Markdown Configuration
108
+ MarkdownConfig::setup(settings.pres_dir)
66
109
  end
67
110
 
68
111
  def self.pres_dir_current
@@ -71,12 +114,12 @@ class ShowOff < Sinatra::Application
71
114
  end
72
115
 
73
116
  def require_ruby_files
74
- Dir.glob("#{options.pres_dir}/*.rb").map { |path| require path }
117
+ Dir.glob("#{settings.pres_dir}/*.rb").map { |path| require path }
75
118
  end
76
119
 
77
120
  helpers do
78
121
  def load_section_files(section)
79
- section = File.join(options.pres_dir, section)
122
+ section = File.join(settings.pres_dir, section)
80
123
  files = if File.directory? section
81
124
  Dir.glob("#{section}/**/*").sort
82
125
  else
@@ -87,23 +130,34 @@ class ShowOff < Sinatra::Application
87
130
  end
88
131
 
89
132
  def css_files
90
- Dir.glob("#{options.pres_dir}/*.css").map { |path| File.basename(path) }
133
+ Dir.glob("#{settings.pres_dir}/*.css").map { |path| File.basename(path) }
91
134
  end
92
135
 
93
136
  def js_files
94
- Dir.glob("#{options.pres_dir}/*.js").map { |path| File.basename(path) }
137
+ Dir.glob("#{settings.pres_dir}/*.js").map { |path| File.basename(path) }
95
138
  end
96
139
 
97
140
 
98
141
  def preshow_files
99
- Dir.glob("#{options.pres_dir}/_preshow/*").map { |path| File.basename(path) }.to_json
142
+ Dir.glob("#{settings.pres_dir}/_preshow/*").map { |path| File.basename(path) }.to_json
100
143
  end
101
144
 
102
145
  # todo: move more behavior into this class
103
146
  class Slide
104
- attr_reader :classes, :text
105
- def initialize classes = ""
106
- @classes = ["content"] + classes.strip.chomp('>').split
147
+ attr_reader :classes, :text, :tpl, :bg
148
+ def initialize( context = "")
149
+
150
+ @tpl = "default"
151
+ @classes = []
152
+
153
+ # Parse the context string for options and content classes
154
+ if context and context.match(/(\[(.*?)\])?(.*)/)
155
+ options = ShowOffUtils.parse_options($2)
156
+ @tpl = options["tpl"] if options["tpl"]
157
+ @bg = options["bg"] if options["bg"]
158
+ @classes += $3.strip.chomp('>').split if $3
159
+ end
160
+
107
161
  @text = ""
108
162
  end
109
163
  def <<(s)
@@ -115,8 +169,13 @@ class ShowOff < Sinatra::Application
115
169
  end
116
170
  end
117
171
 
118
-
119
- def process_markdown(name, content, static=false, pdf=false)
172
+ def process_markdown(name, content, opts={:static=>false, :pdf=>false, :print=>false, :toc=>false, :supplemental=>nil})
173
+ if settings.encoding and content.respond_to?(:force_encoding)
174
+ content.force_encoding(settings.encoding)
175
+ end
176
+ engine_options = ShowOffUtils.showoff_renderer_options(settings.pres_dir)
177
+ @logger.debug "renderer: #{Tilt[:markdown].name}"
178
+ @logger.debug "render options: #{engine_options.inspect}"
120
179
 
121
180
  # if there are no !SLIDE markers, then make every H1 define a new slide
122
181
  unless content =~ /^\<?!SLIDE/m
@@ -131,7 +190,8 @@ class ShowOff < Sinatra::Application
131
190
  until lines.empty?
132
191
  line = lines.shift
133
192
  if line =~ /^<?!SLIDE(.*)>?/
134
- slides << (slide = Slide.new($1))
193
+ ctx = $1 ? $1.strip : $1
194
+ slides << (slide = Slide.new(ctx))
135
195
  else
136
196
  slide << line
137
197
  end
@@ -144,7 +204,35 @@ class ShowOff < Sinatra::Application
144
204
  seq = 1
145
205
  end
146
206
  slides.each do |slide|
147
- md = ''
207
+ # update section counters before we reject slides so the numbering is consistent
208
+ if slide.classes.include? 'subsection'
209
+ @section_major += 1
210
+ @section_minor = 0
211
+ end
212
+
213
+ if opts[:supplemental]
214
+ # if we're looking for supplemental material, only include the content we want
215
+ next unless slide.classes.include? 'supplemental'
216
+ next unless slide.classes.include? opts[:supplemental]
217
+ else
218
+ # otherwise just skip all supplemental material completely
219
+ next if slide.classes.include? 'supplemental'
220
+ end
221
+
222
+ unless opts[:toc]
223
+ # just drop the slide if we're not generating a table of contents
224
+ next if slide.classes.include? 'toc'
225
+ end
226
+
227
+ if opts[:print]
228
+ # drop all slides not intended for the print version
229
+ next if slide.classes.include? 'noprint'
230
+ else
231
+ # drop slides that are intended for the print version only
232
+ next if slide.classes.include? 'printonly'
233
+ end
234
+
235
+ @slide_count += 1
148
236
  content_classes = slide.classes
149
237
 
150
238
  # extract transition, defaulting to none
@@ -153,43 +241,203 @@ class ShowOff < Sinatra::Application
153
241
  # extract id, defaulting to none
154
242
  id = nil
155
243
  content_classes.delete_if { |x| x =~ /^#([\w-]+)/ && id = $1 }
244
+ id = name unless id
156
245
  @logger.debug "id: #{id}" if id
157
246
  @logger.debug "classes: #{content_classes.inspect}"
158
247
  @logger.debug "transition: #{transition}"
159
- # create html
160
- md += "<div"
161
- md += " id=\"#{id}\"" if id
162
- md += " class=\"slide\" data-transition=\"#{transition}\">"
248
+ @logger.debug "tpl: #{slide.tpl} " if slide.tpl
249
+ @logger.debug "bg: #{slide.bg}" if slide.bg
250
+
251
+
252
+ template = "~~~CONTENT~~~"
253
+ # Template handling
254
+ if settings.pres_template
255
+ # We allow specifying a new template even when default is
256
+ # not given.
257
+ if settings.pres_template.include?(slide.tpl) and
258
+ File.exists?(settings.pres_template[slide.tpl])
259
+ template = File.open(settings.pres_template[slide.tpl], "r").read()
260
+ end
261
+ end
262
+
263
+ # create html for the slide
264
+ classes = content_classes.join(' ')
265
+ content = "<div"
266
+ content += " id=\"#{id}\"" if id
267
+ content += " style=\"background: url('file/#{slide.bg}') center no-repeat;\"" if slide.bg
268
+ content += " class=\"slide #{classes}\" data-transition=\"#{transition}\">"
269
+
270
+ # name the slide. If we've got multiple slides in this file, we'll have a sequence number
271
+ # include that sequence number to index directly into that content
163
272
  if seq
164
- md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}/#{seq.to_s}\">\n"
165
- seq += 1
273
+ content += "<div class=\"content #{classes}\" ref=\"#{name}/#{seq.to_s}\">\n"
166
274
  else
167
- md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}\">\n"
275
+ content += "<div class=\"content #{classes}\" ref=\"#{name}\">\n"
276
+ end
277
+
278
+ # Apply the template to the slide and replace the key to generate the content of the slide
279
+ sl = process_content_for_replacements(template.gsub(/~~~CONTENT~~~/, slide.text))
280
+ sl = Tilt[:markdown].new(nil, nil, engine_options) { sl }.render
281
+ sl = update_p_classes(sl)
282
+ sl = process_content_for_section_tags(sl)
283
+ sl = update_special_content(sl, @slide_count, name) # TODO: deprecated
284
+ sl = update_image_paths(name, sl, opts)
285
+
286
+ content += sl
287
+ content += "</div>\n"
288
+ content += "</div>\n"
289
+
290
+ final += update_commandline_code(content)
291
+
292
+ if seq
293
+ seq += 1
168
294
  end
169
- sl = Markdown.new(slide.text).to_html
170
- sl = update_image_paths(name, sl, static, pdf)
171
- md += sl
172
- md += "</div>\n"
173
- md += "</div>\n"
174
- final += update_commandline_code(md)
175
- final = update_p_classes(final)
176
295
  end
177
296
  final
178
297
  end
179
298
 
299
+ # This method processes the content of the slide and replaces
300
+ # content markers with their actual value information
301
+ def process_content_for_replacements(content)
302
+ # update counters, incrementing section:minor if needed
303
+ result = content.gsub("~~~CURRENT_SLIDE~~~", @slide_count.to_s)
304
+ result.gsub!("~~~SECTION:MAJOR~~~", @section_major.to_s)
305
+ if result.include? "~~~SECTION:MINOR~~~"
306
+ @section_minor += 1
307
+ result.gsub!("~~~SECTION:MINOR~~~", @section_minor.to_s)
308
+ end
309
+
310
+ # scan for pagebreak tags. Should really only be used for handout notes or supplemental materials
311
+ result.gsub!("~~~PAGEBREAK~~~", '<div class="break">continued...</div>')
312
+
313
+ # Now check for any kind of options
314
+ content.scan(/(~~~CONFIG:(.*?)~~~)/).each do |match|
315
+ result.gsub!(match[0], settings.showoff_config[match[1]]) if settings.showoff_config.key?(match[1])
316
+ end
317
+
318
+ # Load and replace any file tags
319
+ content.scan(/(~~~FILE:([^:]*):?(.*)?~~~)/).each do |match|
320
+ # get the file content and parse out html entities
321
+ file = HTMLEntities.new.encode(File.read(File.join(settings.pres_dir, '_files', match[1])))
322
+
323
+ # make a list of sh_highlight classes to include
324
+ css = match[2].split.collect {|i| "sh_#{i.downcase}" }.join(' ')
325
+
326
+ result.gsub!(match[0], "<pre class=\"#{css}\"><code>#{file}</code></pre>")
327
+ end
328
+
329
+ result
330
+ end
331
+
332
+ # replace section tags with classed div tags
333
+ def process_content_for_section_tags(content)
334
+ return unless content
335
+
336
+ # because this is post markdown rendering, we may need to shift a <p> tag around
337
+ # remove the tags if they're by themselves
338
+ result = content.gsub(/<p>~~~SECTION:([^~]*)~~~<\/p>/, '<div class="\1">')
339
+ result.gsub!(/<p>~~~ENDSECTION~~~<\/p>/, '</div>')
340
+
341
+ # shove it around the div if it belongs to the contained element
342
+ result.gsub!(/(<p>)?~~~SECTION:([^~]*)~~~/, '<div class="\2">\1')
343
+ result.gsub!(/~~~ENDSECTION~~~(<\/p>)?/, '\1</div>')
344
+
345
+ result
346
+ end
347
+
348
+ def process_content_for_all_slides(content, num_slides, opts={})
349
+ content.gsub!("~~~NUM_SLIDES~~~", num_slides.to_s)
350
+
351
+ # Should we build a table of contents?
352
+ if opts[:toc]
353
+ frag = Nokogiri::HTML::DocumentFragment.parse ""
354
+ toc = Nokogiri::XML::Node.new('div', frag)
355
+ toc['id'] = 'toc'
356
+ frag.add_child(toc)
357
+
358
+ Nokogiri::HTML(content).css('div.subsection > h1').each do |section|
359
+ entry = Nokogiri::XML::Node.new('div', frag)
360
+ entry['class'] = 'tocentry'
361
+ toc.add_child(entry)
362
+
363
+ link = Nokogiri::XML::Node.new('a', frag)
364
+ link['href'] = "##{section.parent.parent['id']}"
365
+ link.content = section.content
366
+ entry.add_child(link)
367
+ end
368
+
369
+ # swap out the tag, if found, with the table of contents
370
+ content.gsub!("~~~TOC~~~", frag.to_html)
371
+ end
372
+
373
+ content
374
+ end
375
+
180
376
  # find any lines that start with a <p>.(something) and turn them into <p class="something">
181
377
  def update_p_classes(markdown)
182
378
  markdown.gsub(/<p>\.(.*?) /, '<p class="\1">')
183
379
  end
184
380
 
185
- def update_image_paths(path, slide, static=false, pdf=false)
381
+ # TODO: deprecated
382
+ def update_special_content(content, seq, name)
383
+ doc = Nokogiri::HTML::DocumentFragment.parse(content)
384
+ %w[notes handouts instructor solguide].each { |mark| update_special_content_mark(doc, mark) }
385
+ update_download_links(doc, seq, name)
386
+
387
+ # TODO: what the bloody hell. Figure out how to either make Nokogiri output closed
388
+ # tags or figure out how to get its XML output to quit adding gratuitious spaces.
389
+ doc.to_html.gsub(/(<img [^>]*)>/, '\1 />')
390
+ end
391
+
392
+ # TODO: deprecated
393
+ def update_special_content_mark(doc, mark)
394
+ container = doc.css("p.#{mark}").first
395
+ return unless container
396
+
397
+ @logger.warn "Special mark (#{mark}) is deprecated. Please replace with section tags. See the README for details."
398
+
399
+ # only allow localhost to print the instructor guide
400
+ if mark == 'instructor' and request.env['REMOTE_HOST'] != 'localhost'
401
+ container.remove
402
+ else
403
+ raw = container.inner_html
404
+ fixed = raw.gsub(/^\.#{mark} ?/, '')
405
+ markdown = Tilt[:markdown].new { fixed }.render
406
+
407
+ container.name = 'div'
408
+ container.inner_html = markdown
409
+ end
410
+ end
411
+ private :update_special_content_mark
412
+
413
+ def update_download_links(doc, seq, name)
414
+ container = doc.css("p.download").first
415
+ return unless container
416
+
417
+ raw = container.text
418
+ fixed = raw.gsub(/^\.download ?/, '')
419
+
420
+ # first create the data structure
421
+ # [ enabled, slide name, [array, of, files] ]
422
+ @@downloads[seq] = [ false, name, [] ]
423
+
424
+ fixed.split("\n").each { |file|
425
+ # then push each file onto the list
426
+ @@downloads[seq][2].push(file.strip)
427
+ }
428
+
429
+ container.remove
430
+ end
431
+ private :update_download_links
432
+
433
+ def update_image_paths(path, slide, opts={:static=>false, :pdf=>false})
186
434
  paths = path.split('/')
187
435
  paths.pop
188
436
  path = paths.join('/')
189
- replacement_prefix = static ?
190
- ( pdf ? %(img src="file://#{options.pres_dir}/#{path}) : %(img src="./file/#{path}) ) :
191
- %(img src="/image/#{path})
192
- slide.gsub(/img src=\"([^\/].*?)\"/) do |s|
437
+ replacement_prefix = opts[:static] ?
438
+ ( opts[:pdf] ? %(img src="file://#{settings.pres_dir}/#{path}) : %(img src="./file/#{path}) ) :
439
+ %(img src="#{@asset_path}image/#{path})
440
+ slide.gsub(/img src=[\"\'](?!https?:\/\/)([^\/].*?)[\"\']/) do |s|
193
441
  img_path = File.join(path, $1)
194
442
  w, h = get_image_size(img_path)
195
443
  src = %(#{replacement_prefix}/#{$1}")
@@ -228,7 +476,7 @@ class ShowOff < Sinatra::Application
228
476
  lines = out.split("\n")
229
477
  if lines.first.strip[0, 3] == '@@@'
230
478
  lang = lines.shift.gsub('@@@', '').strip
231
- pre.set_attribute('class', 'sh_' + lang.downcase)
479
+ pre.set_attribute('class', 'sh_' + lang.downcase) if !lang.empty?
232
480
  code.content = lines.join("\n")
233
481
  end
234
482
  end
@@ -264,28 +512,32 @@ class ShowOff < Sinatra::Application
264
512
  html.root.to_s
265
513
  end
266
514
 
267
- def get_slides_html(static=false, pdf=false)
268
- sections = ShowOffUtils.showoff_sections(options.pres_dir, @logger)
515
+ def get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil})
516
+ @slide_count = 0
517
+ @section_major = 0
518
+ @section_minor = 0
519
+
520
+ sections = ShowOffUtils.showoff_sections(settings.pres_dir, @logger)
269
521
  files = []
270
522
  if sections
271
523
  data = ''
272
524
  sections.each do |section|
273
525
  if section =~ /^#/
274
526
  name = section.each_line.first.gsub(/^#*/,'').strip
275
- data << process_markdown(name, "<!SLIDE subsection>\n" + section, static, pdf)
527
+ data << process_markdown(name, "<!SLIDE subsection>\n" + section, opts)
276
528
  else
277
529
  files = []
278
530
  files << load_section_files(section)
279
531
  files = files.flatten
280
- files = files.select { |f| f =~ /.md/ }
532
+ files = files.select { |f| f =~ /.md$/ }
281
533
  files.each do |f|
282
- fname = f.gsub(options.pres_dir + '/', '').gsub('.md', '')
283
- data << process_markdown(fname, File.read(f), static, pdf)
534
+ fname = f.gsub(settings.pres_dir + '/', '').gsub('.md', '')
535
+ data << process_markdown(fname, File.read(f), opts)
284
536
  end
285
537
  end
286
538
  end
287
539
  end
288
- data
540
+ process_content_for_all_slides(data, @slide_count, opts)
289
541
  end
290
542
 
291
543
  def inline_css(csses, pre = nil)
@@ -294,7 +546,7 @@ class ShowOff < Sinatra::Application
294
546
  if pre
295
547
  css_file = File.join(File.dirname(__FILE__), '..', pre, css_file)
296
548
  else
297
- css_file = File.join(options.pres_dir, css_file)
549
+ css_file = File.join(settings.pres_dir, css_file)
298
550
  end
299
551
  css_content += File.read(css_file)
300
552
  end
@@ -308,9 +560,15 @@ class ShowOff < Sinatra::Application
308
560
  if pre
309
561
  js_file = File.join(File.dirname(__FILE__), '..', pre, js_file)
310
562
  else
311
- js_file = File.join(options.pres_dir, js_file)
563
+ js_file = File.join(settings.pres_dir, js_file)
564
+ end
565
+
566
+ begin
567
+ js_content += File.read(js_file)
568
+ rescue Errno::ENOENT
569
+ $stderr.puts "WARN: Failed to inline JS. No such file: #{js_file}"
570
+ next
312
571
  end
313
- js_content += File.read(js_file)
314
572
  end
315
573
  js_content += '</script>'
316
574
  js_content
@@ -322,14 +580,25 @@ class ShowOff < Sinatra::Application
322
580
 
323
581
  def index(static=false)
324
582
  if static
325
- @title = ShowOffUtils.showoff_title
326
- @slides = get_slides_html(static)
583
+ @title = ShowOffUtils.showoff_title(settings.pres_dir)
584
+ @slides = get_slides_html(:static=>static)
585
+ @pause_msg = ShowOffUtils.pause_msg
586
+
587
+ # Identify which languages to bundle for highlighting
588
+ @languages = @slides.scan(/<pre class=".*(?!sh_sourceCode)(sh_[\w-]+).*"/).uniq.map{ |w| "sh_lang/#{w[0]}.min.js"}
589
+
327
590
  @asset_path = "./"
328
591
  end
592
+
593
+ # Check to see if the presentation has enabled feedback
594
+ @feedback = settings.showoff_config['feedback']
329
595
  erb :index
330
596
  end
331
597
 
332
598
  def presenter
599
+ @issues = settings.showoff_config['issues']
600
+ @@cookie ||= guid()
601
+ response.set_cookie('presenter', @@cookie)
333
602
  erb :presenter
334
603
  end
335
604
 
@@ -361,33 +630,89 @@ class ShowOff < Sinatra::Application
361
630
  assets << href if href
362
631
  end
363
632
 
364
- css = Dir.glob("#{options.public}/**/*.css").map { |path| path.gsub(options.public + '/', '') }
633
+ css = Dir.glob("#{settings.public_folder}/**/*.css").map { |path| path.gsub(settings.public_folder + '/', '') }
365
634
  assets << css
366
635
 
367
- js = Dir.glob("#{options.public}/**/*.js").map { |path| path.gsub(options.public + '/', '') }
636
+ js = Dir.glob("#{settings.public_folder}/**/*.js").map { |path| path.gsub(settings.public_folder + '/', '') }
368
637
  assets << js
369
638
 
370
639
  assets.uniq.join("\n")
371
640
  end
372
641
 
373
642
  def slides(static=false)
374
- get_slides_html(static)
643
+ get_slides_html(:static=>static)
375
644
  end
376
645
 
377
646
  def onepage(static=false)
378
- @slides = get_slides_html(static)
647
+ @slides = get_slides_html(:static=>static, :toc=>true)
648
+ #@languages = @slides.scan(/<pre class=".*(?!sh_sourceCode)(sh_[\w-]+).*"/).uniq.map{ |w| "/sh_lang/#{w[0]}.min.js"}
379
649
  erb :onepage
380
650
  end
381
651
 
652
+ def print(static=false)
653
+ @slides = get_slides_html(:static=>static, :toc=>true, :print=>true)
654
+ erb :onepage
655
+ end
656
+
657
+ def supplemental(content, static=false)
658
+ @slides = get_slides_html(:static=>static, :supplemental=>content)
659
+ @wrapper_classes = ['supplemental']
660
+ erb :onepage
661
+ end
662
+
663
+ def download()
664
+ begin
665
+ shared = Dir.glob("#{settings.pres_dir}/_files/share/*").map { |path| File.basename(path) }
666
+ # We use the icky -999 magic index because it has to be comparable for the view sort
667
+ @downloads = { -999 => [ true, 'Shared Files', shared ] }
668
+ rescue Errno::ENOENT => e
669
+ # don't fail if the directory doesn't exist
670
+ @downloads = {}
671
+ end
672
+ @downloads.merge! @@downloads
673
+ erb :download
674
+ end
675
+
676
+ def stats()
677
+ if request.env['REMOTE_HOST'] == 'localhost'
678
+ # the presenter should have full stats
679
+ @counter = @@counter
680
+ end
681
+
682
+ @all = Hash.new
683
+ @@counter.each do |slide, stats|
684
+ @all[slide] = 0
685
+ stats.map { |host, count| @all[slide] += count }
686
+ end
687
+
688
+ # most and least five viewed slides
689
+ @least = @all.sort_by {|slide, time| time}[0..4]
690
+ @most = @all.sort_by {|slide, time| -time}[0..4]
691
+
692
+ erb :stats
693
+ end
694
+
382
695
  def pdf(static=true)
383
- @slides = get_slides_html(static, true)
384
- @no_js = false
696
+ @slides = get_slides_html(:static=>static, :pdf=>true)
697
+ @inline = true
698
+
699
+ # Identify which languages to bundle for highlighting
700
+ @languages = @slides.scan(/<pre class=".*(?!sh_sourceCode)(sh_[\w-]+).*"/).uniq.map{ |w| "/sh_lang/#{w[0]}.min.js"}
701
+
385
702
  html = erb :onepage
386
703
  # TODO make a random filename
387
704
 
705
+ # Process inline css and js for included images
706
+ # The css uses relative paths for images and we prepend the file url
707
+ html.gsub!(/url\([\"\']?(?!https?:\/\/)(.*?)[\"\']?\)/) do |s|
708
+ "url(file://#{settings.pres_dir}/#{$1})"
709
+ end
710
+
711
+ # Todo fix javascript path
712
+
388
713
  # PDFKit.new takes the HTML and any options for wkhtmltopdf
389
714
  # run `wkhtmltopdf --extended-help` for a full list of options
390
- kit = PDFKit.new(html, :page_size => 'Letter', :orientation => 'Landscape')
715
+ kit = PDFKit.new(html, ShowOffUtils.showoff_pdf_options(settings.pres_dir))
391
716
 
392
717
  # Save the PDF to a file
393
718
  file = kit.to_file('/tmp/preso.pdf')
@@ -399,14 +724,16 @@ class ShowOff < Sinatra::Application
399
724
  def self.do_static(what)
400
725
  what = "index" if !what
401
726
 
402
- # Nasty hack to get the actual ShowOff module
403
- showoff = ShowOff.new
404
- while !showoff.is_a?(ShowOff)
405
- showoff = showoff.instance_variable_get(:@app)
406
- end
727
+ # Sinatra now aliases new to new!
728
+ # https://github.com/sinatra/sinatra/blob/v1.3.3/lib/sinatra/base.rb#L1369
729
+ showoff = ShowOff.new!
730
+
407
731
  name = showoff.instance_variable_get(:@pres_name)
408
732
  path = showoff.instance_variable_get(:@root_path)
733
+ logger = showoff.instance_variable_get(:@logger)
734
+
409
735
  data = showoff.send(what, true)
736
+
410
737
  if data.is_a?(File)
411
738
  FileUtils.cp(data.path, "#{name}.pdf")
412
739
  else
@@ -433,7 +760,7 @@ class ShowOff < Sinatra::Application
433
760
  # Set up file dir
434
761
  file_dir = File.join(out, 'file')
435
762
  FileUtils.makedirs(file_dir)
436
- pres_dir = showoff.options.pres_dir
763
+ pres_dir = showoff.settings.pres_dir
437
764
 
438
765
  # ..., copy all user-defined styles and javascript files
439
766
  Dir.glob("#{pres_dir}/*.{css,js}").each { |path|
@@ -441,17 +768,21 @@ class ShowOff < Sinatra::Application
441
768
  }
442
769
 
443
770
  # ... and copy all needed image files
444
- data.scan(/img src=\".\/file\/(.*?)\"/).flatten.each do |path|
445
- dir = File.dirname(path)
446
- FileUtils.makedirs(File.join(file_dir, dir))
447
- FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
771
+ [/img src=[\"\'].\/file\/(.*?)[\"\']/, /style=[\"\']background: url\(\'file\/(.*?)'/].each do |regex|
772
+ data.scan(regex).flatten.each do |path|
773
+ dir = File.dirname(path)
774
+ FileUtils.makedirs(File.join(file_dir, dir))
775
+ FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
776
+ end
448
777
  end
449
778
  # copy images from css too
450
779
  Dir.glob("#{pres_dir}/*.css").each do |css_path|
451
780
  File.open(css_path) do |file|
452
781
  data = file.read
453
- data.scan(/url\((.*)\)/).flatten.each do |path|
454
- @logger.debug path
782
+ data.scan(/url\([\"\']?(?!https?:\/\/)(.*?)[\"\']?\)/).flatten.each do |path|
783
+ path.gsub!(/(\#.*)$/, '') # get rid of the anchor
784
+ path.gsub!(/(\?.*)$/, '') # get rid of the query
785
+ logger.debug path
455
786
  dir = File.dirname(path)
456
787
  FileUtils.makedirs(File.join(file_dir, dir))
457
788
  FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
@@ -467,6 +798,35 @@ class ShowOff < Sinatra::Application
467
798
  e.message
468
799
  end
469
800
 
801
+ # Basic auth boilerplate
802
+ def protected!
803
+ unless authorized?
804
+ response['WWW-Authenticate'] = %(Basic realm="#{@title}: Protected Area")
805
+ throw(:halt, [401, "Not authorized\n"])
806
+ end
807
+ end
808
+
809
+ def authorized?
810
+ if not settings.showoff_config.has_key? 'password'
811
+ # if no password is set, then default to allowing access to localhost
812
+ request.env['REMOTE_HOST'] == 'localhost' or request.ip == '127.0.0.1'
813
+ else
814
+ auth ||= Rack::Auth::Basic::Request.new(request.env)
815
+ user = settings.showoff_config['user'] || ''
816
+ password = settings.showoff_config['password']
817
+ auth.provided? && auth.basic? && auth.credentials && auth.credentials == [user, password]
818
+ end
819
+ end
820
+
821
+ def guid
822
+ # this is a terrifyingly simple GUID generator
823
+ (0..15).to_a.map{|a| rand(16).to_s(16)}.join
824
+ end
825
+
826
+ def valid_cookie
827
+ (request.cookies['presenter'] == @@cookie)
828
+ end
829
+
470
830
  get '/eval_ruby' do
471
831
  return eval_ruby(params[:code]) if ENV['SHOWOFF_EVAL_RUBY']
472
832
 
@@ -475,20 +835,169 @@ class ShowOff < Sinatra::Application
475
835
 
476
836
  get %r{(?:image|file)/(.*)} do
477
837
  path = params[:captures].first
478
- full_path = File.join(options.pres_dir, path)
479
- send_file full_path
838
+ full_path = File.join(settings.pres_dir, path)
839
+ if File.exist?(full_path)
840
+ send_file full_path
841
+ else
842
+ raise Sinatra::NotFound
843
+ end
844
+ end
845
+
846
+ get '/control' do
847
+ if !request.websocket?
848
+ raise Sinatra::NotFound
849
+ else
850
+ request.websocket do |ws|
851
+ ws.onopen do
852
+ ws.send( { 'current' => @@current[:number] }.to_json )
853
+ settings.sockets << ws
854
+
855
+ @logger.warn "Open sockets: #{settings.sockets.size}"
856
+ end
857
+ ws.onmessage do |data|
858
+ begin
859
+ control = JSON.parse(data)
860
+
861
+ @logger.warn "#{control.inspect}"
862
+
863
+ case control['message']
864
+ when 'update'
865
+ # websockets don't use the same auth standards
866
+ # we use a session cookie to identify the presenter
867
+ if valid_cookie()
868
+ name = control['name']
869
+ slide = control['slide'].to_i
870
+
871
+ # check to see if we need to enable a download link
872
+ if @@downloads.has_key?(slide)
873
+ @logger.debug "Enabling file download for slide #{name}"
874
+ @@downloads[slide][0] = true
875
+ end
876
+
877
+ # update the current slide pointer
878
+ @logger.debug "Updated current slide to #{name}"
879
+ @@current = { :name => name, :number => slide }
880
+
881
+ # schedule a notification for all clients
882
+ EM.next_tick { settings.sockets.each{|s| s.send({ 'current' => @@current[:number] }.to_json) } }
883
+ end
884
+
885
+ when 'register'
886
+ # save a list of presenters
887
+ if valid_cookie()
888
+ remote = request.env['REMOTE_HOST'] || request.env['REMOTE_ADDR']
889
+ settings.presenters << ws
890
+ @logger.warn "Registered new presenter: #{remote}"
891
+ end
892
+
893
+ when 'track'
894
+ remote = request.env['REMOTE_HOST'] || request.env['REMOTE_ADDR']
895
+ slide = control['slide']
896
+ time = control['time'].to_f
897
+
898
+ @logger.debug "Logged #{time} on slide #{slide} for #{remote}"
899
+
900
+ # a bucket for this slide
901
+ @@counter[slide] ||= Hash.new
902
+ # a bucket of slideviews for this address
903
+ @@counter[slide][remote] ||= Array.new
904
+ # and add this slide viewing to the bucket
905
+ @@counter[slide][remote] << { :elapsed => time, :timestamp => Time.now.to_i, :presenter => @@current[:name] }
906
+
907
+ when 'position'
908
+ ws.send( { 'current' => @@current[:number] }.to_json ) unless @@cookie.nil?
909
+
910
+ when 'pace', 'question'
911
+ # just forward to the presenter(s)
912
+ EM.next_tick { settings.presenters.each{|s| s.send(data) } }
913
+
914
+ when 'feedback'
915
+ filename = "#{settings.statsdir}/#{settings.feedback}"
916
+ slide = control['slide']
917
+ rating = control['rating']
918
+ feedback = control['feedback']
919
+
920
+ begin
921
+ log = JSON.parse(File.read(filename))
922
+ rescue
923
+ # do nothing
924
+ end
925
+
926
+ log ||= Hash.new
927
+ log[slide] ||= Array.new
928
+ log[slide] << { :rating => rating, :feedback => feedback }
929
+
930
+ if settings.verbose then
931
+ File.write(filename, JSON.pretty_generate(log))
932
+ else
933
+ File.write(filename, log.to_json)
934
+ end
935
+
936
+ else
937
+ @logger.warn "Unknown message <#{control['message']}> received."
938
+ @logger.warn control.inspect
939
+ end
940
+
941
+ rescue Exception => e
942
+ @logger.warn "Messaging error: #{e}"
943
+ end
944
+ end
945
+ ws.onclose do
946
+ @logger.warn("websocket closed")
947
+ settings.sockets.delete(ws)
948
+ end
949
+ end
950
+ end
480
951
  end
481
952
 
482
- get %r{/(.*)} do
483
- @title = ShowOffUtils.showoff_title
953
+ # gawd, this whole routing scheme is bollocks
954
+ get %r{/([^/]*)/?([^/]*)} do
955
+ @title = ShowOffUtils.showoff_title(settings.pres_dir)
956
+ @pause_msg = ShowOffUtils.pause_msg
484
957
  what = params[:captures].first
958
+ opt = params[:captures][1]
485
959
  what = 'index' if "" == what
486
- if (what != "favicon.ico")
487
- data = send(what)
488
- if data.is_a?(File)
489
- send_file data.path
960
+
961
+ if settings.showoff_config.has_key? 'protected'
962
+ protected! if settings.showoff_config['protected'].include? what
963
+ end
964
+
965
+ # this hasn't been set to anything remotely interesting for a long time now
966
+ @asset_path = nil
967
+
968
+ begin
969
+ if (what != "favicon.ico")
970
+ if what == 'supplemental'
971
+ data = send(what, opt)
972
+ else
973
+ data = send(what)
974
+ end
975
+ if data.is_a?(File)
976
+ send_file data.path
977
+ else
978
+ data
979
+ end
980
+ end
981
+ rescue NoMethodError => e
982
+ @logger.warn "Invalid object #{what} requested."
983
+ raise Sinatra::NotFound
984
+ end
985
+ end
986
+
987
+ not_found do
988
+ # Why does the asset path start from cwd??
989
+ @asset_path.slice!(/^./)
990
+ @env = request.env
991
+ erb :'404'
992
+ end
993
+
994
+ at_exit do
995
+ if defined?(@@counter)
996
+ filename = "#{settings.statsdir}/#{settings.viewstats}"
997
+ if settings.verbose then
998
+ File.write(filename, JSON.pretty_generate(@@counter))
490
999
  else
491
- data
1000
+ File.write(filename, @@counter.to_json)
492
1001
  end
493
1002
  end
494
1003
  end