lib/showoff.rb in showoff-0.9.11.1 vs lib/showoff.rb in showoff-0.10.0

- old
+ new

@@ -8,10 +8,11 @@ require 'sinatra-websocket' here = File.expand_path(File.dirname(__FILE__)) require "#{here}/showoff_utils" require "#{here}/commandline_parser" +require "#{here}/keymap" begin require 'RMagick' rescue LoadError # nop @@ -42,11 +43,12 @@ set :server, 'thin' set :sockets, [] set :presenters, [] set :verbose, false - set :review, false + set :review, false + set :execute, false set :pres_dir, '.' set :pres_file, 'showoff.json' set :page_size, "Letter" set :pres_template, nil @@ -57,19 +59,25 @@ super(app) @logger = Logger.new(STDOUT) @logger.formatter = proc { |severity,datetime,progname,msg| "#{progname} #{msg}\n" } @logger.level = settings.verbose ? Logger::DEBUG : Logger::WARN - @review = settings.review + @review = settings.review + @execute = settings.execute dir = File.expand_path(File.join(File.dirname(__FILE__), '..')) @logger.debug(dir) showoff_dir = File.expand_path(File.join(File.dirname(__FILE__), '..')) 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')) + @keymap = Keymap.default + @keymap.merge! JSON.parse(File.read(keymapfile)) rescue {} + settings.pres_dir = File.expand_path(settings.pres_dir) if (settings.pres_file) ShowOffUtils.presentation_config_file = settings.pres_file end @@ -83,10 +91,22 @@ settings.encoding = showoff_json["encoding"] settings.page_size = showoff_json["page-size"] || "Letter" settings.pres_template = showoff_json["templates"] end + # code execution timeout + settings.showoff_config['timeout'] ||= 15 + + # 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 'Showoff Presentation' + @logger.debug settings.pres_template @cached_image_size = {} @logger.debug settings.pres_dir @pres_name = settings.pres_dir.split('/').pop @@ -178,15 +198,25 @@ def js_files Dir.glob("#{settings.pres_dir}/*.js").map { |path| File.basename(path) } end - def preshow_files Dir.glob("#{settings.pres_dir}/_preshow/*").map { |path| File.basename(path) }.to_json end + # return a list of keys associated with a given action in the keymap + 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 + # todo: move more behavior into this class class Slide attr_reader :classes, :text, :tpl, :bg def initialize( context = "") @@ -374,17 +404,20 @@ result.gsub!(match[0], settings.showoff_config[match[1]]) if settings.showoff_config.key?(match[1]) 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 - file = HTMLEntities.new.encode(File.read(File.join(settings.pres_dir, '_files', match[1]))) + 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" - # make a list of sh_highlight classes to include - css = match[2].split.collect {|i| "sh_#{i.downcase}" }.join(' ') - - result.gsub!(match[0], "<pre class=\"#{css}\"><code>#{file}</code></pre>") + result.gsub!(match[0], "<pre class=\"highlight\"><code class=\"#{css}\">#{file}</code></pre>") end result end @@ -710,14 +743,19 @@ parser = CommandlineParser.new html.css('pre').each do |pre| pre.css('code').each do |code| out = code.text + + # Skip this if we've got an empty code block + next if out.empty? + lines = out.split("\n") if lines.first.strip[0, 3] == '@@@' lang = lines.shift.gsub('@@@', '').strip - pre.set_attribute('class', 'sh_' + lang.downcase) if !lang.empty? + pre.set_attribute('class', 'highlight') + code.set_attribute('class', 'language-' + lang.downcase) if !lang.empty? code.content = lines.join("\n") end end end @@ -750,14 +788,10 @@ end html.to_html end def get_slides_html(opts={:static=>false, :pdf=>false, :toc=>false, :supplemental=>nil}) - @slide_count = 0 - @section_major = 0 - @section_minor = 0 - @section_title = settings.showoff_config['name'] sections = ShowOffUtils.showoff_sections(settings.pres_dir, @logger) files = [] if sections data = '' @@ -827,13 +861,10 @@ if static @title = ShowOffUtils.showoff_title(settings.pres_dir) @slides = get_slides_html(:static=>static) @pause_msg = ShowOffUtils.pause_msg - # Identify which languages to bundle for highlighting - @languages = @slides.scan(/<pre class=".*(?!sh_sourceCode)(sh_[\w-]+).*"/).uniq.map{ |w| "sh_lang/#{w[0]}.min.js"} - @asset_path = "./" end # Display favicon in the window if configured @favicon = settings.showoff_config['favicon'] @@ -897,11 +928,11 @@ end def onepage(static=false) @slides = get_slides_html(:static=>static, :toc=>true) @favicon = settings.showoff_config['favicon'] - #@languages = @slides.scan(/<pre class=".*(?!sh_sourceCode)(sh_[\w-]+).*"/).uniq.map{ |w| "/sh_lang/#{w[0]}.min.js"} + erb :onepage end def print(static=false) @slides = get_slides_html(:static=>static, :toc=>true, :print=>true) @@ -953,13 +984,10 @@ def pdf(static=true) @slides = get_slides_html(:static=>static, :pdf=>true) @inline = true - # Identify which languages to bundle for highlighting - @languages = @slides.scan(/<pre class=".*(?!sh_sourceCode)(sh_[\w-]+).*"/).uniq.map{ |w| "/sh_lang/#{w[0]}.min.js"} - html = erb :onepage # TODO make a random filename # Process inline css and js for included images # The css uses relative paths for images and we prepend the file url @@ -1063,16 +1091,21 @@ end end end end - def eval_ruby code - eval(code).to_s - rescue => e - e.message - end + # Load a slide file from disk, parse it and return the text of a code block by index + def get_code_from_slide(path, index) + slide = "#{path}.md" + return unless File.exists? slide + html = process_markdown(slide, File.read(slide), {}) + doc = Nokogiri::HTML::DocumentFragment.parse(html) + + return doc.css('code.execute')[index.to_i].text rescue 'Invalid code block index' + end + # Basic auth boilerplate def protected! unless authorized? response['WWW-Authenticate'] = %(Basic realm="#{@title}: Protected Area") throw(:halt, [401, "Not authorized\n"]) @@ -1131,14 +1164,43 @@ end end end.to_json end - get '/eval_ruby' do - return eval_ruby(params[:code]) if ENV['SHOWOFF_EVAL_RUBY'] + # Evaluate known good code from a slide file on disk. + get '/execute/:lang' do |lang| + return 'Run showoff with -x or --executecode to enable code execution' unless @execute - return "Ruby Evaluation is off. To turn it on set ENV['SHOWOFF_EVAL_RUBY']" + code = get_code_from_slide(params[:path], params[:index]) + + require 'timeout' + require 'open3' # for 1.8 compatibility :/ + begin + Timeout::timeout(settings.showoff_config['timeout']) do + case lang + when 'ruby' + eval(code).to_s + when 'shell' + %x(#{code}) + when 'puppet' + stdout, err = Open3.capture2('puppet', 'apply', '--color=false', '-e', code) + stdout + when 'python' + stdout, err = Open3.capture2('python', '-c', code) + stdout + when 'perl' + stdout, err = Open3.capture2('perl', '-e', code) + stdout + when 'null' + code + else + "No exec handler for #{lang}" + end + end + rescue => e + e.message + end.gsub(/\n/, '<br />') end # provide a callback to trigger a local file editor, but only when called when viewing from localhost. get '/edit/*' do |path| # Docs suggest that old versions of Sinatra might provide an array here, so just make sure. @@ -1314,10 +1376,10 @@ end end not_found do # Why does the asset path start from cwd?? - @asset_path.slice!(/^./) + @asset_path.slice!(/^./) rescue nil @env = request.env erb :'404' end at_exit do