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