class Showoff
Constants
- GEMROOT
Attributes
Public Class Methods
# 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
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
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
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
# 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
# File lib/showoff.rb, line 246 def self.pres_dir_current opt = {:pres_dir => Dir.pwd} Showoff.set opt end
Public Instance Methods
# 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
# 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
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
# File lib/showoff.rb, line 1274 def clean_link(href) if href && href[0, 1] == '/' href = href[1, href.size] end href end
# 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
# 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
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
# File lib/showoff.rb, line 1020 def form_checked?(modifier) modifier.downcase.include?('x') ? "checked='checked'" : '' end
# File lib/showoff.rb, line 1012 def form_classes(modifier) modifier.downcase! classes = ['response'] classes << 'correct' if modifier.include?('=') classes.join(' ') end
# 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
# 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
# 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
# File lib/showoff.rb, line 918 def form_element_checkboxes(id, code, items) form_element_check_or_radio_set('checkbox', id, code, items) end
# 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
# File lib/showoff.rb, line 914 def form_element_radio(id, code, items) form_element_check_or_radio_set('radio', id, code, items) end
# 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
# 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
# File lib/showoff.rb, line 905 def form_element_text(id, code, length) "<input type='text' id='#{id}_response' name='#{code}' size='#{length}' />" end
# 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
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
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
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
# 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
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
# 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
# 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
# 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
# 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
# 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
# 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
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
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
# File lib/showoff.rb, line 1658 def localhost? request.env['REMOTE_HOST'] == 'localhost' or request.ip == '127.0.0.1' end
# 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
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
# File lib/showoff.rb, line 1687 def master_presenter? @@master == @client_id end
# 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
# 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
# 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
# File lib/showoff.rb, line 1330 def print(section=nil, munged=false) @slides = get_slides_html(:static=>true, :toc=>true, :print=>true, :section=>section) @favicon = settings.showoff_config['favicon'] unless munged @baseurl = '../' * section.split('/').count end erb :onepage end
# 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
# 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
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
# 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
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
# File lib/showoff.rb, line 251 def require_ruby_files Dir.glob("#{settings.pres_dir}/*.rb").map { |path| require path } end
# 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
# 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
# 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
# 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
# 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
# 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
# File lib/showoff.rb, line 1056 def update_download_links(doc, seq, name) container = doc.css("p.download").first return unless container raw = container.text fixed = raw.gsub(/^\.download ?/, '') # first create the data structure # [ enabled, slide name, [array, of, files] ] @@downloads[seq] = [ false, name, [] ] fixed.split("\n").each { |file| # then push each file onto the list @@downloads[seq][2].push(file.strip) } container.remove end
# 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
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
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
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
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
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