Skip to content

Commit

Permalink
Merge pull request #149 from grundprinzip/templating
Browse files Browse the repository at this point in the history
Templating + Fixes for Static and PDF Generation
  • Loading branch information
goncalossilva committed Mar 5, 2012
2 parents 252c22a + 4030a46 commit 580c025
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 35 deletions.
97 changes: 85 additions & 12 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

{
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions example/four/01slide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
!SLIDE[tpl=special]

# A Template #

Really? How many slides? -- ~~~NUM_SLIDES~~~
7 changes: 6 additions & 1 deletion example/showoff.json
Original file line number Diff line number Diff line change
@@ -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"}
]
}
2 changes: 2 additions & 0 deletions example/simple.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div class="border">~~~CONFIG:author~~~@~~~CURRENT_SLIDE~~~</div>
<div class="main">~~~CONTENT~~~<div>
117 changes: 107 additions & 10 deletions lib/showoff.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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">')
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(settings.pres_dir))
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions lib/showoff_utils.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading

0 comments on commit 580c025

Please sign in to comment.