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