showoff 0.16.1 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f799ced051d346d7788de67fc8737fce0f374f2e
4
- data.tar.gz: 315d9a402e3ace5d16c1f6184518c0aeb84495ba
3
+ metadata.gz: bcb7404f722269ce7b4c571c0a7d34c72f76d684
4
+ data.tar.gz: 7a2b4b825af14a88c58fccd9a851f51587894d9f
5
5
  SHA512:
6
- metadata.gz: a8c3c7c3462fbb722ea6097d53264f271ca0e10ab111a71d9fb4f242adfa59f934926a05a42403535b011148c324be0832219f64682de87df3e29b1146f84fcd
7
- data.tar.gz: 564ec5a49755719797eb3f07f759f91e0be067e13fff279b84cdc9441a2d12ed4fc330cd67487aaeba2187a59d363998f4d73b4090730d3fc5829a7335f94c8c
6
+ metadata.gz: 180720b95f90f0948642258fca4c84327da441b85d7b23bb00203272ee05c7ed07c85af8665a5938c5c1b8ca683e98887aa1b98514ae4b59d32c9e8dde41af2e
7
+ data.tar.gz: 38036fa41718cf12953dd2db5a32cdc2b05840c11fea9652dfe8a525b0a095238a97bffbf643ddd5d4ea1ac57b0b2ce5af89929c7710761467e6323169dd3167
data/bin/showoff CHANGED
@@ -4,6 +4,7 @@ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
4
  require 'showoff'
5
5
  require 'showoff/version'
6
6
  require 'rubygems'
7
+ require 'fidget'
7
8
  require 'gli'
8
9
 
9
10
  # See https://github.com/davetron5000/gli/issues/196 for rationale for this silly wrapper
@@ -98,24 +99,24 @@ module Wrapper
98
99
  arg_name 'heroku_name'
99
100
  long_desc 'Creates the configuration files needed to Herokuize a Showoff presentation and then deploys it for you.'
100
101
  command :heroku do |c|
101
-
102
+
102
103
  c.desc 'add password protection to your heroku site'
103
104
  c.flag [:p,:password]
104
-
105
+
105
106
  c.desc 'force overwrite of existing Gemfile/.gems and config.ru files if they exist'
106
107
  c.switch [:f,:force]
107
-
108
+
108
109
  c.action do |global_options,options,args|
109
110
  raise "heroku_name is required" if args.empty?
110
111
  raise "Name must start with a letter and can only contain lowercase letters, numbers, and dashes." unless args.first =~ /^[a-z][a-z1-9-]*$/
111
-
112
+
112
113
  unless system('git remote get-url heroku')
113
114
  ShowOffUtils.command("heroku create #{args[0]}", "Please ensure that the heroku gem is installed and you're logged in.")
114
115
  end
115
-
116
+
116
117
  if ShowOffUtils.heroku(args[0],options[:f],options[:p])
117
118
  ShowOffUtils.command('bundle install', 'Please ensure that the bundler gem is installed.')
118
-
119
+
119
120
  begin
120
121
  ShowOffUtils.command('git add Procfile Gemfile Gemfile.lock config.ru')
121
122
  ShowOffUtils.command('git commit -m "Herokuized by Showoff"')
@@ -129,11 +130,11 @@ module Wrapper
129
130
  puts
130
131
  puts 'When done, please run "git push heroku master"'
131
132
  end
132
-
133
+
133
134
  if options[:p]
134
135
  puts "CAREFUL: you are commiting your access password - anyone with read access to the repo can access the preso\n\n"
135
136
  end
136
-
137
+
137
138
  puts 'Your presentation has been Herokuized. Run `heroku open` to see it.'
138
139
  end
139
140
  end
@@ -159,6 +160,9 @@ module Wrapper
159
160
  c.desc 'Disable content caching'
160
161
  c.switch :nocache
161
162
 
163
+ c.desc 'Prevent the computer from sleeping during your presentation'
164
+ c.switch :nosleep
165
+
162
166
  c.desc 'Port on which to run'
163
167
  c.default_value "9090"
164
168
  c.flag [:p, :port]
@@ -230,6 +234,11 @@ module Wrapper
230
234
 
