Templating + Fixes for Static and PDF Generation #149

Merged
merged 16 commits into from Mar 5, 2012
View
@@ -84,7 +84,23 @@ the following contents:
That represents two slides, the first contains just a large title, and the
second is faded into view showing the title and three bullets that are then
-incrementally shown. In order for ShowOff to see those slides, your
+incrementally shown. In addition you can configure a certain template for
+this slide overriding the default one:
+
+ !SLIDE[tpl=title]
+
+ # My Presentation #
+
+ !SLIDE bullets incremental
+
+ # Bullet Points #
+
+ * first point
+ * second point
+ * third point
+
+
+In order for ShowOff to see those slides, your
<tt>showoff.json</tt> file needs to look something like this:
{
@@ -95,19 +111,20 @@ incrementally shown. In order for ShowOff to see those slides, your
]
}
-If you have multiple sections in your talk, you can make this json array
-include all the sections you want to show in which order you want to show
-them.
+If you have multiple sections in your talk, you can make this json
+array include all the sections you want to show in which order you
+want to show them. Template configuration is done in
+<tt>showoff.json</tt> as well.
-Instead of a hash, you can use a plain string as an entry in the `sections`
-section of `showoff.json`.
-And if that plain string starts with '#' then it is interpreted not as a
-filename, but as markdown. This is used for inserting interstitial slides
-or notes -- for instance, Alex Chaffee's
-[Ruby Notes](http://github.com/alexch/ruby_notes)
-uses it to insert lab instructions between lecture slide sections, which may
-vary from venue to venue.
+Instead of a hash, you can use a plain string as an entry in the
+`sections` section of `showoff.json`. And if that plain string starts
+with '#' then it is interpreted not as a filename, but as
+markdown. This is used for inserting interstitial slides or notes --
+for instance, Alex Chaffee's [Ruby
+Notes](http://github.com/alexch/ruby_notes) uses it to insert lab
+instructions between lecture slide sections, which may vary from venue
+to venue.
If you want to keep the ability to emit an HTML document from your
Markdown source file -- say, for a TextMate preview or a GitHub rendering
@@ -447,6 +464,62 @@ You'll then need to install a version of wkhtmltopdf available at the {wkhtmltop
Then restart showoff, and navigate to <tt>/pdf</tt> (e.g. http://localhost/pdf) of your presentation and a PDF will be generated with the browser.
+= ShowOff Templates
+
+Templates can come handy if you need more than what you can achieve
+via CSS. To configure templates you'll have to specify them in the
+<tt>showoff.json</tt> by adding an entry called "templates". This
+entry is an object where you can specify as many templates as you
+want. The default template is marked with the "default" key.
+
+ {
+ "name": "Something",
+ "description": "Example Presentation",
+ "templates" : {
+ "default" : "tpl1.tpl",
+ "special" : "tpl2.tpl"
+ },
+ "sections": [
+ {"section":"one"}
+ ]
+ }
+
+If the "default" key is not given, no template will be used for the
+default slide. If you want to apply a certain layout to a slide you
+have to specify it in the slide header:
+
+ !SLIDE[tpl=special]
+ # Header
+
+== Template Commands
+
+You can place content anywhere in your template, but you have to
+explicitly mark the location using a special command:
+
+[~~~CONTENT~~~] is replaced by the slide content
+
+[~~~CURRENT_SLIDE~~~] is replaced by the current slide number
+
+[~~~NUM_SLIDES~~~] is replaced by the total number of slides
+
+[~~~CONFIG:*~~~] is replaced by any value (*) from the
+ <tt>showoff.json</tt> configuration. This can be used to
+ specify an author, venue etc. A simple example would be
+ <tt>~~~CONFIG:author~~~</tt>
+
+
+The usage of these replacements is not limited to templates, but
+anywhere in your slides.
+
+== Template Hints
+
+You can basically put everything you want into templates, but you
+should make sure that the CSS is applied fine. The best way to apply a
+custom layout is to create a container that uses absolute positioning
+and has width and height set to 100% which are then derived from the
+parent slide element.
+
+
= Completion
== ZSH completion
View
@@ -0,0 +1,5 @@
+!SLIDE[tpl=special]
+
+# A Template #
+
+Really? How many slides? -- ~~~NUM_SLIDES~~~
View
@@ -1,9 +1,14 @@
{
"name": "Something",
"description": "Example Presentation",
+ "author": "Foo Bar John",
+ "templates" : {
+ "special" : "simple.tpl"
+ },
"sections": [
{"section":"one"},
{"section":"two"},
- {"section":"three"}
+ {"section":"three"},
+ {"section":"four"}
]
}
View
@@ -0,0 +1,2 @@
+<div class="border">~~~CONFIG:author~~~@~~~CURRENT_SLIDE~~~</div>
+<div class="main">~~~CONTENT~~~<div>
View
@@ -33,6 +33,9 @@ class ShowOff < Sinatra::Application
set :verbose, false
set :pres_dir, '.'
set :pres_file, 'showoff.json'
+ set :page_size, "Letter"
+ set :pres_template, nil
+ set :showoff_config, nil
def initialize(app=nil)
super(app)
@@ -51,6 +54,21 @@ def initialize(app=nil)
if (settings.pres_file)
ShowOffUtils.presentation_config_file = settings.pres_file
end
+
+ # Load configuration for page size and template from the
+ # configuration JSON file
+ if File.exists?(ShowOffUtils.presentation_config_file)
+ showoff_json = JSON.parse(File.read(ShowOffUtils.presentation_config_file))
+ settings.showoff_config = showoff_json
+
+ # Set options for template and page size
+ settings.page_size = showoff_json["page-size"] || "Letter"
+ settings.pres_template = showoff_json["templates"]
+ end
+
+
+ @logger.debug settings.pres_template
+
@cached_image_size = {}
@logger.debug settings.pres_dir
@pres_name = settings.pres_dir.split('/').pop
@@ -96,9 +114,21 @@ def preshow_files
# todo: move more behavior into this class
class Slide
- attr_reader :classes, :text
- def initialize classes = ""
- @classes = ["content"] + classes.strip.chomp('>').split
+ attr_reader :classes, :text, :tpl
+ def initialize( context = "")
+
+ @tpl = "default"
+ @classes = ["content"]
+
+ # Parse the context string for options and content classes
+ if context and context.match(/(\[(.*?)\])?(.*)/)
+
+ options = ShowOffUtils.parse_options($2)
+ @tpl = options["tpl"] if options["tpl"]
+ @classes += $3.strip.chomp('>').split if $3
+
+ end
+
@text = ""
end
def <<(s)
@@ -126,7 +156,8 @@ def process_markdown(name, content, static=false, pdf=false)
until lines.empty?
line = lines.shift
if line =~ /^<?!SLIDE(.*)>?/
- slides << (slide = Slide.new($1))
+ ctx = $1 ? $1.strip : $1
+ slides << (slide = Slide.new(ctx))
else
slide << line
end
@@ -139,6 +170,7 @@ def process_markdown(name, content, static=false, pdf=false)
seq = 1
end
slides.each do |slide|
+ @slide_count += 1
md = ''
content_classes = slide.classes
@@ -151,27 +183,70 @@ def process_markdown(name, content, static=false, pdf=false)
@logger.debug "id: #{id}" if id
@logger.debug "classes: #{content_classes.inspect}"
@logger.debug "transition: #{transition}"
+ @logger.debug "tpl: #{slide.tpl} " if slide.tpl
# create html
md += "<div"
md += " id=\"#{id}\"" if id
md += " class=\"slide\" data-transition=\"#{transition}\">"
+
+
+ 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.exists?(settings.pres_template[slide.tpl])
+ template = File.open(settings.pres_template[slide.tpl], "r").read()
+ end
+ end
+
+ # Extract the content of the slide
+ content = ""
if seq
- md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}/#{seq.to_s}\">\n"
- seq += 1
+ content += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}/#{seq.to_s}\">\n"
else
- md += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}\">\n"
+ content += "<div class=\"#{content_classes.join(' ')}\" ref=\"#{name}\">\n"
end
sl = Tilt[:markdown].new { slide.text }.render
sl = update_image_paths(name, sl, static, pdf)
- md += sl
- md += "</div>\n"
+ content += sl
+ content += "</div>\n"
+
+ # Apply the template to the slide and replace the key with
+ # content of the slide
+ md += process_content_for_replacements(template.gsub(/~~~CONTENT~~~/, content), @slide_count)
+
+ # Apply other configuration
+
md += "</div>\n"
final += update_commandline_code(md)
final = update_p_classes(final)
+
+ if seq
+ seq += 1
+ end
end
final
end
+ # This method processes the content of the slide and replaces
+ # content markers with their actual value information
+ def process_content_for_replacements(content, seq)
+ result = content.gsub("~~~CURRENT_SLIDE~~~", seq.to_s)
+ # Now check for any kind of options
+ content.scan(/(~~~CONFIG:(.*?)~~~)/).each do |match|
+ result.gsub!(match[0], settings.showoff_config[match[1]]) if settings.showoff_config.key?(match[1])
+ end
+
+ result
+ end
+
+ def process_content_for_all_slides(content, num_slides)
+ content.gsub("~~~NUM_SLIDES~~~", num_slides.to_s)
+ end
+
+
# 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">')
@@ -260,6 +335,7 @@ def update_commandline_code(slide)
end
def get_slides_html(static=false, pdf=false)
+ @slide_count = 0
sections = ShowOffUtils.showoff_sections(settings.pres_dir, @logger)
files = []
if sections
@@ -280,7 +356,7 @@ def get_slides_html(static=false, pdf=false)
end
end
end
- data
+ process_content_for_all_slides(data, @slide_count)
end
def inline_css(csses, pre = nil)
@@ -319,6 +395,11 @@ def index(static=false)
if static
@title = ShowOffUtils.showoff_title
@slides = get_slides_html(static)
+
+ # Identify which languages to bundle for highlighting
+ @languages = []
+ @languages += @slides.scan(/<pre class="(sh_.*?\w)"/).uniq.map{ |w| "sh_lang/#{w[0]}.min.js"}
+
@asset_path = "./"
end
erb :index
@@ -377,9 +458,22 @@ def onepage(static=false)
def pdf(static=true)
@slides = get_slides_html(static, true)
@no_js = false
+
+ # Identify which languages to bundle for highlighting
+ @languages = []
+ @languages += @slides.scan(/<pre class="(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
+ html.gsub!(/url\(([^\/].*?)\)/) do |s|
+ "url(file://#{settings.pres_dir}/#{$1})"
+ end
+
+ # Todo fix javascript path
+
# 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)
@@ -403,6 +497,7 @@ def self.do_static(what)
path = showoff.instance_variable_get(:@root_path)
logger = showoff.instance_variable_get(:@logger)
data = showoff.send(what, true)
+
if data.is_a?(File)
FileUtils.cp(data.path, "#{name}.pdf")
else
@@ -479,7 +574,9 @@ def eval_ruby code
@title = ShowOffUtils.showoff_title
what = params[:captures].first
what = 'index' if "" == what
+
@asset_path = (env['SCRIPT_NAME'] || '').gsub(/\/?$/, '/').gsub(/^\//, '')
+
if (what != "favicon.ico")
data = send(what)
if data.is_a?(File)
View
@@ -1,4 +1,28 @@
class ShowOffUtils
+
+ # Helper method to parse a comma separated options string and stores
+ # the result in a dictionrary
+ #
+ # Example:
+ #
+ # "tpl=hpi,title=Over the rainbow"
+ #
+ # will be stored as
+ #
+ # { "tpl" => "hpi", "title" => "Over the rainbow" }
+ def self.parse_options(option_string="")
+ result = {}
+
+ if option_string
+ option_string.split(",").each do |element|
+ pair = element.split("=")
+ result[pair[0]] = pair.size > 1 ? pair[1] : nil
+ end
+ end
+
+ result
+ end
+
def self.presentation_config_file
@presentation_config_file ||= 'showoff.json'
end
Oops, something went wrong.