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 +4 -4
- data/bin/showoff +28 -9
- data/lib/keymap.rb +145 -1
- data/lib/showoff.rb +184 -63
- data/lib/showoff/version.rb +1 -1
- data/lib/showoff_utils.rb +76 -39
- data/public/css/TimeCircles.css +42 -0
- data/public/css/presenter.css +161 -45
- data/public/css/showoff.css +185 -75
- data/public/favicon.ico +0 -0
- data/public/js/TimeCircles.js +984 -0
- data/public/js/presenter.js +147 -64
- data/public/js/showoff.js +224 -85
- data/views/download.erb +21 -24
- data/views/header.erb +5 -2
- data/views/index.erb +20 -10
- data/views/presenter.erb +19 -11
- data/views/stats.erb +47 -49
- metadata +28 -16
- data/public/css/close.png +0 -0
- data/public/css/run_code-dim.png +0 -0
- data/public/css/run_code.png +0 -0
- data/public/js/keyDictionary.json +0 -139
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0547aee458c3417a677758cdf70ea8a063e2d89
|
4
|
+
data.tar.gz: ecb30bc4b56493533a5b70327cedec5b75d2475f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
183
|
-
:port
|
184
|
-
:pres_file
|
185
|
-
:pres_dir
|
186
|
-
:verbose
|
187
|
-
:review
|
188
|
-
:execute
|
189
|
-
:nocache
|
190
|
-
:bind
|
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'
|
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.
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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.
|
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}
|
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}'
|
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
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
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
|
-
|
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
|
-
|
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=\"
|
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.
|
1220
|
+
return unless File.exist? slide
|
1148
1221
|
|
1149
|
-
|
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
|
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
|
-
|
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
|
-
|
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.
|
1412
|
-
protected!
|
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(/\/?$/, '/')
|