231
235
  "
232
236
 
237
+ if options[:nosleep] and Fidget.current_process
238
+ puts '**** System sleep has been suspended. ****'
239
+ puts
240
+ end
241
+
233
242
  if options[:url]
234
243
  ShowOffUtils.clone(options[:git_url], options[:git_branch], options[:git_path]) do
235
244
  ShowOff.run!(options) do |server|
data/lib/showoff.rb CHANGED
@@ -140,6 +140,7 @@ class ShowOff < Sinatra::Application
140
140
  @section_major = 0
141
141
  @section_minor = 0
142
142
  @section_title = settings.showoff_config['name'] rescue 'Showoff Presentation'
143
+ @@slide_titles = [] # a list of generated slide names, used for cross references later.
143
144
 
144
145
  @logger.debug settings.pres_template
145
146
 
@@ -161,12 +162,16 @@ class ShowOff < Sinatra::Application
161
162
  begin
162
163
  @@counter = JSON.parse(File.read("#{settings.statsdir}/#{settings.viewstats}"))
163
164
 
164
- # port old format stats
165
+ # TODO: remove this logic 4/15/2017: port old format stats
165
166
  unless @@counter.has_key? 'user_agents'
166
- @@counter = { 'user_agents' => {}, 'pageviews' => @@counter }
167
+ @@counter['pageviews'] = @@counter
167
168
  end
169
+
170
+ @@counter['current'] ||= {}
171
+ @@counter['pageviews'] ||= {}
172
+ @@counter['user_agents'] ||= {}
168
173
  rescue
169
- @@counter = { 'user_agents' => {}, 'pageviews' => {} }
174
+ @@counter = { 'user_agents' => {}, 'pageviews' => {}, 'current' => {} }
170
175
  end
171
176
 
172
177
  # keeps track of form responses. In memory to avoid concurrence issues.
@@ -395,11 +400,9 @@ class ShowOff < Sinatra::Application
395
400
 
396
401
  # name the slide. If we've got multiple slides in this file, we'll have a sequence number
397
402
  # include that sequence number to index directly into that content
398
- if seq
399
- content += "<div class=\"content #{classes}\" ref=\"#{name}:#{seq.to_s}\">\n"
400
- else
401
- content += "<div class=\"content #{classes}\" ref=\"#{name}\">\n"
402
- end
403
+ ref = seq ? "#{name}:#{seq.to_s}" : name
404
+ content += "<div class=\"content #{classes}\" ref=\"#{ref}\">\n"
405
+ @@slide_titles << ref
403
406
 
404
407
  # renderers like wkhtmltopdf needs an <h1> tag to use for a section title, but only when printing.
405
408
  if opts[:print]
@@ -492,12 +495,6 @@ class ShowOff < Sinatra::Application
492
495
  # Turn this into a document for munging
493
496
  doc = Nokogiri::HTML::DocumentFragment.parse(result)
494
497
 
495
- if opts[:section]
496
- doc.css('div.notes-section').each do |section|
497
- section.remove unless section.attr('class').split.include? opts[:section]
498
- end
499
- end
500
-
501
498
  filename = File.join(settings.pres_dir, '_notes', "#{name}.md")
502
499
  @logger.debug "personal notes filename: #{filename}"
503
500
  if [nil, 'notes'].include? opts[:section] and File.file? filename
@@ -519,9 +516,68 @@ class ShowOff < Sinatra::Application
519
516
  end
520
517
  end
521
518
 
522
- # Now add a target so we open all external links from notes in a new window
519
+ doc.css('.callout.glossary').each do |item|
520
+ next unless item.content =~ /^([^|]+)\|([^:]+):(.*)$/
521
+ item['data-term'] = $1
522
+ item['data-target'] = $2
523
+ item['data-text'] = $3
524
+ item.content = $3
525
+
526
+ glossary = (item.attr('class').split - ['callout', 'glossary']).first
527
+ address = glossary ? "#{glossary}/#{$2}" : $2
528
+ frag = "<a class=\"processed label\" href=\"glossary://#{address}\">#{$1}</a>"
529
+
530
+ item.children.before(Nokogiri::HTML::DocumentFragment.parse(frag))
531
+ end
532
+
533
+ # Process links
523
534
  doc.css('a').each do |link|
