showoff 0.12.0 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
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(/\/?$/, '/')