class Export
Renders a self-contained, print-ready HTML page containing all slides for PDF export.
All slides are rendered in a single page with CSS break-after: page, so a single
WebDriver print() call produces a multi-page PDF without any merging step.
Definitions
PageSize
Holds slide canvas and notes panel dimensions, and converts between CSS pixels and centimetres for WebDriver print (96 px/inch × 2.54 cm/inch).
Implementation
PageSize = Struct.new(:slide_width_px, :slide_height_px, :notes_height_px, keyword_init: true) do
PX_PER_CM = 96.0 / 2.54
def slide_width_cm = (slide_width_px / PX_PER_CM).round(4)
def slide_height_cm = (slide_height_px / PX_PER_CM).round(4)
def notes_height_cm = (notes_height_px / PX_PER_CM).round(4)
end
def self.options_from_query(query)
Parse export options from a URL query string.
Signature
-
parameter
queryString | Nil The raw query string, e.g.
"notes=true&speaker=true".-
returns
Hash Options suitable for passing to
.new.
Implementation
def self.options_from_query(query)
return {} unless query
params = URI.decode_www_form(query).to_h
page_size = PageSize.new(
slide_width_px: (params["slide_width_px"] || PageSize::DEFAULT.slide_width_px).to_i,
slide_height_px: (params["slide_height_px"] || PageSize::DEFAULT.slide_height_px).to_i,
notes_height_px: (params["notes_height_px"] || PageSize::DEFAULT.notes_height_px).to_i,
)
{
notes: params["notes"] != "false",
speaker: params["speaker"] != "false",
timing: params["timing"] != "false",
page_size: page_size,
}
end
def initialize(presentation:, page_size: PageSize::DEFAULT, notes: true, speaker: true, timing: true)
Signature
-
parameter
presentationPresentation The presentation to export.
-
parameter
page_sizePageSize Slide canvas and notes panel dimensions.
-
parameter
notesBoolean Whether to include presenter notes below each slide.
-
parameter
speakerBoolean Whether to include the speaker name.
-
parameter
timingBoolean Whether to include per-slide timing information.
Implementation
def initialize(presentation:, page_size: PageSize::DEFAULT, notes: true, speaker: true, timing: true)
@presentation = presentation
@page_size = page_size
@notes = notes
@speaker = speaker
@timing = timing
@renderer = SlideRenderer.new
end
attr :page_size
Signature
-
attribute
PageSize The slide canvas and notes panel dimensions.
attr :notes
Signature
-
attribute
Boolean Whether presenter notes are included.
attr :speaker
Signature
-
attribute
Boolean Whether speaker names are included.
attr :timing
Signature
-
attribute
Boolean Whether slide timing is included.
def render_slide(slide)
Render a single slide to an HTML string.
Signature
-
parameter
slideSlide The slide to render.
-
returns
XRB::MarkupString
Implementation
def render_slide(slide)
@renderer.render_to_html(slide)
end
def render_notes(slide, index)
Render the notes panel for a single slide.
Signature
-
parameter
slideSlide The slide to render notes for.
-
parameter
indexInteger The 1-based slide index.
-
returns
XRB::MarkupString
Implementation
def render_notes(slide, index)
builder = XRB::Builder.new
# Meta strip — mirrors the presenter view's timing bar.
builder.tag(:div, class: "export-meta") do
builder.tag(:span, class: "export-slide-number") do
builder.text("Slide #{index} of #{@presentation.slide_count}")
end
builder.tag(:span, class: "export-filename") do
builder.tag(:code){builder.text(File.basename(slide.path))}
end
if @timing
elapsed = expected_time_at(index - 1)
builder.tag(:span, class: "export-elapsed") do
builder.text("Elapsed: #{format_duration(elapsed)}")
end
builder.tag(:span, class: "export-duration") do
builder.text("Slide: #{format_duration(slide.duration)}")
end
end
if @speaker && slide.speaker
builder.tag(:span, class: "export-speaker") do
builder.tag(:span, class: "speaker-label"){builder.text("🎤")}
builder.text(" #{slide.speaker}")
end
end
end
# Notes panel — mirrors the presenter view's .notes section.
builder.tag(:div, class: "notes") do
builder.tag(:div, class: "notes-content") do
if slide.notes && !slide.notes.empty?
builder.raw(slide.notes.to_html)
else
builder.tag(:p, class: "no-notes"){builder.text("No presenter notes for this slide.")}
end
end
end
XRB::MarkupString.raw(builder.to_s)
end
def format_duration(seconds)
Format a duration in seconds as MM:SS.
Signature
-
parameter
secondsInteger Duration in seconds.
-
returns
String
Implementation
def format_duration(seconds)
minutes = seconds / 60
secs = seconds % 60
format("%02d:%02d", minutes, secs)
end
def expected_time_at(index)
Calculate the expected elapsed time at the start of the given slide index.
Signature
-
parameter
indexInteger Zero-based slide index.
-
returns
Integer Elapsed seconds up to (but not including) that slide.
Implementation
def expected_time_at(index)
@presentation.slides.first(index).sum(&:duration)
end
def call
Render the full export page to an HTML string.
Signature
-
returns
String
Implementation
def call
TEMPLATE.to_string(self)
end
def slides
The slides in the presentation.
Signature
-
returns
Array(Slide)
Implementation
def slides
@presentation.slides
end