524
- link.set_attribute('target', '_blank') unless link['href'].start_with? '#'
535
+ next if link['href'].start_with? '#'
536
+ next if link['class'].split.include? 'processed' rescue nil
537
+
538
+ # If these are glossary links, populate the notes/handouts sections
539
+ if link['href'].start_with? 'glossary://'
540
+ doc.add_child '<div class="notes-section notes"></div>' if doc.css('div.notes-section.notes').empty?
541
+ doc.add_child '<div class="notes-section handouts"></div>' if doc.css('div.notes-section.handouts').empty?
542
+
543
+ term = link.content
544
+ text = link['title']
545
+ href = link['href']
546
+ href.slice!('glossary://')
547
+
548
+ parts = href.split('/')
549
+ target = parts.pop
550
+ name = parts.pop # either the glossary name or nil
551
+
552
+ link['class'] = 'term'
553
+
554
+ label = link.clone
555
+ label['class'] = 'label processed'
556
+
557
+ frag = Nokogiri::HTML::DocumentFragment.parse('<p></p>')
558
+ definition = frag.children.first
559
+ definition['class'] = "callout glossary #{name}"
560
+ definition['data-term'] = term
561
+ definition['data-target'] = target
562
+ definition['data-text'] = text
563
+ definition.content = text
564
+ definition.children.before(label)
565
+
566
+ [doc.css('div.notes-section.notes'), doc.css('div.notes-section.handouts')].each do |section|
567
+ section.first.add_child(definition.clone)
568
+ end
569
+
570
+ else
571
+ # Add a target so we open all external links from notes in a new window
572
+ link.set_attribute('target', '_blank')
573
+ end
574
+ end
575
+
576
+ # finally, remove any sections we don't want to print
577
+ if opts[:section]
578
+ doc.css('div.notes-section').each do |section|
579
+ section.remove unless section.attr('class').split.include? opts[:section]
580
+ end
525
581
  end
526
582
 
527
583
  doc.to_html
@@ -552,31 +608,87 @@ class ShowOff < Sinatra::Application
552
608
  end
553
609
 
554
610
  def process_content_for_all_slides(content, num_slides, opts={})
611
+ # this has to be text replacement for now, since the string can appear in any context
555
612
  content.gsub!("~~~NUM_SLIDES~~~", num_slides.to_s)
613
+ doc = Nokogiri::HTML::DocumentFragment.parse(content)
556
614
 
557
615
  # Should we build a table of contents?
558
616
  if opts[:toc]
559
- frag = Nokogiri::HTML::DocumentFragment.parse ""
560
- toc = Nokogiri::XML::Node.new('div', frag)
561
- toc['id'] = 'toc'
562
- frag.add_child(toc)
563
-
564
- Nokogiri::HTML(content).css('div.subsection > h1:not(.section_title)').each do |section|
565
- entry = Nokogiri::XML::Node.new('div', frag)
566
- entry['class'] = 'tocentry'
567
- toc.add_child(entry)
568
-
569
- link = Nokogiri::XML::Node.new('a', frag)
570
- link['href'] = "##{section.parent.parent['id']}"
571
- link.content = section.content
572
- entry.add_child(link)
617
+ toc = Nokogiri::HTML::DocumentFragment.parse("<p id=\"toc\"></p>")
618
+
619
+ doc.css('div.subsection > h1:not(.section_title)').each do |section|
620
+ href = section.parent.parent['id']
621
+ frag = "<div class=\"tocentry\"><a href=\"##{href}\">#{section.content}</a></div>"
622
+ link = Nokogiri::HTML::DocumentFragment.parse(frag)
623
+
624
+ toc.children.first.add_child(link)
573
625
  end
574
626
 
575
627
  # swap out the tag, if found, with the table of contents
576
- content.gsub!("~~~TOC~~~", frag.to_html)
628
+ doc.at('p:contains("~~~TOC~~~")').replace(toc)
577
629
  end
578
630
 
