class Showoff

Constants

GEMROOT

Attributes

cached_image_size[R]

Public Class Methods

do_static(args, opts = {}) click to toggle source
# File lib/showoff.rb, line 1476
def self.do_static(args, opts = {})
    args ||= [] # handle nil arguments
    what   = args[0] || "index"
    opt    = args[1]

    ShowoffUtils.presentation_config_file = opts[:f]

    # Sinatra now aliases new to new!
    # https://github.com/sinatra/sinatra/blob/v1.3.3/lib/sinatra/base.rb#L1369
    showoff = Showoff.new!

    name = showoff.instance_variable_get(:@pres_name)
    path = showoff.instance_variable_get(:@root_path)
    logger = showoff.instance_variable_get(:@logger)

    I18n.locale = opts[:language]

    case what
    when 'supplemental'
      data = showoff.send(what, opt, true)
    when 'pdf'
      opt ||= "#{name}.pdf"
      data = showoff.send(what, opt)
    when 'print'
      opt ||= 'handouts'
      data = showoff.send(what, opt, true)
    else
      data = showoff.send(what, true)
    end

    if data.is_a?(File)
      logger.warn "Generated PDF as #{opt}"
    else
      out = File.expand_path("#{path}/static")
      # First make a directory
      FileUtils.makedirs(out)
      # Then write the html
      file = File.new("#{out}/index.html", "w")
      file.puts(data)
      file.close
      # Now copy all the js and css
      my_path = File.join( File.dirname(__FILE__), '..', 'public')
      ["js", "css"].each { |dir|
        FileUtils.copy_entry("#{my_path}/#{dir}", "#{out}/#{dir}", false, false, true)
      }

      # @todo: uh. I don't know how this ever worked. my_path is showoff and name is presentation.
      # And copy the directory
      Dir.glob("#{my_path}/#{name}/*").each { |subpath|
        base = File.basename(subpath)
        next if "static" == base
        next unless File.directory?(subpath) || base.match(/\.(css|js)$/)
        FileUtils.copy_entry(subpath, "#{out}/#{base}")
      }

      # Set up file dir
      file_dir = File.join(out, 'file')
      FileUtils.makedirs(file_dir)
      pres_dir = showoff.settings.pres_dir

      # ..., copy all user-defined styles and javascript files
      showoff.css_files.each { |path|
        dest = File.join(out, path)
        FileUtils.mkdir_p(File.dirname(dest))
        FileUtils.copy(path, dest)
      }
      showoff.js_files.each { |path|
        dest = File.join(out, path)
        FileUtils.mkdir_p(File.dirname(dest))
        FileUtils.copy(path, dest)
      }

      # ... and copy all needed image files
      [/img src=[\"\'].\/file\/(.*?)[\"\']/, /style=[\"\']background(?:-image): url\(\'file\/(.*?)'/].each do |regex|
        data.scan(regex).flatten.each do |path|
          dir = File.dirname(path)
          FileUtils.makedirs(File.join(file_dir, dir))
          begin
            FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
          rescue Errno::ENOENT => e
            puts "Missing source file: #{path}"
          end
        end
      end
      # copy images from css too
      showoff.css_files.each do |css_path|
        File.open(css_path) do |file|
          data = file.read
          data.scan(/url\([\"\']?(?!https?:\/\/)(.*?)[\"\']?\)/).flatten.each do |path|
            path.gsub!(/(\#.*)$/, '') # get rid of the anchor
            path.gsub!(/(\?.*)$/, '') # get rid of the query

            # resolve relative paths in the stylesheet
            path = "#{File.dirname(css_path)}/#{path}" unless path.start_with? '/'

            logger.debug path
            dir = File.dirname(path)
            FileUtils.makedirs(File.join(file_dir, dir))
            begin
              FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
            rescue Errno::ENOENT => e
              puts "Missing source file: #{path}"
            end
          end
        end
      end
    end
  end
flush() click to toggle source

save stats to disk

# File lib/showoff.rb, line 221
def self.flush
  begin
    if defined?(@@counter) and not @@counter.empty?
      File.open("#{settings.statsdir}/#{settings.viewstats}", 'w') do |f|
        if settings.verbose then
          f.write(JSON.pretty_generate(@@counter))
        else
          f.write(@@counter.to_json)
        end
      end
    end

    if defined?(@@forms) and not @@forms.empty?
      File.open("#{settings.statsdir}/#{settings.forms}", 'w') do |f|
        if settings.verbose then
          f.write(JSON.pretty_generate(@@forms))
        else
          f.write(@@forms.to_json)
        end
      end
    end
  rescue Errno::ENOENT => e
  end
end
generatePDF() click to toggle source

Generate a PDF version of the presentation in the current directory. This requires that the HTML snaphot exists, and it will remove that snapshot if the PDF generation is successful.

@note

wkhtmltopdf is terrible and will often report hard failures even after
successfully building a PDF. Therefore, we check file existence and
display different error messaging.

@see

https://github.com/puppetlabs/showoff/blob/220d6eef4c5942eda625dd6edc5370c7490eced7/lib/showoff.rb#L1447-L1471
# File lib/showoff_ng.rb, line 75
def self.generatePDF
  begin
    require 'pdfkit'
    output = Showoff::Config.get('name')+'.pdf'

    kit = PDFKit.new(File.new('static/index.html'), Showoff::Config.get('pdf_options'))
    kit.to_file(output)
    FileUtils.rm_rf('static')

  rescue RuntimeError => e
    if File.exist? output
      Showoff::Logger.warn "Your PDF was generated, but PDFkit reported an error. Inspect the file #{output} for suitability."
      Showoff::Logger.warn "You might try loading `static/index.html` in a web browser and checking the developer console for 404 errors."
    else
      Showoff::Logger.error "Generating your PDF with wkhtmltopdf was not successful."
      Showoff::Logger.error "Try running the following command manually to see what it's failing on."
      Showoff::Logger.error e.message.sub('--quiet', '')
    end
  rescue LoadError
    Showoff::Logger.error 'Generating a PDF version of your presentation requires the `pdfkit` gem.'
  end

end
makeSnapshot(presentation) click to toggle source

Generate a static HTML snapshot of the presentation in the ‘static` directory. Note that the `Showoff::Presentation` determines the format of the generated presentation based on the content requested.

@see

https://github.com/puppetlabs/showoff/blob/220d6eef4c5942eda625dd6edc5370c7490eced7/lib/showoff.rb#L1506-L1574
# File lib/showoff_ng.rb, line 40
def self.makeSnapshot(presentation)
  FileUtils.mkdir_p 'static'
  File.write(File.join('static', 'index.html'), presentation.static)

  ['js', 'css'].each { |dir|
    src  = File.join(GEMROOT, 'public', dir)
    dest = File.join('static', dir)

    FileUtils.copy_entry(src, dest, false, false, true)
  }

  # now copy all the files we care about
  presentation.assets.each do |path|
    src  = File.join(Showoff::Config.root, path)
    dest = File.join('static', path)

    FileUtils.mkdir_p(File.dirname(dest))
    begin
      FileUtils.copy(src, dest)
    rescue Errno::ENOENT => e
      Showoff::Logger.warn "Missing source file: #{path}"
    end
  end
end
new(app=nil) click to toggle source
Calls superclass method
# File lib/showoff.rb, line 72
def initialize(app=nil)
  super(app)
  @logger = Logger.new(STDERR)
  @logger.formatter = proc { |severity,datetime,progname,msg| "#{progname} #{msg}\n" }
  @logger.level = settings.verbose ? Logger::DEBUG : Logger::WARN

  @review  = settings.review
  @execute = settings.execute

  settings.pres_dir ||= Dir.pwd
  @root_path = "."

  # Load up the default keymap, then merge in any customizations
  keymapfile   = File.expand_path(File.join('~', '.showoff', 'keymap.json')) rescue nil
  @keymap      = Keymap.default
  @keymap.merge! JSON.parse(File.read(keymapfile)) rescue {}

  # map keys to the labels we're using
  @keycode_dictionary   = Keymap.keycodeDictionary
  @keycode_shifted_keys = Keymap.shiftedKeyDictionary

  settings.pres_dir = File.expand_path(settings.pres_dir)
  if (settings.pres_file and settings.pres_file != 'showoff.json')
    ShowoffUtils.presentation_config_file = settings.pres_file
  end

  # Load configuration for page size and template from the
  # configuration JSON file
  if File.exist?(ShowoffUtils.presentation_config_file)
    showoff_json = JSON.parse(File.read(ShowoffUtils.presentation_config_file))
    settings.showoff_config = showoff_json

    # Set options for encoding, template and page size
    settings.encoding      = showoff_json["encoding"]  || 'UTF-8'
    settings.page_size     = showoff_json["page-size"] || "Letter"
    settings.pres_template = showoff_json["templates"]
  end

  # if no sections are provided, we'll just start from cwd
  settings.showoff_config['sections'] ||= ['.']

  # code execution timeout
  settings.showoff_config['timeout'] ||= 15

  # If favicon in presentation root, use it by default
  if File.exist? 'favicon.ico'
    settings.showoff_config['favicon'] ||= 'file/favicon.ico'
  end

  # default protection levels
  if settings.showoff_config.has_key? 'password'
    settings.showoff_config['protected'] ||= ["presenter", "onepage", "print"]
  else
    settings.showoff_config['protected'] ||= Array.new
  end

  if settings.showoff_config.has_key? 'key'
    settings.showoff_config['locked'] ||= ["slides"]
  else
    settings.showoff_config['locked'] ||= Array.new
  end

  # default code parsers (for executable code blocks)
  settings.showoff_config['parsers'] ||= {}
  settings.showoff_config['parsers']['perl']   ||= 'perl'
  settings.showoff_config['parsers']['puppet'] ||= 'puppet apply --color=false'
  settings.showoff_config['parsers']['python'] ||= 'python'
  settings.showoff_config['parsers']['ruby']   ||= 'ruby'
  settings.showoff_config['parsers']['shell']  ||= 'sh'

  # default code validators
  settings.showoff_config['validators'] ||= {}
  settings.showoff_config['validators']['perl']   ||= 'perl -cw'
  settings.showoff_config['validators']['puppet'] ||= 'puppet parser validate'
  settings.showoff_config['validators']['python'] ||= 'python -m py_compile'
  settings.showoff_config['validators']['ruby']   ||= 'ruby -c'
  settings.showoff_config['validators']['shell']  ||= 'sh -n'

  # highlightjs syntax style
  @highlightStyle = settings.showoff_config['highlight'] || 'default'

  # variables used for building section numbering and title
  @slide_count   = 0
  @section_major = 0
  @section_minor = 0
  @section_title = settings.showoff_config['name'] rescue I18n.t('name')
  @@slide_titles  = [] # a list of generated slide names, used for cross references later.

  @logger.debug settings.pres_template

  @cached_image_size = {}
  @logger.debug settings.pres_dir
  @pres_name = settings.pres_dir.split('/').pop
  require_ruby_files

  # invert the logic to maintain backwards compatibility of interactivity on by default
  @interactive = ! settings.standalone rescue false

  # Create stats directory
  FileUtils.mkdir settings.statsdir unless File.directory? settings.statsdir if @interactive

  # Page view time accumulator. Tracks how often slides are viewed by the audience
  begin
    @@counter = JSON.parse(File.read("#{settings.statsdir}/#{settings.viewstats}"))

    # TODO: remove this logic 4/15/2017: port old format stats
    unless @@counter.has_key? 'user_agents'
      @@counter['pageviews'] = @@counter
    end

    @@counter['current']     ||= {}
    @@counter['pageviews']   ||= {}
    @@counter['user_agents'] ||= {}
  rescue
    @@counter = { 'user_agents' => {}, 'pageviews' => {}, 'current' => {} }
  end

  # keeps track of form responses. In memory to avoid concurrence issues.
  begin
    @@forms = JSON.parse(File.read("#{settings.statsdir}/#{settings.forms}"))
  rescue
    @@forms = Hash.new
  end

  @@downloads = Hash.new # Track downloadable files
  @@cookie    = nil      # presenter cookie. Identifies the presenter for control messages
  @@master    = nil      # this holds the @client_id of the master presenter, for the cases in which multiple presenters are loaded
  @@current   = Hash.new # The current slide that the presenter is viewing
  @@cache     = Hash.new # Cache slide content for subsequent hits
  @@activity  = []       # keep track of completion for activity slides

  if @interactive
    # flush stats to disk periodically
    Thread.new do
      loop do
        sleep 30
        Showoff.flush
      end
    end
  end

  # Initialize Markdown Configuration
  MarkdownConfig::setup(settings.pres_dir)

  # Process renderer config options
  @engine_options = ShowoffUtils.showoff_renderer_options(settings.pres_dir)

end
pres_dir_current() click to toggle source
# File lib/showoff.rb, line 246
def self.pres_dir_current
  opt = {:pres_dir => Dir.pwd}
  Showoff.set opt
end

Public Instance Methods

assets_needed() click to toggle source
# File lib/showoff.rb, line 1281
def assets_needed
  assets = ["index", "slides"]

  index = erb :index
  html = Nokogiri::XML.parse(index)
  html.css('head link').each do |link|
    href = clean_link(link['href'])
    assets << href if href
  end
  html.css('head script').each do |link|
    href = clean_link(link['src'])
    assets << href if href
  end

  slides = get_slides_html
  html = Nokogiri::XML.parse("<slides>" + slides + "</slides>")
  html.css('img').each do |link|
    href = clean_link(link['src'])
    assets << href if href
  end

  css = Dir.glob("#{settings.public_folder}/**/*.css").map { |path| path.gsub(settings.public_folder + '/', '') }
  assets << css

  js = Dir.glob("#{settings.public_folder}/**/*.js").map { |path| path.gsub(settings.public_folder + '/', '') }
  assets << js

  assets.uniq.join("\n")
end
authenticate(credentials) click to toggle source
# File lib/showoff.rb, line 1662
def authenticate(credentials)
  auth = Rack::Auth::Basic::Request.new(request.env)

  return false unless auth.provided? && auth.basic? && auth.credentials

  case credentials
  when Array
     auth.credentials == credentials
  when String
    auth.credentials.last == credentials
  else
    false
  end
end
authorized?() click to toggle source
# File lib/showoff.rb, line 1638
def authorized?
  # allow localhost if we have no password
  if not settings.showoff_config.has_key? 'password'
    localhost?
  else
    user     = settings.showoff_config['user'] || ''
    password = settings.showoff_config['password']
    authenticate([user, password])
  end
end
build_forms(content, classes=[]) click to toggle source

replace custom markup with html forms

# File lib/showoff.rb, line 849
def build_forms(content, classes=[])
  title = classes.collect { |cl| $1 if cl =~ /^form=(\w+)$/ }.compact.first
  # only process slides marked as forms
  return content if title.nil?

  begin
    tools =  '<div class="tools">'
    tools << "<input type=\"button\" class=\"display\" value=\"#{I18n.t('forms.display')}\">"
    tools << "<input type=\"submit\" class=\"save\" value=\"#{I18n.t('forms.save')}\" disabled=\"disabled\">"
    tools << '</div>'
    form  = "<form id='#{title}' action='form/#{title}' method='POST'>#{content}#{tools}</form>"
    doc = Nokogiri::HTML::DocumentFragment.parse(form)
    doc.css('p').each do |p|
      if p.text =~ /^(\w*) ?(?:->)? ?(.*)? (\*?)= ?(.*)?$/
        code     = $1
        id       = "#{title}_#{code}"
        name     = $2.empty? ? code : $2
        required = ! $3.empty?
        rhs      = $4

        p.replace form_element(id, code, name, required, rhs, p.text)
      end
    end
    doc.to_html
  rescue Exception => e
    @logger.warn "Form parsing failed: #{e.message}"
    @logger.debug "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
    content
  end
end
css_files() click to toggle source
# File lib/showoff.rb, line 256
def css_files
  base  = Dir.glob("#{settings.pres_dir}/*.css").map { |path| File.basename(path) }
  extra = Array(settings.showoff_config['styles'])
  base + extra
end
download() click to toggle source
# File lib/showoff.rb, line 1349
def download()
  begin
    shared = Dir.glob("#{settings.pres_dir}/_files/share/*").map { |path| File.basename(path) }
    # We use the icky -999 magic index because it has to be comparable for the view sort
    @downloads = { -999 => [ true, 'Shared Files', shared ] }
    @favicon = settings.showoff_config['favicon']
  rescue Errno::ENOENT => e
    # don't fail if the directory doesn't exist
    @downloads = {}
  end
  @downloads.merge! @@downloads
  erb :download
end
final_slide_fixup(text) click to toggle source

TODO: damn, this one is bad. It’s named generically so we can add to it if needed.

This method is intended to be the dumping ground for the slide fixups that we can't do in
other places until we get #615 implemented. Then this method should be refactored away.
# File lib/showoff.rb, line 723
def final_slide_fixup(text)
  # Turn this into a document for munging
  doc     = Nokogiri::HTML::DocumentFragment.parse(text)
  slide   = doc.at_css 'div.slide'
  content = doc.at_css 'div.content'

  # move each notes section outside of the content div
  doc.css('div.notes-section').each do |note|
    content.add_next_sibling(note)
  end

  # this is a list of classes that we want applied *only* to content, and not to the slide,
  # typically so that overly aggressive selectors don't match more than they should.
  blacklist = ['bigtext']
  slide['class'] = slide['class'].split.reject { |klass| blacklist.include? klass }.join(' ')

  doc.to_html
end
form_checked?(modifier) click to toggle source
# File lib/showoff.rb, line 1020
def form_checked?(modifier)
  modifier.downcase.include?('x') ? "checked='checked'" : ''
end
form_classes(modifier) click to toggle source
# File lib/showoff.rb, line 1012
def form_classes(modifier)
  modifier.downcase!
  classes = ['response']
  classes << 'correct' if modifier.include?('=')

  classes.join(' ')
end
form_element(id, code, name, required, rhs, text) click to toggle source
# File lib/showoff.rb, line 880
def form_element(id, code, name, required, rhs, text)
  required = required ? 'required' : ''
  str =  "<div class='form element #{required}' id='#{id}' data-name='#{code}'>"
  str << "<label class='question' for='#{id}'>#{name}</label>"
  case rhs
  when /^\[\s+(\d*)\]$$/             # value = [    5]                                     (textarea)
    str << form_element_textarea(id, code, $1)
  when /^___+(?:\[(\d+)\])?$/        # value = ___[50]                                     (text)
    str << form_element_text(id, code, $1)
  when /^\(.?\)/                     # value = (x) option one (=) opt2 () opt3 -> option 3 (radio)
    str << form_element_radio(id, code, rhs.scan(/\((.?)\)\s*([^()]+)\s*/))
  when /^\[.?\]/                     # value = [x] option one [=] opt2 [] opt3 -> option 3 (checkboxes)
    str << form_element_checkboxes(id, code, rhs.scan(/\[(.?)\] ?([^\[\]]+)/))
  when /^\{(.*)\}$/                  # value = {BOS, [SFO], (NYC)}                         (select shorthand)
    str << form_element_select(id, code, rhs.scan(/[(\[]?\w+[)\]]?/))
  when /^\{$/                        # value = {                                           (select)
    str << form_element_select_multiline(id, code, text)
  when ''                            # value =                                             (radio/checkbox list)
    str << form_element_multiline(id, code, text)
  else
    @logger.warn "Unmatched form element: #{rhs}"
  end
  str << '</div>'
end
form_element_check_or_radio(type, id, code, value, label, modifier) click to toggle source
# File lib/showoff.rb, line 1002
def form_element_check_or_radio(type, id, code, value, label, modifier)
  # yes, value and id are conflated, because this is the id of the parent widget
  checked = form_checked?(modifier)
  classes = form_classes(modifier)

  name = (type == 'checkbox') ? "#{code}[]" : code
  str  =  "<input type='#{type}' name='#{name}' id='#{id}_#{value}' value='#{value}' class='#{classes}' #{checked} />"
  str << "<label for='#{id}_#{value}' class='#{classes}'>#{label}</label>"
end
form_element_check_or_radio_set(type, id, code, items) click to toggle source
# File lib/showoff.rb, line 985
def form_element_check_or_radio_set(type, id, code, items)
  str = ''
  items.each do |item|
    modifier = item[0]

    if item[1] =~ /^(\w*) -> (.*)$/
      value = $1
      label = $2
    else
      value = label = item[1]
    end

    str << form_element_check_or_radio(type, id, code, value, label, modifier)
  end
  str
end
form_element_checkboxes(id, code, items) click to toggle source
# File lib/showoff.rb, line 918
def form_element_checkboxes(id, code, items)
  form_element_check_or_radio_set('checkbox', id, code, items)
end
form_element_multiline(id, code, text) click to toggle source
# File lib/showoff.rb, line 961
def form_element_multiline(id, code, text)
  str = '<ul>'

  text.split("\n")[1..-1].each do |item|
    case item
    when /\((.?)\)\s*(\w+)\s*(?:->\s*(.*)?)?/
      modifier = $1
      type     = 'radio'
      value    = $2
      label    = $3 || $2
    when /\[(.?)\]\s*(\w+)\s*(?:->\s*(.*)?)?/
      modifier = $1
      type     = 'checkbox'
      value    = $2
      label    = $3 || $2
    end

    str << '<li>'
    str << form_element_check_or_radio(type, id, code, value, label, modifier)
    str << '</li>'
  end
  str << '</ul>'
end
form_element_radio(id, code, items) click to toggle source
# File lib/showoff.rb, line 914
def form_element_radio(id, code, items)
  form_element_check_or_radio_set('radio', id, code, items)
end
form_element_select(id, code, items) click to toggle source
# File lib/showoff.rb, line 922
def form_element_select(id, code, items)
  str =  "<select id='#{id}_response' name='#{code}'>"
  str << '<option value="">----</option>'

  items.each do |item|
    if item =~ /\((\w+)\)/
      item     = $1
      selected = 'selected'
    else
      selected = ''
    end
    str << "<option value='#{item}' #{selected}>#{item}</option>"
  end
  str << '</select>'
end
form_element_select_multiline(id, code, text) click to toggle source
# File lib/showoff.rb, line 938
def form_element_select_multiline(id, code, text)
  str =  "<select id='#{id}_response' name='#{code}'>"
  str << '<option value="">----</option>'

  text.split("\n")[1..-1].each do |item|
    case item
    when /^   +\((\w+) -> (.+)\),?$/         # (NYC -> New York City)
      str << "<option value='#{$1}' selected>#{$2}</option>"
    when /^   +\[(\w+) -> (.+)\],?$/         # [NYC -> New York City]
      str << "<option value='#{$1}' class='correct'>#{$2}</option>"
    when /^   +(\w+) -> (.+),?$/             # NYC -> New, York City
      str << "<option value='#{$1}'>#{$2}</option>"
    when /^   +\((.+)\)$/                    # (Boston)
      str << "<option value='#{$1}' selected>#{$1}</option>"
    when /^   +\[(.+)\]$/                    # [Boston]
      str << "<option value='#{$1}' class='correct'>#{$1}</option>"
    when /^   +([^\(].+[^\),]),?$/           # Boston
      str << "<option value='#{$1}'>#{$1}</option>"
    end
  end
  str << '</select>'
end
form_element_text(id, code, length) click to toggle source
# File lib/showoff.rb, line 905
def form_element_text(id, code, length)
  "<input type='text' id='#{id}_response' name='#{code}' size='#{length}' />"
end
form_element_textarea(id, code, rows) click to toggle source
# File lib/showoff.rb, line 909
def form_element_textarea(id, code, rows)
  rows = 3 if rows.empty?
  "<textarea id='#{id}_response' name='#{code}' rows='#{rows}'></textarea>"
end
get_code_from_slide(path, index, executable=true) click to toggle source

Load a slide file from disk, parse it and return the text of a code block by index

# File lib/showoff.rb, line 1586
def get_code_from_slide(path, index, executable=true)
  if path =~ /^(.*)(?::)(\d+)$/
    path = $1
    num  = $2.to_i
  else
    num = 1
  end

  classes = executable ? 'code.execute' : 'code'

  slide = "#{path}.md"
  return [] unless File.exist? slide

  content = File.read(slide)
  return [] if content.nil?
  return [] if content.empty?

  if defined? num
    content = content.split(/^\<?!SLIDE/m).reject { |sl| sl.empty? }[num-1]
  end

  html = process_markdown(slide, '', content, {})
  doc  = Nokogiri::HTML::DocumentFragment.parse(html)

  if index == 'all'
    doc.css(classes).collect do |code|
      classes = code.attr('class').split rescue []
      lang    = classes.shift =~ /language-(\S*)/ ? $1 : nil

      [lang, code.text.gsub(/^\* /, ' '), classes]
    end
  else
    doc.css(classes)[index.to_i].text.gsub(/^\* /, ' ') rescue 'Invalid code block index'
  end
end
get_language_name(locale) click to toggle source

turns a locale code into a string name

# File lib/showoff.rb, line 299
def get_language_name(locale)
  with_locale(locale) do |str|
    result = ISO_639.find(str)
    result[3] unless result.nil?
  end
end
get_locale_dir(prefix, locale) click to toggle source

This function returns the directory containing translated content, defaulting to cwd. This works similarly to I18n fallback, but we cannot reuse that as it’s a different translation mechanism.

# File lib/showoff.rb, line 309
def get_locale_dir(prefix, locale)
  return '.' if locale == 'disable'

  with_locale(locale) do |str|
    path = "#{prefix}/#{str}"
    return path if File.directory?(path)
  end || '.'
end
get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil, :section=>nil}) click to toggle source
# File lib/showoff.rb, line 1165
    def get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil, :section=>nil})
      sections = nil
      Dir.chdir(get_locale_dir('locales', @locale)) do
        sections = ShowoffUtils.showoff_sections(settings.pres_dir, settings.showoff_config, @logger)
      end

      if sections
        data = ''
        sections.each do |section, slides|
          slides.each do |filename|
            next unless filename.end_with? '.md'
            path = filename.chomp('.md') # TODO: I don't know why we do this silly thing
            begin
              data << process_markdown(path, section, File.read(filename), opts)
            rescue Errno::ENOENT => e
              @logger.error e.message
              data << process_markdown(path, section, "!SLIDE\n# Missing File!\n## #{filename}", opts)
            end
          end

# I don't know what this part was supposed to do
#           if section =~ /^#/
#             name = section.each_line.first.gsub(/^#*/,'').strip
#             data << process_markdown(name, "<!SLIDE subsection>\n" + section, opts)
#           else

        end
      end
      process_content_for_all_slides(data, @slide_count, opts)
    end
get_translations() click to toggle source

returns a hash of all translations for the current language. This is used for the javascript half of the translations

# File lib/showoff.rb, line 344
def get_translations
  languages = I18n.backend.send(:translations)
  fallback  = I18n.fallbacks[I18n.locale].select { |f| languages.keys.include? f }.first
  languages[fallback]
end
guid() click to toggle source
# File lib/showoff.rb, line 1677
def guid
  # this is a terrifyingly simple GUID generator
  (0..15).to_a.map{|a| rand(16).to_s(16)}.join
end
index(static=false) click to toggle source
# File lib/showoff.rb, line 1234
def index(static=false)
  if static
    @title = ShowoffUtils.showoff_title(settings.pres_dir)
    @slides = get_slides_html(:static=>static)
    @pause_msg = ShowoffUtils.pause_msg
  end

  # Display favicon in the window if configured
  @favicon  = settings.showoff_config['favicon']

  # Check to see if the presentation has enabled feedback
  @feedback = settings.showoff_config['feedback'] unless (params && params[:feedback] == 'false')

  # If we're static, we need to not show the downloads page
  @static   = static

  # Provide a button in the sidebar for interactive editing if configured
  @edit     = settings.showoff_config['edit'] if @review

  # translated UI strings, according to the current locale
  @language = get_translations()

  # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc.
  manage_client_cookies()

  erb :index
end
inline_all_js(jses_directory) click to toggle source
# File lib/showoff.rb, line 1230
def inline_all_js(jses_directory)
   inline_js(Dir.entries(File.join(File.dirname(__FILE__), '..', jses_directory)).find_all{|filename| filename.length > 2 }, jses_directory)
end
inline_css(csses, pre = nil) click to toggle source
# File lib/showoff.rb, line 1196
def inline_css(csses, pre = nil)
  css_content = '<style type="text/css">'
  csses.each do |css_file|
    if pre
      css_file = File.join(File.dirname(__FILE__), '..', pre, css_file)
    else
      css_file = File.join(settings.pres_dir, css_file)
    end
    css_content += File.read(css_file)
  end
  css_content += '</style>'
  css_content
end
inline_js(jses, pre = nil) click to toggle source
# File lib/showoff.rb, line 1210
def inline_js(jses, pre = nil)
  js_content = '<script type="text/javascript">'
  jses.each do |js_file|
    if pre
      js_file = File.join(File.dirname(__FILE__), '..', pre, js_file)
    else
      js_file = File.join(settings.pres_dir, js_file)
    end

    begin
      js_content += File.read(js_file)
    rescue Errno::ENOENT
      $stderr.puts "WARN: Failed to inline JS. No such file: #{js_file}"
      next
    end
  end
  js_content += '</script>'
  js_content
end
js_files() click to toggle source
# File lib/showoff.rb, line 262
def js_files
  base  = Dir.glob("#{settings.pres_dir}/*.js").map { |path| File.basename(path) }
  extra = Array(settings.showoff_config['scripts'])
  base + extra
end
language_names() click to toggle source

return a hash of all language codes available and the long name description of each

# File lib/showoff.rb, line 319
def language_names
  strings = JSON.parse(File.read('locales/strings.json')) rescue {}
  locales = Dir.glob('locales/*')
               .select {|f| File.directory?(f) }
               .map    {|f| File.basename(f)   }

  (strings.keys + locales).inject({}) do |memo, locale|
    memo.update(locale => get_language_name(locale))
  end
end
locale(user_locale) click to toggle source

returns the minimized canonical version of the current selected content locale it assumes that if the user has specified a locale, that it’s already minimized note: if the locale doesn’t exist on disk, it will just default to no translation

# File lib/showoff.rb, line 333
def locale(user_locale)
  if [nil, '', 'auto'].include? user_locale
    languages = I18n.available_locales
    I18n.fallbacks[I18n.locale].select { |f| languages.include? f }.first
  else
    user_locale
  end
end
localhost?() click to toggle source
# File lib/showoff.rb, line 1658
def localhost?
  request.env['REMOTE_HOST'] == 'localhost' or request.ip == '127.0.0.1'
end
locked!() click to toggle source
# File lib/showoff.rb, line 1630
def locked!
  # check auth first, because if the presenter has logged in with a password, we don't want to prompt again
  unless authorized? or unlocked?
    response['WWW-Authenticate'] = %(Basic realm="#{@title}: Locked Area. A presentation key is required to view.")
    throw(:halt, [401, "Not authorized."])
  end
end
manage_client_cookies(presenter=false) click to toggle source
# File lib/showoff.rb, line 1691
def manage_client_cookies(presenter=false)
  # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc.
  if request.nil?   # when running showoff static
    @client_id = guid()
  else
    if request.cookies['client_id']
      @client_id = request.cookies['client_id']
    else
      @client_id = guid()
      response.set_cookie('client_id', @client_id)
    end

    # if we have no content translations then remove the cookie
    response.delete_cookie('locale') if language_names.empty?
  end

  if presenter
    @@master ||= @client_id
    @@cookie ||= guid()
    response.set_cookie('presenter', @@cookie)
  end
end
mapped_keys(action, klass='key') click to toggle source

return a list of keys associated with a given action in the keymap

# File lib/showoff.rb, line 275
def mapped_keys(action, klass='key')
  list = @keymap.select { |key,value| value == action }.keys

  if klass
    list.map { |val| "<span class=\"#{klass}\">#{val}</span>" }.join
  else
    list.join ', '
  end
end
master_presenter?() click to toggle source
# File lib/showoff.rb, line 1687
def master_presenter?
  @@master == @client_id
end
pdf(name) click to toggle source
# File lib/showoff.rb, line 1447
def pdf(name)
  @slides = get_slides_html(:static=>true, :toc=>true, :print=>true)
  @inline = true

  html = erb :onepage

  # Process inline css and js for included images
  # The css uses relative paths for images and we prepend the file url
  html.gsub!(/url\([\"\']?(?!https?:\/\/)(.*?)[\"\']?\)/) do |s|
    "url(file://#{settings.pres_dir}/#{$1})"
  end

  # remove the weird /files component, since that doesn't exist on the filesystem
  # replace it for file://<PATH> for correct use with wkhtmltopdf (exactly with qt-webkit)
  html.gsub!(/<img src=".\/file\/([^"]*)/) do |s|
    "<img src=\"file:\/\/#{settings.pres_dir}\/#{$1}"
  end

  # PDFKit.new takes the HTML and any options for wkhtmltopdf
  # run `wkhtmltopdf --extended-help` for a full list of options
  kit = PDFKit.new(html, ShowoffUtils.showoff_pdf_options(settings.pres_dir))

  # Save the PDF to a file
  kit.to_file(name)
end
presenter() click to toggle source
# File lib/showoff.rb, line 1262
def presenter
  @favicon   = settings.showoff_config['favicon']
  @issues    = settings.showoff_config['issues']
  @edit      = settings.showoff_config['edit'] if @review
  @feedback  = settings.showoff_config['feedback']
  @language  = get_translations()

  manage_client_cookies(true)

  erb :presenter
end
preshow_files() click to toggle source
# File lib/showoff.rb, line 268
def preshow_files
  files = Dir.glob("#{settings.pres_dir}/_preshow/*")
  files.reject! { |path| ['.txt', '.md'].include? File.extname(path) }
  files.map { |path| File.basename(path) }.to_json
end
print(section=nil, munged=false) click to toggle source
process_content_for_all_slides(content, num_slides, opts={}) click to toggle source
# File lib/showoff.rb, line 742
def process_content_for_all_slides(content, num_slides, opts={})
  # this has to be text replacement for now, since the string can appear in any context
  content.gsub!("~~~NUM_SLIDES~~~", num_slides.to_s)
  doc = Nokogiri::HTML::DocumentFragment.parse(content)

  # Should we build a table of contents?
  if opts[:toc]
    toc = Nokogiri::HTML::DocumentFragment.parse("<p id=\"toc\"></p>")

    case opts[:toc]
      when :all
        titles = doc.css('div.slide:not(.toc) > div.content:not(.cover) > h1:not(.section_title)')
      else
        titles = doc.css('div.subsection > h1:not(.section_title)')
    end

    titles.each do |section|
      href = section.parent.parent['id']
      frag = "<div class=\"tocentry\"><a href=\"##{href}\">#{section.content}</a></div>"
      link = Nokogiri::HTML::DocumentFragment.parse(frag)

      toc.children.first.add_child(link)
    end

    # swap out the tag, if found, with the table of contents
    doc.at('p:contains("~~~TOC~~~")').replace(toc) rescue nil
  end

  doc.css('.slide.glossary .content').each do |glossary|
    name = (glossary.attr('class').split - ['content', 'glossary']).first
    list = Nokogiri::HTML::DocumentFragment.parse('<ul class="glossary terms"></ul>')
    seen = []

    doc.css('.callout.glossary').each do |item|
      target = (item.attr('class').split - ['callout', 'glossary']).first

      # if the name matches or if we didn't name it to begin with.
      next unless target == name

      # the definition can exist in multiple places, so de-dup it here
      term = item.attr('data-term')
      next if seen.include? term
      seen << term

      # excrutiatingly find the parent slide content and grab the ref
      # in a library less shitty, this would be something like
      # $(this).parent().siblings('.content').attr('ref')
      href = nil
      item.ancestors('.slide').first.traverse do |element|
        next if element['class'].nil?
        next unless element['class'].split.include? 'content'

        href = element.attr('ref').gsub('/', '_')
      end

      text   = item.attr('data-text')
      link   = item.attr('data-target')
      page   = glossary.attr('ref')
      anchor = "#{page}+#{link}"
      next if href.nil? or text.nil? or link.nil?

      frag = "<li><a id=\"#{anchor}\" class=\"label\">#{term}</a>#{text}<a href=\"##{href}\" class=\"return\">↩</a></li>"
      item = Nokogiri::HTML::DocumentFragment.parse(frag)

      list.children.first.add_child(item)
    end

    glossary.add_child(list)
  end

  # now fix all the links to point to the glossary page
  doc.css('a').each do |link|
    next if link['href'].nil?
    next unless link['href'].start_with? 'glossary://'

    href = link['href']
    href.slice!('glossary://')

    parts  = href.split('/')
    target = parts.pop
    name   = parts.pop # either the glossary name or nil

    classes = name.nil? ? ".slide.glossary" : ".slide.glossary.#{name}"
    href    = doc.at("#{classes} .content").attr('ref') rescue nil

    link['href'] = "##{href}+#{target}"
  end

  doc.to_html
end
process_content_for_language(content, locale) click to toggle source
# File lib/showoff.rb, line 542
def process_content_for_language(content, locale)
    lang = locale.to_s.split('-').first
    result = content

    content.scan(/^((~~~LANG:([\w-]+)~~~\n)(.+?)(\n~~~ENDLANG~~~\n))/m).each do |match|
        if match[2] == lang or match[2] == locale.to_s
            result.sub!(match[0], match[3])
        else
            result.sub!(match[0], "\n")
        end
    end

    result
end
process_content_for_replacements(content) click to toggle source

This method processes the content of the slide and replaces content markers with their actual value information

# File lib/showoff.rb, line 559
def process_content_for_replacements(content)
  # update counters, incrementing section:minor if needed
  result = content.gsub("~~~CURRENT_SLIDE~~~", @slide_count.to_s)
  result.gsub!("~~~SECTION:MAJOR~~~", @section_major.to_s)
  if result.include? "~~~SECTION:MINOR~~~"
    @section_minor += 1
    result.gsub!("~~~SECTION:MINOR~~~", @section_minor.to_s)
  end

  # scan for pagebreak tags. Should really only be used for handout notes or supplemental materials
  result.gsub!("~~~PAGEBREAK~~~", '<div class="pagebreak">continued...</div>')

  # replace with form rendering placeholder
  result.gsub!(/~~~FORM:([^~]*)~~~/, '<div class="form wrapper" title="\1"></div>')

  # Now check for any kind of options
  content.scan(/(~~~CONFIG:(.*?)~~~)/).each do |match|
    parts = match[1].split('.') # Use dots ('.') to separate Hash keys
    if parts.size > 1
      value = settings.showoff_config.dig(parts[0]).to_h.dig(*parts[1..-1])
    else
      value = settings.showoff_config.fetch(parts[0],nil)
    end

    unless value.is_a?(String)
      msg = "#{match[0]} refers to a non-String data type (#{value.class})"
      msg = "#{match[0]}: not found in settings data" if value.nil?
      @logger.warn(msg)
      next
    end

    result.gsub!(match[0], value)
  end

  # Load and replace any file tags
  content.scan(/(~~~FILE:([^:~]*):?(.*)?~~~)/).each do |match|
    # make a list of code highlighting classes to include
    css  = match[2].split.collect {|i| "language-#{i.downcase}" }.join(' ')

    # get the file content and parse out html entities
    name = match[1]
    file = File.read(File.join(settings.pres_dir, '_files', name)) rescue "Nonexistent file: #{name}"
    file = "Empty file: #{name}" if file.empty?
    file = HTMLEntities.new.encode(file) rescue "HTML parsing of #{name} failed"

    result.gsub!(match[0], "<pre class=\"highlight\"><code class=\"#{css}\">#{file}</code></pre>")
  end

  result.gsub!(/\[(fa\w?)-(\S*)\]/, '<i class="\1 fa-\2"></i>')

  # For fenced code blocks, translate the space separated classes into one
  # colon separated string so Commonmarker doesn't ignore the rest
  result.gsub!(/^`{3} *(.+)$/) {|s| "``` #{$1.split.join(':')}"}

  result
end
process_content_for_section_tags(content, name = nil, opts = {}) click to toggle source

replace section tags with classed div tags

# File lib/showoff.rb, line 617
def process_content_for_section_tags(content, name = nil, opts = {})
  return unless content

  # because this is post markdown rendering, we may need to shift a <p> tag around
  # remove the tags if they're by themselves
  result = content.gsub(/<p>~~~SECTION:([^~]*)~~~<\/p>/, '<div class="notes-section \1">')
  result.gsub!(/<p>~~~ENDSECTION~~~<\/p>/, '</div>')

  # shove it around the div if it belongs to the contained element
  result.gsub!(/(<p>)?~~~SECTION:([^~]*)~~~/, '<div class="notes-section \2">\1')
  result.gsub!(/~~~ENDSECTION~~~(<\/p>)?/, '\1</div>')

  # Turn this into a document for munging
  doc = Nokogiri::HTML::DocumentFragment.parse(result)

  filename = File.join(settings.pres_dir, '_notes', "#{name}.md")
  @logger.debug "personal notes filename: #{filename}"
  if [nil, 'notes'].include? opts[:section] and File.file? filename
    # Make sure we've got a notes div to hang personal notes from
    doc.add_child '<div class="notes-section notes"></div>' if doc.css('div.notes-section.notes').empty?
    doc.css('div.notes-section.notes').each do |section|
      text = Tilt[:markdown].new(nil, nil, @engine_options) { File.read(filename) }.render
      frag = "<div class=\"personal\"><h1>#{I18n.t('presenter.notes.personal')}</h1>#{text}</div>"
      note = Nokogiri::HTML::DocumentFragment.parse(frag)

      if section.children.size > 0
        section.children.before(note)
      else
        section.add_child(note)
      end
    end
  end

  doc.css('.callout.glossary').each do |item|
    next unless item.content =~ /^([^|]+)\|([^:]+):(.*)$/
    item['data-term']   = $1
    item['data-target'] = $2
    item['data-text']   = $3
    item.content        = $3

    glossary = (item.attr('class').split - ['callout', 'glossary']).first
    address  = glossary ? "#{glossary}/#{$2}" : $2
    frag     = "<a class=\"processed label\" href=\"glossary://#{address}\">#{$1}</a>"

    item.children.before(Nokogiri::HTML::DocumentFragment.parse(frag))
  end

  # Process links
  doc.css('a').each do |link|
    next unless link['href']
    next if link['href'].start_with? '#'
    next if link['class'].split.include? 'processed' rescue nil

    # If these are glossary links, populate the notes/handouts sections
    if link['href'].start_with? 'glossary://'
      doc.add_child '<div class="notes-section notes"></div>' if doc.css('div.notes-section.notes').empty?
      doc.add_child '<div class="notes-section handouts"></div>' if doc.css('div.notes-section.handouts').empty?

      term = link.content
      text = link['title']
      href = link['href']
      href.slice!('glossary://')

      parts  = href.split('/')
      target = parts.pop
      name   = parts.pop # either the glossary name or nil

      link['class']  = 'term'

      label = link.clone
      label['class'] = 'label processed'

      frag = Nokogiri::HTML::DocumentFragment.parse('<p></p>')
      definition = frag.children.first
      definition['class'] = "callout glossary #{name}"
      definition['data-term']   = term
      definition['data-target'] = target
      definition['data-text']   = text
      definition.content = text
      definition.children.before(label)

      [doc.css('div.notes-section.notes'), doc.css('div.notes-section.handouts')].each do |section|
        section.first.add_child(definition.clone)
      end

    else
      # Add a target so we open all external links from notes in a new window
      link.set_attribute('target', '_blank')
    end
  end

  # finally, remove any sections we don't want to print
  if opts[:section]
    doc.css('div.notes-section').each do |section|
      section.remove unless section.attr('class').split.include? opts[:section]
    end
  end

  doc.to_html
end
process_markdown(name, section, content, opts={:static=>false, :pdf=>false, :print=>false, :toc=>false, :supplemental=>nil, :section=>nil, :merged=>false}) click to toggle source
# File lib/showoff.rb, line 389
def process_markdown(name, section, content, opts={:static=>false, :pdf=>false, :print=>false, :toc=>false, :supplemental=>nil, :section=>nil, :merged=>false})
  if settings.encoding and content.respond_to?(:force_encoding)
    content.force_encoding(settings.encoding)
  end
  @logger.debug "renderer: #{Tilt[:markdown].name}"
  @logger.debug "render options: #{@engine_options.inspect}"

  # if there are no !SLIDE markers, then make every H1 define a new slide
  unless content =~ /^\<?!SLIDE/m
    content = content.gsub(/^# /m, "<!SLIDE>\n# ")
  end

  # todo: unit test
  lines = content.split("\n")
  @logger.debug "#{name}: #{lines.length} lines"
  slides = []
  slides << (slide = Slide.new)
  until lines.empty?
    line = lines.shift
    if line =~ /^<?!SLIDE(.*)>?/
      ctx = $1 ? $1.strip : $1
      slides << (slide = Slide.new(ctx))
    else
      slide << line
    end
  end

  slides.delete_if {|slide| slide.empty? and not slide.bg }

  final = ''
  if slides.size > 1
    seq = 1
  end
  slides.each do |slide|
    # update section counters before we reject slides so the numbering is consistent
    if slide.classes.include? 'subsection'
      @section_major += 1
      @section_minor = 0
    end

    # merged output means that we just want to generate *everything*. This is used by internal,
    # methods such as content validation, where we want all content represented.
    unless opts[:merged]
      if opts[:supplemental]
        # if we're looking for supplemental material, only include the content we want
        next unless slide.classes.include? 'supplemental'
        next unless slide.classes.include? opts[:supplemental]
      else
        # otherwise just skip all supplemental material completely
        next if slide.classes.include? 'supplemental'
      end

      unless opts[:toc]
        # just drop the slide if we're not generating a table of contents
        next if slide.classes.include? 'toc'
      end

      if opts[:print]
        # drop all slides not intended for the print version
        next if slide.classes.include? 'noprint'
      else
        # drop slides that are intended for the print version only
        next if slide.classes.include? 'printonly'
      end
    end

    @slide_count += 1
    content_classes = slide.classes

    # extract transition, defaulting to none
    transition = 'none'
    content_classes.delete_if { |x| x =~ /^transition=(.+)/ && transition = $1 }
    # extract id, defaulting to none
    id = nil
    content_classes.delete_if { |x| x =~ /^#([\w-]+)/ && id = $1 }
    id = name.dup unless id
    id.gsub!(/[^-A-Za-z0-9_]/, '_') # valid HTML id characters
    id << seq.to_s if seq

    @logger.debug "id: #{id}" if id
    @logger.debug "classes: #{content_classes.inspect}"
    @logger.debug "transition: #{transition}"
    @logger.debug "tpl: #{slide.tpl} " if slide.tpl
    @logger.debug "bg: #{slide.bg}" if slide.bg


    template = "~~~CONTENT~~~"
    # Template handling
    if settings.pres_template
      # We allow specifying a new template even when default is
      # not given.
      if settings.pres_template.include?(slide.tpl) and
          File.exist?(settings.pres_template[slide.tpl])
        template = File.open(settings.pres_template[slide.tpl], "r").read()
      end
    end

    # create html for the slide
    classes = content_classes.join(' ')
    content = "<div data-section=\"#{section}\" data-title=\"#{File.basename(name)}\""
    content += " id=\"#{id}\"" if id
    content += " style=\"background-image: url('file/#{slide.bg}');\"" if slide.bg
    content += " class=\"slide #{classes}\" data-transition=\"#{transition}\">"

    # name the slide. If we've got multiple slides in this file, we'll have a sequence number
    # include that sequence number to index directly into that content
    ref = seq ? "#{name}:#{seq.to_s}" : name
    content += "<div class=\"content #{classes}\" ref=\"#{ref}\">\n"
    @@slide_titles << ref

    # renderers like wkhtmltopdf needs an <h1> tag to use for a section title, but only when printing.
    if opts[:print]
      # reset subsection each time we encounter a new subsection slide. Do this in a regex, because it's much
      # easier to just get the first of any header than it is after rendering to html.
      if content_classes.include? 'subsection'
        @section_title = slide.text.match(/#+ *(.*?)#*$/)[1] rescue settings.showoff_config['name']
      end
      # include a header that's hidden by CSS the renderer can use it, but not be visible
      content += "<h1 class=\"section_title\">#{@section_title}</h1>\n"
    end

    # Apply the template to the slide and replace the key to generate the content of the slide
    sl = process_content_for_replacements(template.gsub(/~~~CONTENT~~~/, slide.text))
    sl = process_content_for_language(sl, I18n.locale)
    sl = Tilt[:markdown].new(nil, nil, @engine_options) { sl }.render
    sl = build_forms(sl, content_classes)
    sl = update_p_classes(sl)
    sl = process_content_for_section_tags(sl, name, opts)
    sl = update_special_content(sl, @slide_count, name) # TODO: deprecated
    sl = update_image_paths(name, sl, opts)

    content += sl
    content += "</div>\n"
    if content_classes.include? 'activity'
      content += '<span class="activityToggle">'
      content += "  <label for=\"activity-#{ref}\">#{I18n.t('activity_complete')}</label>"
      content += "  <input type=\"checkbox\" class=\"activity\" name=\"activity-#{ref}\" id=\"activity-#{ref}\">"
      content += '</span>'
    end
    content += "<canvas class=\"annotations\"></canvas>\n"
    content += "</div>\n"

    content = final_slide_fixup(content)

    final += update_commandline_code(content)

    if seq
      seq += 1
    end
  end
  final
end
protected!() click to toggle source

Basic auth boilerplate

# File lib/showoff.rb, line 1623
def protected!
  unless authorized?
    response['WWW-Authenticate'] = %(Basic realm="#{@title}: Protected Area. Please log in.")
    throw(:halt, [401, "Not authorized."])
  end
end
require_ruby_files() click to toggle source
# File lib/showoff.rb, line 251
def require_ruby_files
  Dir.glob("#{settings.pres_dir}/*.rb").map { |path| require path }
end
slides(static=false, merged=false) click to toggle source
# File lib/showoff.rb, line 1311
def slides(static=false, merged=false)
  @logger.info "Cached presentations: #{@@cache.keys}"

  # if we have a cache and we're not asking to invalidate it
  return @@cache[@locale] if (@@cache[@locale] and params['cache'] != 'clear')

  @logger.info "Generating locale: #{@locale}"

  # If we're displaying from a repository, let's update it
  ShowoffUtils.update(settings.verbose) if settings.url

  @@slide_titles = []
  content = get_slides_html(:static=>static, :merged=>merged)

  # allow command line cache disabling
  @@cache[@locale] = content unless settings.nocache
  content
end
stats() click to toggle source
# File lib/showoff.rb, line 1429
def stats()
  if localhost?
    # the presenter should have full stats in the erb
    @counter = @@counter['pageviews']
  end

  # for the full page view. Maybe to be disappeared
  @all = Hash.new
  @@counter['pageviews'].each do |slide, stats|
    @all[slide] = 0
    stats.map do |host, visits|
      visits.each { |entry| @all[slide] += entry['elapsed'].to_f }
    end
  end

  erb :stats
end
stats_data() click to toggle source
# File lib/showoff.rb, line 1363
def stats_data()
  data = {}
  begin

    # what are viewers looking at right now?
    now = Time.now.to_i # let's throw away viewers who haven't done anything in 5m
    active  = @@counter['current'].select {|client, view| (now - view[1]).abs < 300 }

    # percentage of stray viewers
    stray   = active.select {|client, view| view[0] != @@current[:name] }
    stray_p = ((stray.size.to_f / active.size.to_f) * 100).to_i rescue 0
    data['stray_p'] = stray_p

    # percentage of idle viewers
    idle    = @@counter['current'].size - active.size
    idle_p  = ((idle.to_f / @@counter['current'].size.to_f) * 100).to_i rescue 0
    data['idle_p']  = idle_p

    viewers = @@slide_titles.map do |slide|
      count = active.select {|client, view| view[0] == slide }.size
      flags = (slide == @@current[:name]) ? 'current' : nil
      [count, slide, nil, flags]
    end

    # trim the ends, if nobody's looking we don't much care.
    viewers.pop while viewers.last[0] == 0
    viewers.shift while viewers.first[0] == 0
    viewmax = viewers.max_by {|view| view[0] }.first

    data['viewers'] = viewers
    data['viewmax'] = viewmax
  rescue => e
    @logger.warn "Not enough data to generate pageviews."
    @logger.debug e.message
    @logger.debug e.backtrace.first
  end

  begin
    # current elapsed time for the zoomline view
    elapsed = @@slide_titles.map do |slide|
      if @@counter['pageviews'][slide].nil?
        time = 0
      else
        time = @@counter['pageviews'][slide].inject(0) do |outer, (viewer, views)|
          outer += views.inject(0) { |inner, view| inner += view['elapsed'] }
        end
      end
      string = Time.at(time).gmtime.strftime('%M:%S')
      flags  = (slide == @@current[:name]) ? 'current' : nil

      [ time, slide, string, flags ]
    end
    maxtime = elapsed.max_by {|view| view[0] }.first

    data['elapsed'] = elapsed
    data['maxtime'] = maxtime
  rescue => e
    # expected if this is loaded before a presentation has been compiled
    @logger.warn "Not enough data to generate elapsed time."
    @logger.debug e.message
    @logger.debug e.backtrace.first
  end

  data.to_json
end
supplemental(content, static=false) click to toggle source
# File lib/showoff.rb, line 1341
def supplemental(content, static=false)
  # supplemental material is by definition separate from the presentation, so it doesn't make sense to attach notes
  @slides = get_slides_html(:static=>static, :supplemental=>content, :section=>false, :toc=>:all)
  @favicon = settings.showoff_config['favicon']
  @wrapper_classes = ['supplemental']
  erb :onepage
end
unlocked?() click to toggle source
# File lib/showoff.rb, line 1649
def unlocked?
  # allow localhost if we have no key
  if not settings.showoff_config.has_key? 'key'
    localhost?
  else
    authenticate(settings.showoff_config['key'])
  end
end
update_commandline_code(slide) click to toggle source
# File lib/showoff.rb, line 1105
def update_commandline_code(slide)
  html = Nokogiri::HTML::DocumentFragment.parse(slide)
  parser = CommandlineParser.new

  html.css('pre').each do |pre|
    pre.css('code').each do |code|
      out  = code.text
      lang = code.get_attribute('class')

      # Skip this if we've got an empty code block
      next if out.empty?

      # catch fenced code blocks from commonmarker
      if (lang and lang.start_with? 'language-' )
        pre.set_attribute('class', 'highlight')
        # turn the colon separated name back into classes
        code.set_attribute('class', lang.gsub(':', ' '))

      # or we've started a code block with a Showoff language tag
      elsif out.strip[0, 3] == '@@@'
        lines = out.split("\n")
        lang  = lines.shift.gsub('@@@', '').strip
        pre.set_attribute('class', 'highlight')
        code.set_attribute('class', 'language-' + lang.downcase) if !lang.empty?
        code.content = lines.join("\n")
      end

    end
  end

  html.css('.commandline > pre > code').each do |code|
    out = code.text
    code.content = ''
    tree = parser.parse(out)
    transform = Parslet::Transform.new do
      rule(:prompt => simple(:prompt), :input => simple(:input), :output => simple(:output)) do
        command = Nokogiri::XML::Node.new('code', html)
        command.set_attribute('class', 'command')
        command.content = "#{prompt} #{input}"
        code << command

        # Add newline after the input so that users can
        # advance faster than the typewriter effect
        # and still keep inputs on separate lines.
        code << "\n"

        unless output.to_s.empty?

          result = Nokogiri::XML::Node.new('code', html)
          result.set_attribute('class', 'result')
          result.content = output
          code << result
        end
      end
    end
    transform.apply(tree)
  end
  html.to_html
end
update_image_paths(path, slide, opts={:static=>false, :pdf=>false}) click to toggle source
# File lib/showoff.rb, line 1076
def update_image_paths(path, slide, opts={:static=>false, :pdf=>false})
  doc       = Nokogiri::HTML::DocumentFragment.parse(slide)
  slide_dir = File.dirname(path)

  case
  when opts[:static] && opts[:pdf]
    replacement_prefix = "file://#{settings.pres_dir}"
  when opts[:static]
    replacement_prefix = './file'
  else
    replacement_prefix = 'image'
  end

  doc.css('img').each do |img|

    # does the image path start from the preso root?
    if img[:src].start_with? '/'
      img_path = img[:src]
    else
      # clean up the path and remove some of the relative nonsense
      img_path = Pathname.new(File.join(slide_dir, img[:src])).cleanpath.to_path
    end
    src       = "#{replacement_prefix}/#{img_path}"
    img[:src] = src

  end
  doc.to_html
end
update_p_classes(content) click to toggle source

Find any lines that start with a <p>.(something), remove the ones tagged with .break and .comment, then turn the remainder into <p class=“something”> The perlism line noise is splitting multiple classes (.class1.class2) on the period.

TODO: We really need to update this to use the DOM instead of text parsing :/

# File lib/showoff.rb, line 839
def update_p_classes(content)
  # comment & break
  content.gsub!(/<p>\.(?:break|comment)( .*)?<\/p>/, '')
  # paragraph classes
  content.gsub!(/<p>\.(.*?) /) { "<p class=\"#{$1.gsub('.', ' ')}\">" }
  # image classes
  content.gsub(/<img src="(.*)" alt="(\.\S*)\s*(.*)">/) { "<img src=\"#{$1}\" class=\"#{$2.gsub('.', ' ')}\" alt=\"#{$3}\">" }
end
update_special_content(content, seq, name) click to toggle source

TODO: deprecated

# File lib/showoff.rb, line 1025
def update_special_content(content, seq, name)
  doc = Nokogiri::HTML::DocumentFragment.parse(content)
  %w[notes handouts instructor solguide].each { |mark|  update_special_content_mark(doc, mark) }
  update_download_links(doc, seq, name)

  # TODO: what the bloody hell. Figure out how to either make Nokogiri output closed
  # tags or figure out how to get its XML output to quit adding gratuitious spaces.
  doc.to_html.gsub(/(<img [^>]*)>/, '\1 />')
end
update_special_content_mark(doc, mark) click to toggle source

TODO: deprecated

# File lib/showoff.rb, line 1036
def update_special_content_mark(doc, mark)
  container = doc.css("p.#{mark}").first
  return unless container

  @logger.warn "Special mark (#{mark}) is deprecated. Please replace with section tags. See the README for details."

  # only allow localhost to print the instructor guide
  if mark == 'instructor' and request.env['REMOTE_HOST'] != 'localhost'
    container.remove
  else
    raw      = container.inner_html
    fixed    = raw.gsub(/^\.#{mark} ?/, '')
    markdown = Tilt[:markdown].new { fixed }.render

    container.name       = 'div'
    container.inner_html = markdown
  end
end
user_translations() click to toggle source

Finds the language key from strings.json and returns the strings hash. This is used for user translations in the presentation, e.g. SVG translations.

# File lib/showoff.rb, line 352
def user_translations
  return {} unless File.file? 'locales/strings.json'
  strings = JSON.parse(File.read('locales/strings.json')) rescue {}

  with_locale(@locale) do |key|
    return strings[key] if strings.include? key
  end
  {}
end
with_locale(locale) { |locale| ... } click to toggle source

This is just a unified lookup method that takes a full locale name and then resolves it to an available version of the name

# File lib/showoff.rb, line 287
def with_locale(locale)
  locale = locale.to_s
  until (locale.empty?) do
    result = yield(locale)
    return result unless result.nil?

    # if not found, chop off a section and try again
    locale = locale.rpartition(/[-_]/).first
  end
end