showoff 0.12.0 → 0.12.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 172a00f574e54f000a40fad51bfeb2136ca176d0
4
- data.tar.gz: f0702e207b967d9a6328de5a4b47a627553b45db
3
+ metadata.gz: c0547aee458c3417a677758cdf70ea8a063e2d89
4
+ data.tar.gz: ecb30bc4b56493533a5b70327cedec5b75d2475f
5
5
  SHA512:
6
- metadata.gz: 6fab38a39045cd59cf80ee4acc9d29980d7d2f7693eb471207fdae0a5547ee82481152bd9522e658b33a23018f4e4382efb67b054878b2b89bcf075dec87d554
7
- data.tar.gz: 14f92c4fc37add9e2f8eedacbc2c04b474acf71ddc3acc4526b11f53be1e469e94c1d3eee936e6cf2f23b85ab825935dae5c9d14efb8794057d5d82d299aa6f3
6
+ metadata.gz: 58700ccb6dd6d381acb170486ceb0723150a4b6b3e1fd4e8d9be4cb8aa20f10b63c6edc37c4e66a69da546759f5bbbf0e81aaafe949c7c5ba76abd56afea370c
7
+ data.tar.gz: 1100e728e8f85fdba9966c861d2ada537967cd3f985290774958a2058562b959a72c5f9f682b56fe4ce9184ef6eadf5b3294bc009e5d6076a00fbbb435250a49
data/bin/showoff CHANGED
@@ -58,6 +58,20 @@ command [:create,:init] do |c|
58
58
  end
59
59
  end
60
60
 
61
+ desc 'Build a showoff presentation from a showoff.json outling'
62
+ arg_name 'dir_name'
63
+ long_desc 'This command helps start a new showoff presentation by creating each slide listing in the showoff.json file.'
64
+ command [:skeleton] do |c|
65
+
66
+ c.desc 'alternate json filename'
67
+ c.flag [:f,:file]
68
+
69
+ c.action do |global_options,options,args|
70
+ ShowOffUtils.skeleton(options[:f])
71
+ puts "done. run 'showoff serve' to see your slideshow"
72
+ end
73
+ end
74
+
61
75
  desc 'Puts your showoff presentation into a gh-pages branch'
62
76
  long_desc 'Generates a static version of your presentation into your gh-pages branch for publishing to GitHub Pages'
63
77
  command :github do |c|
@@ -125,6 +139,9 @@ command :serve do |c|
125
139
  c.desc 'Enable remote code execution'
126
140
  c.switch [:x, :executecode]
127
141
 
142
+ c.desc 'Run in standalone mode, with no audience interaction'
143
+ c.switch [:S, :standalone]
144
+
128
145
  c.desc 'Disable content caching'
129
146
  c.switch :nocache
130
147
 
@@ -162,6 +179,7 @@ command :serve do |c|
162
179
  options[:ssl] ||= config['ssl']
163
180
  options[:ssl_certificate] ||= config['ssl_certificate']
164
181
  options[:ssl_private_key] ||= config['ssl_private_key']
182
+ options[:standalone] ||= config['standalone']
165
183
 
166
184
  protocol = options[:ssl] ? 'https' : 'http'
167
185
  host = options[:host] == '0.0.0.0' ? 'localhost' : options[:host]