579
- content
631
+ doc.css('.slide.glossary .content').each do |glossary|
632
+ name = (glossary.attr('class').split - ['content', 'glossary']).first
633
+ list = Nokogiri::HTML::DocumentFragment.parse('<ul class="glossary terms"></ul>')
634
+ seen = []
635
+
636
+ doc.css('.callout.glossary').each do |item|
637
+ target = (item.attr('class').split - ['callout', 'glossary']).first
638
+
639
+ # if the name matches or if we didn't name it to begin with.
640
+ next unless target == name
641
+
642
+ # the definition can exist in multiple places, so de-dup it here
643
+ term = item.attr('data-term')
644
+ next if seen.include? term
645
+ seen << term
646
+
647
+ # excrutiatingly find the parent slide content and grab the ref
648
+ # in a library less shitty, this would be something like
649
+ # $(this).parent().siblings('.content').attr('ref')
650
+ href = nil
651
+ item.ancestors('.slide').first.traverse do |element|
652
+ next if element['class'].nil?
653
+ next unless element['class'].split.include? 'content'
654
+
655
+ href = element.attr('ref').gsub('/', '_')
656
+ end
657
+
658
+ text = item.attr('data-text')
659
+ link = item.attr('data-target')
660
+ page = glossary.attr('ref')
661
+ anchor = "#{page}+#{link}"
662
+ next if href.nil? or text.nil? or link.nil?
663
+
664
+ frag = "<li><a id=\"#{anchor}\" class=\"label\">#{term}</a>#{text}<a href=\"##{href}\" class=\"return\">↩</a></li>"
665
+ item = Nokogiri::HTML::DocumentFragment.parse(frag)
666
+
667
+ list.children.first.add_child(item)
668
+ end
669
+
670
+ glossary.add_child(list)
671
+ end
672
+
673
+ # now fix all the links to point to the glossary page
674
+ doc.css('a').each do |link|
675
+ next if link['href'].nil?
676
+ next unless link['href'].start_with? 'glossary://'
677
+
678
+ href = link['href']
679
+ href.slice!('glossary://')
680
+
681
+ parts = href.split('/')
682
+ target = parts.pop
683
+ name = parts.pop # either the glossary name or nil
684
+
685
+ classes = name.nil? ? ".slide.glossary" : ".slide.glossary.#{name}"
686
+ href = doc.at("#{classes} .content").attr('ref') rescue nil
687
+
688
+ link['href'] = "##{href}+#{target}"
689
+ end
690
+
691
+ doc.to_html
580
692
  end
581
693
 
582
694
  # Find any lines that start with a <p>.(something), remove the ones tagged with
@@ -991,6 +1103,14 @@ class ShowOff < Sinatra::Application
991
1103
  # Provide a button in the sidebar for interactive editing if configured
992
1104
  @edit = settings.showoff_config['edit'] if @review
993
1105
 
1106
+ # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc.
1107
+ unless request.cookies['client_id']
1108
+ @client_id = guid()
1109
+ response.set_cookie('client_id', @client_id)
1110
+ else
1111
+ @client_id = request.cookies['client_id']
1112
+ end
1113
+
994
1114
  erb :index
995
1115
  end
996
1116
 
@@ -1046,6 +1166,7 @@ class ShowOff < Sinatra::Application
1046
1166
 
1047
1167
  # if we have a cache and we're not asking to invalidate it
1048
1168
  return @@cache if (@@cache and params['cache'] != 'clear')
1169
+ @@slide_titles = []
1049
1170
  content = get_slides_html(:static=>static)
1050
1171
 
1051
1172
  # allow command line cache disabling
@@ -1081,12 +1202,79 @@ class ShowOff < Sinatra::Application
1081
1202
  erb :download
1082
1203
  end
1083
1204
 
