lib/showoff.rb in showoff-0.4.2 vs lib/showoff.rb in showoff-0.6.0
- old
+ new
@@ -1,14 +1,16 @@
require 'rubygems'
require 'sinatra/base'
require 'json'
require 'nokogiri'
require 'fileutils'
+require 'logger'
here = File.expand_path(File.dirname(__FILE__))
require "#{here}/showoff_utils"
require "#{here}/princely"
+require "#{here}/commandline_parser"
begin
require 'RMagick'
rescue LoadError
$stderr.puts 'image sizing disabled - install rmagick'
@@ -25,42 +27,62 @@
rescue LoadError
require 'bluecloth'
Object.send(:remove_const,:Markdown)
Markdown = BlueCloth
end
-require 'pp'
class ShowOff < Sinatra::Application
- Version = VERSION = '0.4.2'
+ Version = VERSION = '0.6.0'
attr_reader :cached_image_size
set :views, File.dirname(__FILE__) + '/../views'
set :public, File.dirname(__FILE__) + '/../public'
- set :pres_dir, 'example'
def initialize(app=nil)
super(app)
- puts dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
- if Dir.pwd == dir
- options.pres_dir = dir + '/example'
+ @logger = Logger.new(STDOUT)
+ @logger.formatter = proc { |severity,datetime,progname,msg| "#{progname} #{msg}\n" }
+ @logger.level = options.verbose ? Logger::DEBUG : Logger::WARN
+
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+ @logger.debug(dir)
+
+ showoff_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+ if Dir.pwd == showoff_dir
+ options.pres_dir = "#{showoff_dir}/example"
@root_path = "."
else
- options.pres_dir = Dir.pwd
+ options.pres_dir ||= Dir.pwd
@root_path = ".."
end
+ options.pres_dir = File.expand_path(options.pres_dir)
+ if (options.pres_file)
+ puts "Using #{options.pres_file}"
+ ShowOffUtils.presentation_config_file = options.pres_file
+ end
+ puts "Serving presentation from #{options.pres_dir}"
@cached_image_size = {}
- puts options.pres_dir
+ @logger.debug options.pres_dir
@pres_name = options.pres_dir.split('/').pop
+ require_ruby_files
end
+ def require_ruby_files
+ Dir.glob("#{options.pres_dir}/*.rb").map { |path| require path }
+ end
+
helpers do
def load_section_files(section)
section = File.join(options.pres_dir, section)
- files = Dir.glob("#{section}/**/*").sort
- pp files
+ files = if File.directory? section
+ Dir.glob("#{section}/**/*").sort
+ else
+ [section]
+ end
+ @logger.debug files
files
end
def css_files
Dir.glob("#{options.pres_dir}/*.css").map { |path| File.basename(path) }
@@ -68,50 +90,84 @@
def js_files
Dir.glob("#{options.pres_dir}/*.js").map { |path| File.basename(path) }
end
+
def preshow_files
Dir.glob("#{options.pres_dir}/_preshow/*").map { |path| File.basename(path) }.to_json
end
- def process_markdown(name, content, static=false)
- slides = content.split(/^<?!SLIDE/)
- slides.delete('')
+ # todo: move more behavior into this class
+ class Slide
+ attr_reader :classes, :text
+ def initialize classes = ""
+ @classes = ["content"] + classes.strip.chomp('>').split
+ @text = ""
+ end
+ def <<(s)
+ @text << s
+ @text << "\n"
+ end
+ def empty?
+ @text.strip == ""
+ end
+ end
+
+
+ def process_markdown(name, content, static=false, pdf=false)
+
+ # 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")
+ puts "#{name}: #{lines.length} lines"
+ slides = []
+ slides << (slide = Slide.new)
+ until lines.empty?
+ line = lines.shift
+ if line =~ /^<?!SLIDE(.*)>?/
+ slides << (slide = Slide.new($1))
+ else
+ slide << line
+ end
+ end
+
+ slides.delete_if {|slide| slide.empty? }
+
final = ''
if slides.size > 1
seq = 1
end
slides.each do |slide|
md = ''
- # extract content classes
- lines = slide.split("\n")
- content_classes = lines.shift.strip.chomp('>').split rescue []
- slide = lines.join("\n")
- # add content class too
- content_classes.unshift "content"
+ 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 }
- puts "id: #{id}" if id
- puts "classes: #{content_classes.inspect}"
- puts "transition: #{transition}"
+ @logger.debug "id: #{id}" if id
+ @logger.debug "classes: #{content_classes.inspect}"
+ @logger.debug "transition: #{transition}"
# create html
md += "<div"
md += " id=\"#{id}\"" if id
md += " class=\"slide\" data-transition=\"#{transition}\">"
if seq
md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}/#{seq.to_s}\">\n"
seq += 1
else
md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}\">\n"
end
- sl = Markdown.new(slide).to_html
- sl = update_image_paths(name, sl, static)
+ sl = Markdown.new(slide.text).to_html
+ sl = update_image_paths(name, sl, static, pdf)
md += sl
md += "</div>\n"
md += "</div>\n"
final += update_commandline_code(md)
final = update_p_classes(final)
@@ -122,18 +178,18 @@
# find any lines that start with a <p>.(something) and turn them into <p class="something">
def update_p_classes(markdown)
markdown.gsub(/<p>\.(.*?) /, '<p class="\1">')
end
- def update_image_paths(path, slide, static=false)
+ def update_image_paths(path, slide, static=false, pdf=false)
paths = path.split('/')
paths.pop
path = paths.join('/')
replacement_prefix = static ?
- %(img src="file://#{options.pres_dir}/#{path}) :
+ ( pdf ? %(img src="file://#{options.pres_dir}/#{path}) : %(img src="./file/#{path}) ) :
%(img src="/image/#{path})
- slide.gsub(/img src=\"(.*?)\"/) do |s|
+ slide.gsub(/img src=\"([^\/].*?)\"/) do |s|
img_path = File.join(path, $1)
w, h = get_image_size(img_path)
src = %(#{replacement_prefix}/#{$1}")
if w && h
src << %( width="#{w}" height="#{h}")
@@ -144,69 +200,90 @@
if defined?(Magick)
def get_image_size(path)
if !cached_image_size.key?(path)
img = Magick::Image.ping(path).first
- cached_image_size[path] = [img.columns, img.rows]
+ # don't set a size for svgs so they can expand to fit their container
+ if img.mime_type == 'image/svg+xml'
+ cached_image_size[path] = [nil, nil]
+ else
+ cached_image_size[path] = [img.columns, img.rows]
+ end
end
cached_image_size[path]
end
else
def get_image_size(path)
end
end
def update_commandline_code(slide)
html = Nokogiri::XML.parse(slide)
+ parser = CommandlineParser.new
html.css('pre').each do |pre|
pre.css('code').each do |code|
out = code.text
lines = out.split("\n")
- if lines.first[0, 3] == '@@@'
+ if lines.first.strip[0, 3] == '@@@'
lang = lines.shift.gsub('@@@', '').strip
- pre.set_attribute('class', 'sh_' + lang)
+ pre.set_attribute('class', 'sh_' + lang.downcase)
code.content = lines.join("\n")
end
end
end
html.css('.commandline > pre > code').each do |code|
out = code.text
- lines = out.split(/^\$(.*?)$/)
- lines.delete('')
code.content = ''
- while(lines.size > 0) do
- command = lines.shift
- result = lines.shift
- c = Nokogiri::XML::Node.new('code', html)
- c.set_attribute('class', 'command')
- c.content = '$' + command
- code << c
- c = Nokogiri::XML::Node.new('code', html)
- c.set_attribute('class', 'result')
- c.content = result
- code << c
+ 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.root.to_s
end
- def get_slides_html(static=false)
- sections = ShowOffUtils.showoff_sections(options.pres_dir)
+ def get_slides_html(static=false, pdf=false)
+ sections = ShowOffUtils.showoff_sections(options.pres_dir, @logger)
files = []
if sections
+ data = ''
sections.each do |section|
- files << load_section_files(section)
+ if section =~ /^#/
+ name = section.each_line.first.gsub(/^#*/,'').strip
+ data << process_markdown(name, "<!SLIDE subsection>\n" + section, static, pdf)
+ else
+ files = []
+ files << load_section_files(section)
+ files = files.flatten
+ files = files.select { |f| f =~ /.md/ }
+ files.each do |f|
+ fname = f.gsub(options.pres_dir + '/', '').gsub('.md', '')
+ data << process_markdown(fname, File.read(f), static, pdf)
+ end
+ end
end
- files = files.flatten
- files = files.select { |f| f =~ /.md/ }
- data = ''
- files.each do |f|
- fname = f.gsub(options.pres_dir + '/', '').gsub('.md', '')
- data += process_markdown(fname, File.read(f), static)
- end
end
data
end
def inline_css(csses, pre = nil)
@@ -295,11 +372,11 @@
@slides = get_slides_html(static)
erb :onepage
end
def pdf(static=true)
- @slides = get_slides_html(static)
+ @slides = get_slides_html(static, true)
@no_js = false
html = erb :onepage
# TODO make a random filename
# PDFKit.new takes the HTML and any options for wkhtmltopdf
@@ -361,11 +438,35 @@
data.scan(/img src=\".\/file\/(.*?)\"/).flatten.each do |path|
dir = File.dirname(path)
FileUtils.makedirs(File.join(file_dir, dir))
FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
end
+ # copy images from css too
+ Dir.glob("#{pres_dir}/*.css").each do |css_path|
+ File.open(css_path) do |file|
+ data = file.read
+ data.scan(/url\((.*)\)/).flatten.each do |path|
+ @logger.debug path
+ dir = File.dirname(path)
+ FileUtils.makedirs(File.join(file_dir, dir))
+ FileUtils.copy(File.join(pres_dir, path), File.join(file_dir, path))
+ end
+ end
+ end
end
end
+
+ def eval_ruby code
+ eval(code).to_s
+ rescue => e
+ e.message
+ end
+
+ get '/eval_ruby' do
+ return eval_ruby(params[:code]) if ENV['SHOWOFF_EVAL_RUBY']
+
+ return "Ruby Evaluation is off. To turn it on set ENV['SHOWOFF_EVAL_RUBY']"
+ end
get %r{(?:image|file)/(.*)} do
path = params[:captures].first
full_path = File.join(options.pres_dir, path)
send_file full_path