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