1205
+ def stats_data()
1206
+ data = {}
1207
+ begin
1208
+
1209
+ # what are viewers looking at right now?
1210
+ now = Time.now.to_i # let's throw away viewers who haven't done anything in 5m
1211
+ active = @@counter['current'].select {|client, view| (now - view[1]).abs < 300 }
1212
+
1213
+ # percentage of stray viewers
1214
+ stray = active.select {|client, view| view[0] != @@current[:name] }
1215
+ stray_p = ((stray.size.to_f / active.size.to_f) * 100).to_i rescue 0
1216
+ data['stray_p'] = stray_p
1217
+
1218
+ # percentage of idle viewers
1219
+ idle = @@counter['current'].size - active.size
1220
+ idle_p = ((idle.to_f / @@counter['current'].size.to_f) * 100).to_i rescue 0
1221
+ data['idle_p'] = idle_p
1222
+
1223
+ viewers = @@slide_titles.map do |slide|
1224
+ count = active.select {|client, view| view[0] == slide }.size
1225
+ flags = (slide == @@current[:name]) ? 'current' : nil
1226
+ [count, slide, nil, flags]
1227
+ end
1228
+
1229
+ # trim the ends, if nobody's looking we don't much care.
1230
+ viewers.pop while viewers.last[0] == 0
1231
+ viewers.shift while viewers.first[0] == 0
1232
+ viewmax = viewers.max_by {|view| view[0] }.first
1233
+
1234
+ data['viewers'] = viewers
1235
+ data['viewmax'] = viewmax
1236
+ rescue => e
1237
+ @logger.warn "Not enough data to generate pageviews."
1238
+ @logger.debug e.message
1239
+ @logger.debug e.backtrace.first
1240
+ end
1241
+
1242
+ begin
1243
+ # current elapsed time for the zoomline view
1244
+ elapsed = @@slide_titles.map do |slide|
1245
+ if @@counter['pageviews'][slide].nil?
1246
+ time = 0
1247
+ else
1248
+ time = @@counter['pageviews'][slide].inject(0) do |outer, (viewer, views)|
1249
+ outer += views.inject(0) { |inner, view| inner += view['elapsed'] }
1250
+ end
1251
+ end
1252
+ string = Time.at(time).gmtime.strftime('%M:%S')
1253
+ flags = (slide == @@current[:name]) ? 'current' : nil
1254
+
1255
+ [ time, slide, string, flags ]
1256
+ end
1257
+ maxtime = elapsed.max_by {|view| view[0] }.first
1258
+
1259
+ data['elapsed'] = elapsed
1260
+ data['maxtime'] = maxtime
1261
+ rescue => e
1262
+ # expected if this is loaded before a presentation has been compiled
1263
+ @logger.warn "Not enough data to generate elapsed time."
1264
+ @logger.debug e.message
1265
+ @logger.debug e.backtrace.first
1266
+ end
1267
+
1268
+ data.to_json
1269
+ end
1270
+
1084
1271
  def stats()
1085
- if request.env['REMOTE_HOST'] == 'localhost'
1272
+ if localhost?
1086
1273
  # the presenter should have full stats in the erb
1087
1274
  @counter = @@counter['pageviews']
1088
1275
  end
1089
1276
 
1277
+ # for the full page view. Maybe to be disappeared
1090
1278
  @all = Hash.new
1091
1279
  @@counter['pageviews'].each do |slide, stats|
1092
1280
  @all[slide] = 0
@@ -1095,10 +1283,6 @@ class ShowOff < Sinatra::Application
1095
1283
  end
1096
1284
  end
1097
1285
 
1098
- # most and least five viewed slides
1099
- @least = @all.sort_by {|slide, time| time}[0..4]
1100
- @most = @all.sort_by {|slide, time| -time}[0..4]
1101
-
1102
1286
  erb :stats
1103
1287
  end
1104
1288
 
@@ -1131,7 +1315,7 @@ class ShowOff < Sinatra::Application
1131
1315
  end
1132
1316
 
1133
1317
 
1134
- def self.do_static(args, opts = {})
1318
+ def self.do_static(args, opts = {})
1135
1319
  args ||= [] # handle nil arguments
1136
1320
  what = args[0] || "index"
1137
1321
  opt = args[1]
@@ -1319,18 +1503,20 @@ class ShowOff < Sinatra::Application
1319
1503
  (0..15).to_a.map{|a| rand(16).to_s(16)}.join
1320
1504
  end
1321
1505
 
1322
- def valid_cookie?
1506
+ def valid_presenter_cookie?
1507
+ return false if @@cookie.nil?
1323
1508
  (request.cookies['presenter'] == @@cookie)
1324
1509
  end