@@ -179,15 +197,16 @@ To run it from presenter view, go to: [ #{url}/presenter ]
179
197
 
180
198
  "
181
199
 
182
- ShowOff.run!( :host => options[:host],
183
- :port => options[:port].to_i,
184
- :pres_file => options[:file],
185
- :pres_dir => args[0],
186
- :verbose => options[:verbose],
187
- :review => options[:review],
188
- :execute => options[:executecode],
189
- :nocache => options[:nocache],
190
- :bind => options[:host],
200
+ ShowOff.run!( :host => options[:host],
201
+ :port => options[:port].to_i,
202
+ :pres_file => options[:file],
203
+ :pres_dir => args[0],
204
+ :verbose => options[:verbose],
205
+ :review => options[:review],
206
+ :execute => options[:executecode],
207
+ :nocache => options[:nocache],
208
+ :bind => options[:host],
209
+ :standalone => options[:standalone],
191
210
  ) do |server|
192
211
  if options[:ssl]
193
212
  ssl_options = {
data/lib/keymap.rb CHANGED
@@ -24,7 +24,151 @@ module Keymap
24
24
  'p' => 'PAUSE',
25
25
  'P' => 'PRESHOW',
26
26
  'x' => 'EXECUTE',
27
- 'f5' => 'EXECUTE',
27
+ 'f5' => 'EXECUTE',
28
28
  }
29
29
  end
30
+
31
+ def self.keycodeDictionary()
32
+ {
33
+ "0" => "\\",
34
+ "8" => "backspace",
35
+ "9" => "tab",
36
+ "12" => "num",
37
+ "13" => "enter",
38
+ "16" => "shift",
39
+ "17" => "ctrl",
40
+ "18" => "alt",
41
+ "19" => "pause",
42
+ "20" => "caps",
43
+ "27" => "esc",
44
+ "32" => "space",
45
+ "33" => "pageup",
46
+ "34" => "pagedown",
47
+ "35" => "end",
48
+ "36" => "home",
49
+ "37" => "left",
50
+ "38" => "up",
51
+ "39" => "right",
52
+ "40" => "down",
53
+ "44" => "print",
54
+ "45" => "insert",
55
+ "46" => "delete",
56
+ "48" => "0",
57
+ "49" => "1",
58
+ "50" => "2",
59
+ "51" => "3",
60
+ "52" => "4",
61
+ "53" => "5",
62
+ "54" => "6",
63
+ "55" => "7",
64
+ "56" => "8",
65
+ "57" => "9",
66
+ "59" => ";",
67
+ "61" => "=",
68
+ "65" => "a",
69
+ "66" => "b",
70
+ "67" => "c",
71
+ "68" => "d",
72
+ "69" => "e",
73
+ "70" => "f",
74
+ "71" => "g",
75
+ "72" => "h",
76
+ "73" => "i",
77
+ "74" => "j",
78
+ "75" => "k",
79
+ "76" => "l",
80
+ "77" => "m",
81
+ "78" => "n",
82
+ "79" => "o",
83
+ "80" => "p",
84
+ "81" => "q",
85
+ "82" => "r",
86
+ "83" => "s",
87
+ "84" => "t",
88
+ "85" => "u",
89
+ "86" => "v",
90
+ "87" => "w",
91
+ "88" => "x",
92
+ "89" => "y",
93
+ "90" => "z",
94
+ "91" => "cmd",
95
+ "92" => "cmd",
96
+ "93" => "cmd",
97
+ "96" => "num_0",
98
+ "97" => "num_1",
99
+ "98" => "num_2",
100
+ "99" => "num_3",
101
+ "100" => "num_4",
102
+ "101" => "num_5",
103
+ "102" => "num_6",
104
+ "103" => "num_7",
105
+ "104" => "num_8",
106
+ "105" => "num_9",
107
+ "106" => "num_multiply",
108
+ "107" => "num_add",
109
+ "108" => "num_enter",
110
+ "109" => "num_subtract",
111
+ "110" => "num_decimal",
112
+ "111" => "num_divide",
113
+ "112" => "f1",
114
+ "113" => "f2",
115
+ "114" => "f3",
116
+ "115" => "f4",
117
+ "116" => "f5",
118
+ "117" => "f6",
119
+ "118" => "f7",
120
+ "119" => "f8",
121
+ "120" => "f9",
122
+ "121" => "f10",
123
+ "122" => "f11",
124
+ "123" => "f12",
125
+ "124" => "print",
126
+ "144" => "num",
127
+ "145" => "scroll",
128
+ "173" => "-",
129
+ "186" => ";",
130
+ "187" => "=",
131
+ "188" => ",",
132
+ "189" => "-",
133
+ "190" => ".",
134
+ "191" => "/",
135
+ "192" => "`",
136
+ "219" => "[",
137
+ "220" => "\\",
138
+ "221" => "]",
139
+ "222" => "'",
140
+ "223" => "`",
141
+ "224" => "cmd",
142
+ "225" => "alt",
143
+ "57392" => "ctrl",
144
+ "63289" => "num",
145
+ }
146
+ end
147
+
148
+ def self.shiftedKeyDictionary()
149
+ {
150
+ "0" => ")",
151
+ "1" => "!",
152
+ "2" => "@",
153
+ "3" => "#",
154
+ "4" => "$",
155
+ "5" => "%",
156
+ "6" => "^",
157
+ "7" => "&",
158
+ "8" => "*",
159
+ "9" => "(",
160
+ "/" => "?",
161
+ "." => ">",
162
+ "," => "<",
163
+ "'" => "\"",
164
+ ";" => ":",
165
+ "[" => "{",
166
+ "]" => "}",
167
+ "\\" => "|",
168
+ "`" => "~",
169
+ "=" => "+",
170
+ "-" => "_",
171
+ }
172
+ end
173
+
30
174
  end
data/lib/showoff.rb CHANGED
@@ -3,6 +3,7 @@ require 'sinatra/base'
3
3
  require 'json'
4
4
  require 'nokogiri'
5
5
  require 'fileutils'
6
+ require 'pathname'
6
7
  require 'logger'
7
8
  require 'htmlentities'
8
9
  require 'sinatra-websocket'
@@ -14,6 +15,15 @@ require "#{here}/keymap"
14
15
 
15
16
  begin
16
17
  require 'rmagick'
18
+ puts "********************************************************************************"
19
+ puts " RMagick support has been deprecated."
20
+ puts
21
+ puts "CSS auto-scaling has improved greatly, and the image manipulation should no"
22
+ puts "longer be required. If you have images that don't scale properly, then you"
23
+ puts "should write custom styles to size them appropriately."
24
+ puts
25
+ puts " RMagic support will be removed completely in the next release."
26
+ puts "********************************************************************************"
17
27
  rescue LoadError
18
28
  # nop
19
29
  end
@@ -76,6 +86,10 @@ class ShowOff < Sinatra::Application
76
86
  @keymap = Keymap.default
77
87
  @keymap.merge! JSON.parse(File.read(keymapfile)) rescue {}
78
88
 
89
+ # map keys to the labels we're using
90
+ @keycode_dictionary = Keymap.keycodeDictionary
91
+ @keycode_shifted_keys = Keymap.shiftedKeyDictionary
92
+
79
93
  settings.pres_dir = File.expand_path(settings.pres_dir)
80
94
  if (settings.pres_file)
81
95
  ShowOffUtils.presentation_config_file = settings.pres_file
@@ -83,7 +97,7 @@ class ShowOff < Sinatra::Application
83
97
 
84
98
  # Load configuration for page size and template from the
85
99
  # configuration JSON file
86
- if File.exists?(ShowOffUtils.presentation_config_file)
100
+ if File.exist?(ShowOffUtils.presentation_config_file)
87
101
  showoff_json = JSON.parse(File.read(ShowOffUtils.presentation_config_file))
88
102
  settings.showoff_config = showoff_json
89
103
 
@@ -96,6 +110,24 @@ class ShowOff < Sinatra::Application
96
110
  # code execution timeout
97
111
  settings.showoff_config['timeout'] ||= 15
98
112
 
113
+ # If favicon in presentation root, use it by default
114
+ if File.exist? 'favicon.ico'
115
+ settings.showoff_config['favicon'] ||= 'file/favicon.ico'
116
+ end
117
+
118
+ # default protection levels
119
+ if settings.showoff_config.has_key? 'password'
120
+ settings.showoff_config['protected'] ||= ["presenter", "onepage", "print"]
121
+ else
122
+ settings.showoff_config['protected'] ||= Array.new
123
+ end
124
+
125
+ if settings.showoff_config.has_key? 'key'
126
+ settings.showoff_config['locked'] ||= ["slides"]
127
+ else
128
+ settings.showoff_config['locked'] ||= Array.new
129
+ end
130
+
99
131
  # highlightjs syntax style
100
132
  @highlightStyle = settings.showoff_config['highlight'] || 'default'
101
133
 
@@ -115,8 +147,11 @@ class ShowOff < Sinatra::Application
115
147
  # Default asset path
116
148
  @asset_path = "./"
117
149
 
150
+ # invert the logic to maintain backwards compatibility of interactivity on by default
151
+ @interactive = ! settings.standalone rescue false
152
+
118
153
  # Create stats directory
119
- FileUtils.mkdir settings.statsdir unless File.directory? settings.statsdir
154
+ FileUtils.mkdir settings.statsdir unless File.directory? settings.statsdir if @interactive
120
155
 
121
156
  # Page view time accumulator. Tracks how often slides are viewed by the audience
122
157
  begin
@@ -142,11 +177,13 @@ class ShowOff < Sinatra::Application
142
177
  @@current = Hash.new # The current slide that the presenter is viewing
143
178
  @@cache = nil # Cache slide content for subsequent hits
144
179
 
145
- # flush stats to disk periodically
146
- Thread.new do
147
- loop do
148
- sleep 30
149
- ShowOff.flush
180
+ if @interactive
181
+ # flush stats to disk periodically
182
+ Thread.new do
183
+ loop do
184
+ sleep 30
185
+ ShowOff.flush
186
+ end
150
187
  end
151
188
  end
152
189
 
@@ -156,24 +193,27 @@ class ShowOff < Sinatra::Application
156
193
 
157
194
  # save stats to disk
158
195
  def self.flush
159
- if defined?(@@counter) and not @@counter.empty?
160
- File.open("#{settings.statsdir}/#{settings.viewstats}", 'w') do |f|
161
- if settings.verbose then
162
- f.write(JSON.pretty_generate(@@counter))
163
- else
164
- f.write(@@counter.to_json)
196
+ begin
197
+ if defined?(@@counter) and not @@counter.empty?
198
+ File.open("#{settings.statsdir}/#{settings.viewstats}", 'w') do |f|
199
+ if settings.verbose then
200
+ f.write(JSON.pretty_generate(@@counter))
201
+ else
202
+ f.write(@@counter.to_json)
203
+ end
165
204
  end
166
205
  end
167
- end
168
206
 
169
- if defined?(@@forms) and not @@forms.empty?
170
- File.open("#{settings.statsdir}/#{settings.forms}", 'w') do |f|
171
- if settings.verbose then
172
- f.write(JSON.pretty_generate(@@forms))
173
- else
174
- f.write(@@forms.to_json)
207
+ if defined?(@@forms) and not @@forms.empty?
208
+ File.open("#{settings.statsdir}/#{settings.forms}", 'w') do |f|
209
+ if settings.verbose then
210
+ f.write(JSON.pretty_generate(@@forms))
211
+ else
212
+ f.write(@@forms.to_json)
213
+ end
175
214
  end
176
215
  end
216
+ rescue Errno::ENOENT => e
177
217
  end
178
218
  end
179
219
 
@@ -248,7 +288,7 @@ class ShowOff < Sinatra::Application
248
288
  end
249
289
  end
250
290
 
251
- def process_markdown(name, content, opts={:static=>false, :pdf=>false, :print=>false, :toc=>false, :supplemental=>nil})
291
+ def process_markdown(name, content, opts={:static=>false, :pdf=>false, :print=>false, :toc=>false, :supplemental=>nil, :section=>nil})
252
292
  if settings.encoding and content.respond_to?(:force_encoding)
253
293
  content.force_encoding(settings.encoding)
254
294
  end
@@ -337,7 +377,7 @@ class ShowOff < Sinatra::Application
337
377
  # We allow specifying a new template even when default is
338
378
  # not given.
339
379
  if settings.pres_template.include?(slide.tpl) and
340
- File.exists?(settings.pres_template[slide.tpl])
380
+ File.exist?(settings.pres_template[slide.tpl])
341
381
  template = File.open(settings.pres_template[slide.tpl], "r").read()
342
382
  end
343
383
  end
@@ -352,7 +392,7 @@ class ShowOff < Sinatra::Application
352
392
  # name the slide. If we've got multiple slides in this file, we'll have a sequence number
353
393
  # include that sequence number to index directly into that content
354
394
  if seq
355
- content += "<div class=\"content #{classes}\" ref=\"#{name}/#{seq.to_s}\">\n"
395
+ content += "<div class=\"content #{classes}\" ref=\"#{name}:#{seq.to_s}\">\n"
356
396
  else
357
397
  content += "<div class=\"content #{classes}\" ref=\"#{name}\">\n"
358
398
  end
@@ -373,7 +413,7 @@ class ShowOff < Sinatra::Application
373
413
  sl = Tilt[:markdown].new(nil, nil, engine_options) { sl }.render
374
414
  sl = build_forms(sl, content_classes)
375
415
  sl = update_p_classes(sl)
376
- sl = process_content_for_section_tags(sl, name)
416
+ sl = process_content_for_section_tags(sl, name, opts)
377
417
  sl = update_special_content(sl, @slide_count, name) # TODO: deprecated
378
418
  sl = update_image_paths(name, sl, opts)
379
419
 
@@ -430,30 +470,36 @@ class ShowOff < Sinatra::Application
430
470
  end
431
471
 
432
472
  # replace section tags with classed div tags
433
- def process_content_for_section_tags(content, name = nil)
473
+ def process_content_for_section_tags(content, name = nil, opts = {})
434
474
  return unless content
435
475
 
436
476
  # because this is post markdown rendering, we may need to shift a <p> tag around
437
477
  # remove the tags if they're by themselves
438
- result = content.gsub(/<p>~~~SECTION:([^~]*)~~~<\/p>/, '<div class="\1">')
478
+ result = content.gsub(/<p>~~~SECTION:([^~]*)~~~<\/p>/, '<div class="notes-section \1">')
439
479
  result.gsub!(/<p>~~~ENDSECTION~~~<\/p>/, '</div>')
440
480
 
441
481
  # shove it around the div if it belongs to the contained element
442
- result.gsub!(/(<p>)?~~~SECTION:([^~]*)~~~/, '<div class="\2">\1')
482
+ result.gsub!(/(<p>)?~~~SECTION:([^~]*)~~~/, '<div class="notes-section \2">\1')
443
483
  result.gsub!(/~~~ENDSECTION~~~(<\/p>)?/, '\1</div>')
444
484
 
445
485
  # Turn this into a document for munging
446
486
  doc = Nokogiri::HTML::DocumentFragment.parse(result)
447
487
 
488
+ if opts[:section]
489
+ doc.css('div.notes-section').each do |section|
490
+ section.remove unless section.attr('class').split.include? opts[:section]
491
+ end
492
+ end
493
+
448
494
  filename = File.join(settings.pres_dir, '_notes', "#{name}.md")
449
495
  @logger.debug "personal notes filename: #{filename}"
450
- if File.file? filename
496
+ if [nil, 'notes'].include? opts[:section] and File.file? filename
451
497
  # TODO: shouldn't have to reparse config all the time
452
498
  engine_options = ShowOffUtils.showoff_renderer_options(settings.pres_dir)
453
499
 
454
500
  # Make sure we've got a notes div to hang personal notes from
455
- doc.add_child '<div class="notes"></div>' if doc.css('div.notes').empty?
456
- doc.css('div.notes').each do |section|
501
+ doc.add_child '<div class="notes-section notes"></div>' if doc.css('div.notes-section.notes').empty?
502
+ doc.css('div.notes-section.notes').each do |section|
457
503
  text = Tilt[:markdown].new(nil, nil, engine_options) { File.read(filename) }.render
458
504
  frag = "<div class=\"personal\"><h1>Personal Notes</h1>#{text}</div>"
459
505
  note = Nokogiri::HTML::DocumentFragment.parse(frag)
@@ -622,7 +668,7 @@ class ShowOff < Sinatra::Application
622
668
  when /^ +\((.+)\)$/ # (Boston)
623
669
  str << "<option value='#{$1}' selected>#{$1}</option>"
624
670
  when /^ +\[(.+)\]$/ # [Boston]
625
- str << "<option value='#{$1}' selected>#{$1}</option>"
671
+ str << "<option value='#{$1}' class='correct'>#{$1}</option>"
626
672
  when /^ +([^\(].+[^\),]),?$/ # Boston
627
673
  str << "<option value='#{$1}'>#{$1}</option>"
628
674
  end
@@ -746,21 +792,32 @@ class ShowOff < Sinatra::Application
746
792
  private :update_download_links
747
793
 
748
794
  def update_image_paths(path, slide, opts={:static=>false, :pdf=>false})
749
- paths = path.split('/')
750
- paths.pop
751
- path = paths.join('/')
752
- replacement_prefix = opts[:static] ?
753
- ( opts[:pdf] ? %(img src="file://#{settings.pres_dir}/#{path}) : %(img src="./file/#{path}) ) :
754
- %(img src="#{@asset_path}image/#{path})
755
- slide.gsub(/img src=[\"\'](?!https?:\/\/)([^\/].*?)[\"\']/) do |s|
756
- img_path = File.join(path, $1)
757
- w, h = get_image_size(img_path)
758
- src = %(#{replacement_prefix}/#{$1}")
795
+ doc = Nokogiri::HTML::DocumentFragment.parse(slide)
796
+ slide_dir = File.dirname(path)
797
+
798
+ case
799
+ when opts[:static] && opts[:pdf]
800
+ replacement_prefix = "file://#{settings.pres_dir}/"
801
+ when opts[:static]
802
+ replacement_prefix = "./file/"
803
+ else
804
+ replacement_prefix = "#{@asset_path}image/"
805
+ end
806
+
807
+ doc.css('img').each do |img|
808
+ # clean up the path and remove some of the relative nonsense
809
+ img_path = Pathname.new(File.join(slide_dir, img[:src])).cleanpath.to_path
810
+ src = "#{replacement_prefix}/#{img_path}"
811
+ img[:src] = src
812
+
813
+ # TDOD: deprecated and to be removed
814
+ w, h = get_image_size(img_path)
759
815
  if w && h
760
- src << %( width="#{w}" height="#{h}")
816
+ img[:width] = w
817
+ img[:height] = h
761
818
  end
762
- src
763
819
  end
820
+ doc.to_html
764
821
  end
765
822
 
766
823
  if defined?(Magick)
@@ -832,7 +889,7 @@ class ShowOff < Sinatra::Application
832
889
  html.to_html
833
890
  end
834
891
 
835
- def get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil})
892
+ def get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil, :section=>nil})
836
893
 
837
894
  sections = ShowOffUtils.showoff_sections(settings.pres_dir, @logger)
838
895
  files = []
@@ -852,7 +909,7 @@ class ShowOff < Sinatra::Application
852
909
  begin
853
910
  data << process_markdown(fname, File.read(f), opts)
854
911
  rescue Errno::ENOENT => e
855
- logger.error e.message
912
+ @logger.error e.message
856
913
  data << process_markdown(fname, "!SLIDE\n# Missing File!\n## #{fname}", opts)
857
914
  end
858
915
  end
@@ -915,6 +972,9 @@ class ShowOff < Sinatra::Application
915
972
  # Check to see if the presentation has enabled feedback
916
973
  @feedback = settings.showoff_config['feedback'] unless (params && params[:feedback] == 'false')
917
974
 
975
+ # If we're static, we need to not show the downloads page
976
+ @static = static
977
+
918
978
  # Provide a button in the sidebar for interactive editing if configured
919
979
  @edit = settings.showoff_config['edit'] if @review
920
980
 
@@ -922,6 +982,7 @@ class ShowOff < Sinatra::Application
922
982
  end
923
983
 
924
984
  def presenter
985
+ @favicon = settings.showoff_config['favicon']
925
986
  @issues = settings.showoff_config['issues']
926
987
  @edit = settings.showoff_config['edit'] if @review
927
988
  @@cookie ||= guid()
@@ -976,14 +1037,15 @@ class ShowOff < Sinatra::Application
976
1037
  content
977
1038
  end
978
1039
 
979
- def print(static=false)
980
- @slides = get_slides_html(:static=>static, :toc=>true, :print=>true)
1040
+ def print(static=false, section=nil)
1041
+ @slides = get_slides_html(:static=>static, :toc=>true, :print=>true, :section=>section)
981
1042
  @favicon = settings.showoff_config['favicon']
982
1043
  erb :onepage
983
1044
  end
984
1045
 
985
1046
  def supplemental(content, static=false)
986
- @slides = get_slides_html(:static=>static, :supplemental=>content)
1047
+ # supplemental material is by definition separate from the presentation, so it doesn't make sense to attach notes
1048
+ @slides = get_slides_html(:static=>static, :supplemental=>content, :section=>false)
987
1049
  @favicon = settings.showoff_config['favicon']
988
1050
  @wrapper_classes = ['supplemental']
989
1051
  erb :onepage
@@ -1037,8 +1099,9 @@ class ShowOff < Sinatra::Application
1037
1099
  end
1038
1100
 
1039
1101
  # remove the weird /files component, since that doesn't exist on the filesystem
1102
+ # replace it for file://<PATH> for correct use with wkhtmltopdf (exactly with qt-webkit)
1040
1103
  html.gsub!(/<img src=".\/file\/([^"]*)/) do |s|
1041
- "<img src=\".\/#{$1}"
1104
+ "<img src=\"file:\/\/#{settings.pres_dir}\/#{$1}"
1042
1105
  end
1043
1106
 
1044
1107
  # PDFKit.new takes the HTML and any options for wkhtmltopdf
@@ -1071,6 +1134,9 @@ class ShowOff < Sinatra::Application
1071
1134
  when 'pdf'
1072
1135
  opt ||= "#{name}.pdf"
1073
1136
  data = showoff.send(what, opt)
1137
+ when 'print'
1138
+ opt ||= 'handouts'
1139
+ data = showoff.send(what, true, opt)
1074
1140
  else
1075
1141
  data = showoff.send(what, true)
1076
1142
  end
@@ -1109,7 +1175,7 @@ class ShowOff < Sinatra::Application
1109
1175
  }
1110
1176
 
1111
1177
  # ... and copy all needed image files
1112
- [/img src=[\"\'].\/file\/(.*?)[\"\']/, /style=[\"\']background: url\(\'file\/(.*?)'/].each do |regex|
1178
+ [/img src=[\"\'].\/file\/(.*?)[\"\']/, /style=[\"\']background(?:-image): url\(\'file\/(.*?)'/].each do |regex|
1113
1179
  data.scan(regex).flatten.each do |path|
1114
1180
  dir = File.dirname(path)
1115
1181
  FileUtils.makedirs(File.join(file_dir, dir))
@@ -1143,10 +1209,22 @@ class ShowOff < Sinatra::Application
1143
1209
 
1144
1210
  # Load a slide file from disk, parse it and return the text of a code block by index
1145
1211
  def get_code_from_slide(path, index)
1212
+ if path =~ /^(.*)(?::)(\d+)$/
1213
+ path = $1
1214
+ num = $2.to_i
1215
+ else
1216
+ num = 1
1217
+ end
1218
+
1146
1219
  slide = "#{path}.md"
1147
- return unless File.exists? slide
1220
+ return unless File.exist? slide
1148
1221
 
1149
- html = process_markdown(slide, File.read(slide), {})
1222
+ content = File.read(slide)
1223
+ if defined? num
1224
+ content = content.split(/^\<?!SLIDE/m).reject { |sl| sl.empty? }[num-1]
1225
+ end
1226
+
1227
+ html = process_markdown(slide, content, {})
1150
1228
  doc = Nokogiri::HTML::DocumentFragment.parse(html)
1151
1229
 
1152
1230
  return doc.css('code.execute')[index.to_i].text rescue 'Invalid code block index'
@@ -1155,20 +1233,55 @@ class ShowOff < Sinatra::Application
1155
1233
  # Basic auth boilerplate
1156
1234
  def protected!
1157
1235
  unless authorized?
1158
- response['WWW-Authenticate'] = %(Basic realm="#{@title}: Protected Area")
1159
- throw(:halt, [401, "Not authorized\n"])
1236
+ response['WWW-Authenticate'] = %(Basic realm="#{@title}: Protected Area. Please log in.")
1237
+ throw(:halt, [401, "Not authorized."])
1238
+ end
1239
+ end
1240
+
1241
+ def locked!
1242
+ # check auth first, because if the presenter has logged in with a password, we don't want to prompt again
1243
+ unless authorized? or unlocked?
1244
+ response['WWW-Authenticate'] = %(Basic realm="#{@title}: Locked Area. A presentation key is required to view.")
1245
+ throw(:halt, [401, "Not authorized."])
1160
1246
  end
1161
1247
  end
1162
1248
 
1163
1249
  def authorized?
1250
+ # allow localhost if we have no password
1164
1251
  if not settings.showoff_config.has_key? 'password'
1165
- # if no password is set, then default to allowing access to localhost
1166
- request.env['REMOTE_HOST'] == 'localhost' or request.ip == '127.0.0.1'
1252
+ localhost?
1167
1253
  else
1168
- auth ||= Rack::Auth::Basic::Request.new(request.env)
1169
1254
  user = settings.showoff_config['user'] || ''
1170
1255
  password = settings.showoff_config['password']
1171
- auth.provided? && auth.basic? && auth.credentials && auth.credentials == [user, password]
1256
+ authenticate([user, password])
1257
+ end
1258
+ end
1259
+
1260
+ def unlocked?
1261
+ # allow localhost if we have no key
1262
+ if not settings.showoff_config.has_key? 'key'
1263
+ localhost?
1264
+ else
1265
+ authenticate(settings.showoff_config['key'])
1266
+ end
1267
+ end
1268
+
1269
+ def localhost?
1270
+ request.env['REMOTE_HOST'] == 'localhost' or request.ip == '127.0.0.1'
1271
+ end
1272
+
1273
+ def authenticate(credentials)
1274
+ auth = Rack::Auth::Basic::Request.new(request.env)
1275
+
1276
+ return false unless auth.provided? && auth.basic? && auth.credentials
1277
+
1278
+ case credentials
1279
+ when Array
1280
+ auth.credentials == credentials
1281
+ when String
1282
+ auth.credentials.last == credentials
1283
+ else
1284
+ false
1172
1285
  end
1173
1286
  end
1174
1287
 
@@ -1289,12 +1402,15 @@ class ShowOff < Sinatra::Application
1289
1402
  end
1290
1403
 
1291
1404
  get '/control' do
1405
+ # leave the route so we don't have 404's for the parts we've missed
1406
+ return nil unless @interactive
1407
+
1292
1408
  if !request.websocket?
1293
1409
  raise Sinatra::NotFound
1294
1410
  else
1295
1411
  request.websocket do |ws|
1296
1412
  ws.onopen do
1297
- ws.send( { 'current' => @@current[:number] }.to_json )
1413
+ ws.send( { 'message' => 'current', 'current' => @@current[:number] }.to_json )
1298
1414
  settings.sockets << ws
1299
1415
 
1300
1416
  @logger.warn "Open sockets: #{settings.sockets.size}"
@@ -1324,7 +1440,7 @@ class ShowOff < Sinatra::Application
1324
1440
  @@current = { :name => name, :number => slide }
1325
1441
 
1326
1442
  # schedule a notification for all clients
1327
- EM.next_tick { settings.sockets.each{|s| s.send({ 'current' => @@current[:number] }.to_json) } }
1443
+ EM.next_tick { settings.sockets.each{|s| s.send({ 'message' => 'current', 'current' => @@current[:number] }.to_json) } }
1328
1444
  end
1329
1445
 
1330
1446
  when 'register'
@@ -1356,11 +1472,14 @@ class ShowOff < Sinatra::Application
1356
1472
  when 'position'
1357
1473
  ws.send( { 'current' => @@current[:number] }.to_json ) unless @@cookie.nil?
1358
1474
 
1359
- when 'pace', 'question'
1475
+ when 'pace', 'question', 'cancel'
1360
1476
  # just forward to the presenter(s) along with a debounce in case a presenter is registered twice
1361
1477
  control['id'] = guid()
1362
1478
  EM.next_tick { settings.presenters.each{|s| s.send(control.to_json) } }
1363
1479
 
1480
+ when 'complete'
1481
+ EM.next_tick { settings.sockets.each{|s| s.send(control.to_json) } }
1482
+
1364
1483
  when 'feedback'
1365
1484
  filename = "#{settings.statsdir}/#{settings.feedback}"
1366
1485
  slide = control['slide']
@@ -1408,8 +1527,10 @@ class ShowOff < Sinatra::Application
1408
1527
  opt = params[:captures][1]
1409
1528
  what = 'index' if "" == what
1410
1529
 
1411
- if settings.showoff_config.has_key? 'protected'
1412
- protected! if settings.showoff_config['protected'].include? what
1530
+ if settings.showoff_config['protected'].include? what
1531
+ protected!
1532
+ elsif settings.showoff_config['locked'].include? what
1533
+ locked!
1413
1534
  end
1414
1535
 
1415
1536
  @asset_path = env['SCRIPT_NAME'] == '' ? nil : env['SCRIPT_NAME'].gsub(/^\/?/, '/').gsub(/\/?$/, '/')