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 +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(/\/?$/, '/')
|