1325
1510
 
1326
1511
  post '/form/:id' do |id|
1327
- @logger.warn("Saving form answers from ip:#{request.ip} for id:##{id}")
1512
+ client_id = request.cookies['client_id']
1513
+ @logger.warn("Saving form answers from ip:#{request.ip} with ID of #{client_id} for id:##{id}")
1328
1514
 
1329
1515
  form = params.reject { |k,v| ['splat', 'captures', 'id'].include? k }
1330
1516
 
1331
1517
  # make sure we've got a bucket for this form, then save our answers
1332
1518
  @@forms[id] ||= {}
1333
- @@forms[id][request.ip] = form
1519
+ @@forms[id][client_id] = form
1334
1520
 
1335
1521
  form.to_json
1336
1522
  end
@@ -1445,13 +1631,13 @@ class ShowOff < Sinatra::Application
1445
1631
  begin
1446
1632
  control = JSON.parse(data)
1447
1633
 
1448
- @logger.warn "#{control.inspect}"
1634
+ @logger.debug "#{control.inspect}"
1449
1635
 
1450
1636
  case control['message']
1451
1637
  when 'update'
1452
1638
  # websockets don't use the same auth standards
1453
1639
  # we use a session cookie to identify the presenter
1454
- if valid_cookie?
1640
+ if valid_presenter_cookie?
1455
1641
  name = control['name']
1456
1642
  slide = control['slide'].to_i
1457
1643
  increment = control['increment'].to_i rescue 0
@@ -1472,29 +1658,37 @@ class ShowOff < Sinatra::Application
1472
1658
 
1473
1659
  when 'register'
1474
1660
  # save a list of presenters
1475
- if valid_cookie?
1661
+ if valid_presenter_cookie?
1476
1662
  remote = request.env['REMOTE_HOST'] || request.env['REMOTE_ADDR']
1477
1663
  settings.presenters << ws
1478
1664
  @logger.warn "Registered new presenter: #{remote}"
1479
1665
  end
1480
1666
 
1481
1667
  when 'track'
1482
- remote = valid_cookie? ? 'presenter' : (request.env['REMOTE_HOST'] || request.env['REMOTE_ADDR'])
1668
+ remote = valid_presenter_cookie? ? 'presenter' : request.cookies['client_id']
1483
1669
  slide = control['slide']
1484
- time = control['time'].to_f
1485
1670
 
1486
- # record the UA of the client if we haven't seen it before
1487
- @@counter['user_agents'][remote] ||= request.user_agent
1671
+ if control.has_key? 'time'
1672
+ time = control['time'].to_f
1673
+
1674
+ # record the UA of the client if we haven't seen it before
1675
+ @@counter['user_agents'][remote] ||= request.user_agent
1488
1676
 
1489
- views = @@counter['pageviews']
1490
- # a bucket for this slide
1491
- views[slide] ||= Hash.new
1492
- # a bucket of slideviews for this address
1493
- views[slide][remote] ||= Array.new
1494
- # and add this slide viewing to the bucket
1495
- views[slide][remote] << { 'elapsed' => time, 'timestamp' => Time.now.to_i, 'presenter' => @@current[:name] }
1677
+ views = @@counter['pageviews']
1678
+ # a bucket for this slide
1679
+ views[slide] ||= Hash.new
1680
+ # a bucket of slideviews for this address
1681
+ views[slide][remote] ||= Array.new
1682
+ # and add this slide viewing to the bucket
1683
+ views[slide][remote] << { 'elapsed' => time, 'timestamp' => Time.now.to_i, 'presenter' => @@current[:name] }
1684
+
1685
+ @logger.debug "Logged #{time} on slide #{slide} for #{remote}"
1686
+
1687
+ else
1688
+ @@counter['current'][remote] = [slide, Time.now.to_i]
1689
+ @logger.debug "Recorded current slide #{slide} for #{remote}"
1690
+ end
1496
1691
 
1497
- @logger.debug "Logged #{time} on slide #{slide} for #{remote}"
1498
1692
 
1499
1693
  when 'position'
1500
1694
  ws.send( { 'current' => @@current[:number] }.to_json ) unless @@cookie.nil?